Hacker News new | past | comments | ask | show | jobs | submit login

I wish they would stop introducing more magic syntaxes.



I 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)
          }
        })
      }
    }


> 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.


Maybe 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")
        }
      }
    }


Except it is. var body is a variable declaration. some View {…} is nested closures with a single statement each so the return keyword is implied. Just add return in your second example


SwiftUI'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.


Right. But it strikes me as one of those things that really helps new developers become productive quickly -- to then, it looks just like code, they probably don't realize it's not code.

But once your app hits a certain size, the abstraction will inevitably leak somewhere, and now you'll need to step back, and learn all about macros and viewbuilders to be able to fix your issues.

Probably worth it! They enable such a wonderful way of drawing a UI. But it's not a zero-cost abstraction, and I like that Go has eschewed _anything_ like that at all. There's no IEnumerable with yield return that compiles into a state matchine. No ViewBuilder that's a macro, not code. There's no Ruby-like adding methods at runtime (Model.find_by_bar_code???). It's all just plain code right in front of you.

They both have their strengths. I think Go made the right trade-off for simple high-throughput microservices, as they're trivial to understand and will never surprise you. And Swift made the right tradeoffs for a UI—-it would be painful to write it in Go.

My reaction as someone who used Swift extensively in 2015, quit, and now came back in 2024 is "wow, did they add too many features? that's a lot of quirks to learn." FWIW I don't feel the same way about TypeScript or C#.

It just feels at first glance like Swift let every feature in the door. And now we've got an awfully complex and laggy compiler in my experience, especially when you run into one of the type inferencing pitfalls leading to an exponential algorithm. Go explicitly made fast compilation a design goal and lost a lot of features to it.


It is code though. It’s a bunch of nested closures with an implicit return


Result builders aren't macros.


@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.


> 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.


I 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.


Is there a specific one you’re objecting to in 6?


Since 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.


The issue is that `~Copyable` is basically an anti-protocol.

With a generics definition like `some Drinkable` you are _restricting_ the set of suitable types (from any type to only the ones implementing `Drinkable`) which then _expands_ the available functionality (the method use() becomes available). From the perspective of type `Water`, it's conformance to `Drinkable` expands the functionality.

The language designers then get in a pickle if some functionality was assumed to exist for all types (e.g. `Copyable`)! By "conforming" to `~Copyable` you are _removing_ functionality. The type can NOT be copied which was assumed to be universally true before. Now, a generics definition like `some ~Copyable` actually _expands_ the set of suitable types (because Copyable types can be used as if they were non-copyable) and reduces the available functionality. It's the inverse of a regular protocol!

It becomes extra confusing if you combine `some Drinkable & ~Copyable` where `Drinkable` and `~Copyable` work in opposite directions.

This problem also exists in Rust. `Sized` is a trait (aka protocol) that basically all normal types implement, but you can opt-out by declaring `!Sized`. Then, if you actually want to include all types in your generics, you need to write `?Sized` (read: Maybe-Sized).


Here’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

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.


Yeah after thinking about it a bit more it does make more sense to me. The primary gap I had was, as you allude here:

> You can use a copyable anywhere you’d use a non-copyable since there is no restriction.

Effectively copyable always conforms to non-copyable, just not the other way around.

And the compiler effectively automatically notates literally everything with Copyable, so you need the explicit (& ~Copyable) in the function definition so you're still able to define functions within a ~Copyable protocol that have Copyable semantics.

It's very in the weeds, and I still don't like it (I would have preferred NotCopyable since the ~, especially next to the &, directly implies something like bitwise operators), but I guess custom ownership is itself very in the weeds and you will have to think about it hard no matter what approach is taken. I would have expected custom ownership to be fundamentally incompatible with Swift, but clearly it's here; I should probably read about it more so I have a more clear understanding.

(I also didn't realize & was extant syntax).


Yeah, I’ll admit it’s hard to get your head around. I had to think about it a couple of times just writing that explanation.

It took me a couple of minutes to figure out why it was in the function definition. I guess it had to be but that wasn’t obvious to me at all at first.

> And the compiler effectively automatically notates literally everything

Right. Just like how all classes in Java extend Object even though you don’t have to literally write it.

I believe they’re still working on a more Rust-like borrowing system, but I could be wrong. I know this helped them implement parts of the standard library much better because they could make assumptions that you can’t make with copyable objects.

I do get your point about just calling it NotCopyable. I don’t actually know why they settled on the name they did, I didn’t ever try to look that up. Maybe it’s because it’s a special thing that requires compiler machinery and there’s no way for you to make an equivalent?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: