44 ms·
I wish they would stop introducing more magic syntaxes.
by skrrtww 2y ago
I wish they would stop introducing more magic syntaxes.
- schrodinger 2y agoI agree! I'm a Go programmer, and while I do wish it had some more features at times, Swift is an example of how it can easily go out of control and ruin a promising language. For example tests, there's so much magic. How do I know it runs the test for each item in the arguments array? What if there were multiple arguments? After using Go for close to a decade now, I'm really seeing the wisdom of avoiding magic, and making your testing code the same language as your building code! Compare: Swift: @Test("Continents mentioned in videos", arguments: [ "A Beach", "By the Lake", "Camping in the Woods" ]) func mentionedContinents(videoName: String) async throws { let videoLibrary = try await VideoLibrary() let video = try #require(await videoLibrary.video(named: videoName)) #expect(video.mentionedContinents.count <= 3) } Go: func TestMentionedContinents(t *testing.T) { tests := []struct{ Name string }{ {"A Beach"}, {"By the Lake"}, {"Camping in the Woods"}, } for _, tt := range tests { video, err := library.FindVideoByName(tt.Name) if err != nil { t.Fatalf("failed to get video: %v", err) } if len(video.MentionedContinents) > 3 { t.Errorf("video %q mentions more than 3 continents", tt.Name) } } } Go with timeout handling in case the FindVideo function takes too long (idk Swift magic well enough to know if it'd do this automatically!) func TestMentionedContinents(t *testing.T) { tests := []struct{ Name string }{ {"A Beach"}, {"By the Lake"}, {"Camping in the Woods"}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond) defer cancel() video, err := library.FindVideoByName(ctx, tt.Name) if err != nil { t.Fatalf("failed to get video: %v", err) } if len(video.MentionedContinents) > 3 { t.Errorf("video %q mentions more than 3 continents", tt.Name) } }) } }
- rescripting 2y ago> How do I know it runs the test for each item in the arguments array I mean the APIs aren’t magic; you can “inspect macro” to see what code is generated at compile time which boils down to something similar to the Go code with better ergonomics.
- schrodinger 2y agoI don't know if I'd agree better ergonomics in this case, since you lose a lot. What if you wanted to load your test cases from a CSV? e.g. you had a two column CSV with 1,000 words, first column mixed case, second case lowercase. They're mixed languages, with unicode oddities sprinkled in. So it really is worth testing against such a corpus. In Go, I could simply open a csv checked into the codebase and use it for my test cases. I'm sure it's possible, but you probably have to break way from the macro (which I argue doesn't add anything) and take a completely different approach. In Go, it's JUST CODE. Again, I really like Swift (besides xcode performance at times… ugh!). It's possible to find flaws in both languages yes still like them. Swift knocks Go out of the water in so many ways. But I'm scarred from ruby on rails magic growing and growing until you had to be an expert in the magic to write code, when the point was for the magic to make it easier.
- tiltowait 2y ago> How do I know it runs the test for each item in the arguments array? At the risk of coming across a bit rudely: this feels analogous to asking “how do I know `for _, tt := range tests` loops over every element in the array?” Both are language/syntactic constructs you have to learn.
- schrodinger 2y agoMaybe I'm being a bit harsh myself, but with the Go code, it's the same syntax that I use whenever I would write a for loop anywhere else in my production codebase. It's not something special to testing, it's literally _just code_. However, I do like Swift, I in fact single-handledy wrote an entire iPhone app used by 10s of thousands of people on it and there were a lot of wonderful things, like nullability being "solved", and smart enums etc. This isn't a language war, I like them both, and could point out flaws in either just as easily. I just feel like Swift has a bit too low of a bar for adding new features, which leads to a lot of nice things, but also a lot of functionality bloat. I can look at Go written 10 years ago and it'll largely be the same as how it'd be written today; with Swift, it's night and day. I built the aforementioned app in 2014 to 2017 (mostly the first year), and there's so much I don't recognize. I think one of the things that bothered me the most where I feel they sort of "jumped the shark" is with ViewBuilders. It looks like code, acts like it 99% of the time, but it isn't. Does that `var body: some View { ... }` return a view anywhere? No, and it'll break if you try! It's a whole different concept to admittedly offer a very nice experience emnulating React with idempotent views. But still, it's awfully strange that this works: struct IntroView: View { @State private var text = "Yes" var body: some View { VStack { Text(text) Button("Toggle") { text = text == "Yes" ? "No" : "Yes" } } } } But this does not, because it's not _actually_ code. struct IntroView: View { @State private var text = "Yes" var body: some View { VStack { Text(text) Button("Toggle") { text = text == "Yes" ? "No" : "Yes" } print("debugging line") } } }
- Aloisius 2y agoSwiftUI's ViewBuilder isn't part of the Swift language. It's a macro implemented in Swift. Now whether macros themselves were a good idea or not, that's an entirely different question.
- Aloisius 2y ago@Test, #require and #expect are just macros. You can expand them if you want to see what they do (or just look at the swift-testing code itself). Perhaps I'm just used to Python unit testing with similar decorators. Presumably, if you need to pass in two arguments, you'd either pass arguments: an array of tuples or a tuple of arrays for combinatorial testing.
- MBCook 2y agoIs there a specific one you’re objecting to in 6?
- skrrtww 2y agoSince you asked: The provided `drinkable` example I think is pretty bad and it's very surprising to me that this is a headline feature. protocol Drinkable: ~Copyable { consuming func use() } struct Coffee: Drinkable, ~Copyable { /\* ... */ } struct Water: Drinkable { /* ... \*/ } func drink(item: consuming some Drinkable & ~Copyable) { item.use() } drink(item: Coffee()) drink(item: Water()) Here we have a drink() function that either accepts something `Copyable` OR non-Copyable (uh, I mean, `~Copyable`) and either consumes it...or doesn't? That seems to me like a fountain for logic errors if the function behaves completely differently with the same signature (which is, in fact, explicitly labeled `consuming`). It seems like it should not just compile if you try to call this with a `Copyable` type like Water, but it does. The syntax for representing this complex and weird concept of "maybe consuming" being `consuming some Drinkable & ~Copyable` is also just totally gross. Why are we using bitwise operation syntax for some weird and logically incoherent kludge? We cannot apply these & and ~ operators indiscriminately, and they do not mean the same thing that they logically mean in any other context, but this function definition definitely implies that they do.
- MBCook 2y agoHere’s my take. I haven’t used this feature yet so I haven’t dug in too deep. drink() takes a Drinkable. A Drinkables can be non-copyable. Copyable is the default, so it has to mark itself as accepting non-copyables. Coffee is non-copyable. Water doesn’t say which means it’s copyable (the default). You can use a copyable anywhere you’d use a non-copyable since there is no restriction. So since drink can take non-copyables it can also use copyables. I’m guessing the function definition has to list non-copyable otherwise it would only allow copyable drinks since the default is all variables are copyable. “consuming some” means the function takes over the ownership of the non-copyable value. It’s no longer usable in the scope that calls drink(). For the copyable value I’m not sure but since they can be copied I could see that going either way. On syntax: Yeah it’s a bit weird, but there was a big debate about it. They wanted something easy to read and fast to use. NotCopyable<Drinkable> is really clear but typing it over and over would get real old. ~ is not special syntax. My understand is “~Copyable” is the name of the type. You can’t just put ~ in front of anything, like ~Drinkable. But since that’s the syntax used for bitwise negation it’s pretty guessable. & is existing syntax for multiple type assertions. You can see the evolution in this Stack Overflow answer: https://stackoverflow.com/a/24089278 https://stackoverflow.com/a/24089278 Seems to read like C to me. It has to be Drinkable and not Copyable. Like I said I haven’t gotten to use this yet, but it seems like a nice improvement. And I know it’s a step in the path towards making it easier to do safe asynchronous programming, object lifetimes, and other features.