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

Super cool! I absolutely adore swift, it has so many simple features I love that I am sometimes surprised to find are missing in other languages. (ex off the top of my head: switching on enum's with associated values is super powerful in Swift and I was surprised Dart was completely missing that when I tried flutter a while back?)

Ex:

enum MyEnum<T> {

   case one

   case two(twosChild: Child)

   case three(threesGenericChild: T)

   case four(foursCallbackChild: () -> Void)
}

You can then pass this enum around and switch on it, with compiler-guaranteed completeness everywhere in your codebase. It seems so freaking simple, but it's incredibly useful.




As others have pointed out, that is actually a stunted and quite limited version of pattern matching that existed in other languages for years and even decades before Swift.

F#, OCaml, and Standard ML are the primary examples. It's surprising to me how ignored these languages are.


I don't really understand the point of pointing this out. The GP already says that it's a simple feature that they're also surprised isn't available in more languages - nowhere did they claim that Apple invented it or that it's a super innovative thing or anything.


The commenter mentioned:

> it has so many simple features I love that I am sometimes surprised to find are missing in other languages

and a few other things that led me to believe they were unaware of discriminated unions. There was an implication that this was a unique or novel feature in Swift. It's like someone being excited about Python's pattern matching, thinking Python came up with it and wishing other languages had it when it's a watered down, less useful version of that found in languages it ignored for 30 years.


I don't know, personally I think it is an immense leap of logic to look at someone's surprise that a simple feature is missing in [another] language and conclude that they're saying the feature is unique or novel.


I don't know how else you interpret what I quoted.

And it's not a judgement.


> are missing in other languages

GP is probably talking about other mainstream languages or language that they regularly use such as javascript/typescript. It makes no sense to interpret this as "are missing in every other language ever invented."

You're right that plenty of people do ascribe Apple credit for inventions that they didn't invent, but there isn't enough context/detail here to conclude that has happened in this case.


We're in the weeds here, but I actually think it was the other commenter responding to me that was under that impression. I didn't interpret the original comment as saying it wasn't in any other language ever. There are all sorts of niche languages that do things in very interesting and novel ways. I just consider F#, OCaml, and Standard ML as mainstream-ish languages. They're just poorly known, despite their influences on languages, although those influences are still not as strong as they should be.

I know this sounds very Emacs-fanatic of me, but it does get a bit tiring in the software world to have spent a lot of time in tool and language discovery only to be forced to work in a language that feels like a step backwards where people suddenly get excited or finally acknowledge the benefits of language designs that came before them. Really, this primarily reflects on Python, which I am currently having to use, and it feels like several step-functions down in capability, robustness, clean-ness, and several other qualitative and quantitative factors compared to something like F# and OCaml. There's a huge amount of "let's ignore other languages but then ad-hoc reinvent what they do in a mudball type of fashion". See Python's concurrency story for a particular egregious example. I am constantly bitten by things in Python that I'm honestly aware of no other language doing.

So it's probably fair to say I read in a bit to the above comment. I'm happy either way that people are appreciating ML-like features.


I could not agree more! Python in particular, is so painful to work in, and the addition of insult to injury of other Python people definitely gets under my skin. I think one of the most disappointing realizations I've had is the truth in "the best product doesn't win," which is especially apparent when it comes to programming languages.

I've been doing Elixir for several years and have tried to get others excited about it, and the frustrating chicken and egg problem of "there's not a good pool of programmers to hire or ecosystem of libraries" often seems to kill it in its tracks. Especially painful when it comes to machine learning where Python has de-facto won.


> You're right that plenty of people do ascribe Apple credit for inventions that they didn't invent

The number of people who do this is far, far outweighed by the people who claim they see it all the time.


Rust has a borrow checker. I think it is unique/novel, and I definitely don't think it is simple. In fact, it's precisely because I think it is unique/novel that I very much am not surprised that it doesn't exist in other languages that I use. Why on earth would I be surprised that a feature I think was just invented isn't yet present in other things? That's not how the passage of time works.

On the other hand, Rust also has first-class functions including closures. I do think that closures are a simple programming language feature, and I'm definitely surprised/put off when I write a language that doesn't have them (though these days I'm not sure there are any mainstream ones, now that Java and C++ both have them in the spec). Again it's precisely because I think of closures as the opposite of unique/novel that I am surprised at their absence. Why would I be surprised that a language does not have closures if I thought the Rust team just made them up?

So yes, I have no idea how one would conclude that someone's surprise at a simple feature being unavailable indicates that they think the feature is unique or novel, other than just being a knee-jerk reaction.


The idea was taken from Cyclone and linear/affine type systems.


Languages without this feature are basically refusing to wash their hands between the morgue and the maternity ward at this point.


[This is a reference to Ignaz Semmelweis, a Hungarian medic who wanted doctors to wash their hands properly based on his observations of the difference between the outcomes for women giving birth who were cared for by midwives (who don't work with corpses) and medical students (who do). This is very slightly before modern germ theory is established, so the doctors who reject Semmelweis' suggestions aren't strictly rejecting known facts about the world - Semmelweis can't explain why it's important to do this, only that it will work (which it does).]


Thanks for explaining the reference! I was wondering about that as I had never heard that turn of phrase before. I looked him up and apparently he was put into an asylum due in part to his recommendations and the rejection of them and subsequently beat to death while committed. An absolutely brutal example of being persecuted for being right.


I don't think Swift's enum with associated values are pattern matching. You can use pattern matching to unpack them though. (https://docs.swift.org/swift-book/documentation/the-swift-pr...)


Yea, I was sloppy with my language. But the point stands, and the unions are intrinsically tied to pattern matching.


I can't tell if your point stands because it was too sloppy.


F# is my “primary” programming language, I work with it daily and absolutely love it. That said, it doesn’t surprise me that people ignore it or don’t know about it given Microsoft’s own lack of attention and focus on it. Half the time it seems new dotnet features aren’t even compatible with F#.

The day C# gets discriminated union types and true exhaustive pattern matching is the day I’ll have to think long and hard about how much more time I want to put into working with my beloved F#.


I strongly believe (waves hands while making incoherent small brained primate noises) that discriminated union types would be fabo for C as well.

Example why it would be sweet. Compiler could spit out a warning if it's not sure the correct union type is being used. Like if the pet union has dog and cat. Cat has respawn set initially to 9. Dog doesn't have respawn. If you try to set respawn without checking if it's a cat compiler whines about it.


The C in CLR nowadays means C#, the Common has been lost long time ago, unfortunately.


How is it stunted or limited?


Dart has it, last 12 months or so.

Funnily enough, I sort of had the opposite journey, Swift seems horribly complicated and over-specified. The Dart compiler knows if I do a null-check, it can treat following references as safe, instead of if let x = x etc.

And it just hasn't lived up to any of the initial starry-eyed hype I had for it and expectations for a wider Swift ecosystem...

Like, here, it's great it runs on Raspberry Pi Pico but...it can't render UI unless someone writes a new UI library from scratch. And whoever does that inevitably will have to copy SwiftUI directly or alienate the people who actually know Swift.


> And whoever does that inevitably will have to copy SwiftUI directly or alienate the people who actually know Swift.

I may be wrong, but I don’t think this is actually true. SwiftUI isn’t universally loved in the community, largely thanks to its rough edges and how it struggles with that last 10% of polish in projects written with it. While copying SwiftUI wouldn’t necessarily be wrong per se, you’d actually probably get more rapid buy-in by copying “boring” old imperative UIKit, as that’s what most writers of Swift are using.


s/SwiftUI/UIKit, doesn't matter. (though I'm happy to have someone else point out SwiftUI has...some growing pains...it would have felt unfair for me to do so)

Point is its DOA for UI on non-Apple platforms.

Either you:

- attract current Swift programmers and keep up with $APPLE_UI_FRAMEWORK with no assistance.*

- attract no one and make a brand new framework in Swift.

- bind to some other UI framework, alienate current Swift programmers*, get all the people who have been dying to write Swift yet didn't build a UI framework for 7 years

There's just no universe where using Swift to write UI on non-Apple platforms makes sense to any constituency. You have to be simultaneously obsessed with Swift and don't care about platform-first UI, or want to maintain two separate UI frontends.

I hate being that absolutist, but that is what 16 years of experience on Apple platforms has taught me.

I'm surprised by the # of people arguing there's a possibility something else coudl work. I guess what I'd say is, if there is, where is it? Swift was on Linux eons ago, I believe before I started at Google so 7+ years.

* People used to try this to do iOS/OSX cross-platform, those projects are dead. One could argue this is just because Catalyst exists. Sure, but, let's just say you need at least N / 2 engineers, where N = the number of engineers Apple has on an arbitrary UI framework, in order to keep up with it. That's a lot.

** I've been in Apple dev since iPhone OS 2.0, and while in theory this could work, in practice it doesn't. Ex. Qt won't appeal to Apple programmers. It's antithetical to community values.


> Swift seems horribly complicated and over-specified. The Dart compiler knows if I do a null-check, it can treat following references as safe, instead of if let x = x etc.

`if let x = x` is an alternative to a null check, it's not as if now you need both a null check and an `if let`. What makes an `if let` more complicated?


Small change made in swift means an unwrap can just be

`if let x { }`

Which while small, is great if you do it all the time.

Swift though made me question how much I used optionals, now I get annoyed if they're over used, it usually means someone down the line has not made a decision, they propagate through the program and eventually someone wonders why they didn't get a value.

I like the Result type and the Flexibility of the Enums a lot for this


Tbh, i now look at optionals as another colour when i read this https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

You return an optional with no reason, it colours all subsequent calls.

When i was younger, i thought of them as an extension of bool, yes/no/maybe, but now, was it cancelled? Did an error occur? Did it just timed out? How many cpu cycles am i wasting throughout my code because every thing down the line checks it for null? It snowballs


There are now 2x as many syntaxes for the same operation, if let and if != null


It's not the same op though. One is a check, the other creates an in-context copy that's unwrapped from the optional. The compiler of course optimizes this out but it's not the same.


Right. In Dart they are the same. In Swift they are distinct.


They have different use cases. if x != nil is to do some work without needing x, because you'd still have to unwrap it if you used it.

That's what if let y = x is for, it creates a safe lexical context to use the unwrapped value.

And then you have guard let y = x which is a stronger guarantee that no code further down in the context is executed if x is nil. This helps to write happy-path code with early returns, avoiding pyramids of doom and encouraging writing code where all the assumptions are listed up front with logic handling exceptional cases before finally arriving at the logic that works under all the assumptions.

Dart feels like a step backwards after seeing the benefits these provide.


> They have different use cases.

But there’s also no harm in combining them. I agree with the OP, TypeScript does what they describe Dart doing and I find it much simpler than the way Swift does it. It’s never confusing.


> But there’s also no harm in combining them

Disagree. I’ve seen people write things like the following:

    if x != nil { x!.foo() }
and then later the x != nil condition is changed to no longer check for nil, or the x!.foo() is moved out of that lexical scope elsewhere, or copypastad, and then the force unwrap crashed. I’ve seen similar yet more subtle bugs with soft unwraps instead of forced: x?.foo()

Not possible if you use if let y = x . If you copypasta the inner scope with the different variable name then it won’t compile.

(ofc these are trivial representative examples, in reality these were lexical scopes with many lines and more complicated code)

It’s not just about how the code looks when you write it, it’s the paths that can be taken by future maintenance programmers.

I‘ve complained about the large surface area of swift syntax, stdlib etc in the past, but I don’t personally find this specific thing confusing.


For removing the null check, that, of course, would cause a compiler error.

For moving the force unwrapped out of the if, that's a problem with force unwrapping, not combining the syntaxes.


Changing the type of something based on a conditional check is not great, because if I wanted a non-optional I would request it. Sometimes a nil check is literally just a nil check. What if I want to set the value back to nil if it is not-nil? Why should I have to wrap things back in an optional if I want to pass it along as such?


The reason why languages promote variable types based on control flow is because developers en masse actually expect that to happen, e.g. facing the code like

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
    }
If compiler says "sorry, maybeDog might be null", developer (rightfully so) usually responds "but I have just checked and I know it is not, why do you bother me?". So languages chose to accommodate this.

> What if I want to set the value back to nil if it is not-nil?

You can. The type of the variable does not actually change. You could say that the information about more precise type is simply propagated to the uses which are guarded by the control flow. The following will compile just fine:

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
      maybeDog = null;
    }
> Why should I have to wrap things back in an optional if I want to pass it along as such?

You don't, with a few exceptions. There is a subtype relationship between T and T?: T <: T?, so the following is just fine:

    void foo(Dog? maybeDog);

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
      foo(maybeDog);  // Dog can be used where Dog? is expected
    }
 
You might need to account for it in places where type inference infers type which is too precise, e.g.

    Dog? maybeDog;
    if (maybeDog != null) {
      // This will be List<Dog> rather than List<Dog?>. 
      final listOfDogs = [maybeDog];
    }
Though I don't think it is that bad of a problem in practice.


See, I don't think languages should accommodate this, because I see it as an ugly solution. It's nice that it works in a few cases but then it very quickly breaks down: a developer finds that their null check is enough to strip an optional, but a test against zero doesn't convert their signed integer to unsigned. Checking that a collection has elements in it doesn't magically turn it into a "NonEmptyCollection" with guaranteed first and last elements. I'm all for compilers getting smarter to help do what programmers expect, but when they can't do a very good job I don't find the tradeoff to be very appealing. Basically, I think pattern matching is a much better solution this problem, rather than repurposing syntax that technically means something else (even though 90% of the time people who reach for it mean to do the other thing).

Also, fwiw, I was mostly talking about things like the last example you gave. I guess it would be possible that in circumstances where T is invalid but T? would be valid, the language actually silently undos the refinement to make that code work. However, I am not sure this is actually a positive, and it doesn't help with the ambiguous cases anyways.


This is the elegant solution.

Where does it break down?

This isn't the same thing as "magically" changing the type.

What does the syntax technically mean?

What refinement is undone?

I think you're a bit too wedded to the idea that there's a type conversion going on or some dark magic or something behind the scenes that's "done" then "undone" like a mechanism. There isn't. It's just static analysis. Same as:

    let x: Animal;
    x = Dog()
    if (x is Dog) { 
      x.bark()
    }

The Zen koan you want to ponder on your end is, why do you want to A) eliminate polymorphism from OOP B) remove the safety of a compiler error if the code is changed to x = Cat()?


I don’t like that either. x is Dog is a boolean expression. Presumably I can write

  let isDog = x is Dog;
And the value whether it is a dog or not goes into that new variable. The fact that I can’t then immediately go

  if (isDog) {
     x.bark()
  }
shows the deficiencies of this static analysis (even if you could do some simple value tracking to make this work, it doesn’t really address the real issue I have with it, which I described above: why can’t I do this refinement for other things?) The conceptual model is one of “we special cased 2-3 cases that we can detect”, which I don’t find very elegant, especially considering that other languages seem to have better solutions that express the operation in what I feel is a better way.

(The equivalent Swift code would be something like this:

  let x = Animal()
  if let dog = x as? Dog {
      dog.bark()
  }
I see this as much superior. One, because x is still available in the scope of I want an Animal and not a Dog for whatever reason. I understand that most of the time you don’t do this but to have it available and not have to do a special case for it is nice. The second thing is that I just like the syntax, since it gives you an opportunity to give a more specific name in that branch as part of the assignment. Third, it composes, because none of the operations are special save for the “if let” binding. This code is also valid:

  let x = Animal()
  let dog /* : Dog? */ = x as? Dog
  if let dog /* = dog */ {
  }
The static analysis for undoing polymorphism is the exact same as the one for binding to optionals, because the idiomatic way to cast results in an optional.)


> The fact that I can’t then immediately go

Who said you can't? :) This actually works in Dart:

    Dog? dog;
    bool isDog = dog is Dog;
    
    if (isDog) {
      dog.bark();
    }
i.e. boolean variables which serve as witnesses for type judgements are integrated into promotion machinery.

I do agree with you that

1. In general there is no limit to developers' expectations with respect to promotion, but there is a limit to what compiler can reasonably achieve. At some point rules become too complex for developers to understand and rely upon. 2. Creating local variables when you need to refine the type is conceptually simpler and more explicit, both for language designers, language developers and language users.

I don't hate excessive typing, especially where it helps to keep things simple and readable - so I would not be against a language that does not do any control flow based variable type promotion. Alas many developers don't share this view of the world and are vehemently opposed to typing anything they believe compiler can already infer.

It's all a matter of personal taste in the end.


Kotlin can't do this, which the language I have more experience with. It's good that Dart does a little better in that regard. And, I think we do agree, I just don't really like the viewpoint of doing this, because I feel like it's not really general enough.


I don't think any of that has to do anything with static analysis deficiencies. The analysis is the same. What Swift requires is for the user to manually help the compiler with a rather verbose and otherwise unnecessary construct.

Type narrowing is not a new idea


It’s not a new idea, but I don’t think it’s a good idea. That’s just it. Swift has a construct to do the equivalent and it’s fewer characters to boot when compared to Dart when you’re going from an optional to non-optional type.


But that's exactly what Swift does: it narrows the types. To do that, it uses a separate construct that only exists to make the parser ever so slightly simpler.


Yes and I think having a separate construct rather than using another one and imbibing it with new meaning is more elegant


The ugly solution is Swift's if let x = x which is literally the same check, but in a more verbose manner.

Yes, compilers should be able to help in this and many other cases, and not just give up and force the programmer to do all the unnecessary manual work


It’s not a check, it’s binding a new variable with a different type (which may or may not have the same name; if it does you can use if let x these days). And solving the other examples I mentioned is generally out of the realm of most programming languages in use today (except maybe TypeScript?) It comes with significant challenges that I’m not sure we are ready to address properly yet.


In Dart if you want to narrow down a nullable final class variable, it is not enough to do

if (value == null) { // value is still nullable here }

instead you need to first capture the value

final value = this.value if (value == null) { // value is non-null }

Swift's guard and if let syntax seems way nicer here. In swift, it's also nice to destructure nested nullable values using if let.

if let value = object.object?.value { // value is non null }

In dart you'd need to first assign this into a local variable and then do the null check which is unnecessarily verbose.


In Dart 3 instead of declaring the variable and then comparing you can simply write an if-case pattern:

    if (value case final value?) {
      // value is a fresh final variable 
    }


So Dart is moving in the Swift direction, which has

    if let value = value {
      // a new binding named ‘value’ refers to value, unwrapped 
    }
or (newer syntax that I initially found even weirder than the older one, but both grew on me)

    if let value {
      // a new binding named ‘value’ refers to value, unwrapped 
    }
(The old version will remain, as it is more flexible, allowing such things as

    if let baz = foo.bar(42) {
      // a new binding named ‘baz’ refers to foo.bar(42), unwrapped 
    }
)


The more you know, I had totally missed this.


> it can't render UI unless someone writes a new UI library from scratch

This is true for literally every programming language. You can’t render a UI, in general, without a target compatible UI library.

SwiftUI and UIKit are specific Apple things built on top of CoreAnimation and completely separate from the Swift language. And swift will happily link to C and somewhat C++, so no it’s not all evident why one would copy Apple’s SDK to render UI on a completely separate platform and environment.


> The Dart compiler knows if I do a null-check, it can treat following references as safe, instead of if let x = x etc.

Does Dart bind a new variable 'x' for the if-statement scope or does the if-statement scope refer to the same 'x' from the outer scope?

The reason Swift and other languages bind a new 'x' variable (if let x = x) is because the original 'x' can be assigned a different value elsewhere, like in another thread or a closure. If a closure captures and assigns null to 'x' and there is only one binding of 'x', then that means the 'x' within the if-statement will become null _post null-check_ which is type unsound. I believe TypeScript has this "problem" as well.


I assume Dart refers to the same variable since:

1) Dart doesn't have threads, it has isolates (where memory is not shared). So no variable modifications here.

2) Dart will promote a variable as not null after a non-null check only if it cannot be modified elsewhere, ie, the variable is final (can't be modified once assigned), or there's no other reference to it.


gentle reminder that `if let x = x` can now be spelled simply `if let x` in current versions of Swift


Have been loving this feature. It’s a small but impactful QoL improvement.


>Dart has it, last 12 months or so.

To clarify, in the above scenario in dart I can have an input with type MyEnum and do this?

switch myEnum {

case let one:

   // react to this here

 case let two(twosChild: twosChild):

   // call an API passing in a child element of twosChild here

 case let three(threesGenericChild: threesGenericChild):

   // T: Protocol, and call an extension function threesGenericChild.extension() here?


 case let four(foursCallbackChild: foursCallbackChild):

   // Pass foursCallbackChild callback into another context, and execute an escaping closure if needed?
}

Last I checked Dart required a nested if/else tree and didn't have compiler-guarantees of switch completeness like the above.


Correct, Dart has enum exhaustiveness checks, associated types, and expressions as of 11-13 months ago: https://cfdevelop.medium.com/dart-switch-expressions-33145c3... (keeping it short and simple because I'm at -2 on the original post, don't want to look like I'm argumentative)


In Dart 3 you have patterns and sealed class families to achieve that. See my comment above[1].

The syntax is unfortunately more verbose, but if all goes well we will address that issue this year.

[1]: https://news.ycombinator.com/item?id=39613415


I believe Dart has pattern matching, but not associated values of different types.


In Dart you use class hierarchies instead, rather than enums (which in Dart are way to define a compile time constant set of values). So the original example becomes:

    sealed class MySomething<T> {
    }

    final class One extends MySomething<Never> {
    }

    final class Two extends MySomething<Never> {
      final Child child; 
      
      Two(this.child);
    }

    final class Three<T> extends MySomething<T> {
      final T value;

      Three(this.value);
    }

    final class Four extends MySomething<Never> {
      final int Function() callback;

      Four(this.callback);
    }
And then you can exhaustively switch over values of MySomething:

    int foo(MySomething<String> foo) {
      return switch (foo) {
        One() => 1,
        Two(child: Child(:final value)) => value,
        Three(:final value) => int.parse(value),
        Four(:final callback) => callback(),
      };
    }
The declaration of a sealed family is considerably more verbose than Swift - and I really would like[1] us to optimized things for the case when people often declare such families. Macros and a primary constructors will likely provide reprieve from this verbosity.

But importantly it composes well with Dart's OOPy nature to achieve things which are not possible in Swift: you can mix and match pattern matching and classical virtual dispatch as you see fit. This, for example, means you can attach different virtual behavior to every member of the sealed class family.

[1]: https://github.com/dart-lang/language/issues/3021


I miss Swift enums when writing Kotlin. Kotlin enums are more restricted in that associated values can’t differ between cases, which limits their usefulness and means that much of the time what you want are instead sealed classes.

There’s actually several bits of Kotlin like this which feel somewhat pedantic/idiosyncratic and fussy if you’re used to writing Swift. I’m sure there’s valid design reasons behind these decisions, but sometimes it feels like unnecessary friction.


Since sealed class definitions can live in the same file and the syntax is quite compact, in Kotlin they don't annoy me as much as in Java.


Yup it's called a sum type, or a tagged union https://en.wikipedia.org/wiki/Tagged_union


The enum example is not really a union though or is a constrained version. For example, the cases are non-recursive.


Swifts enums can be recursive though as can rusts


Thanks for the clarification. That does indeed make them closer to unions.


Um, what? You believe unions are recursive data structures?


Yes. For example, from Standard ML:

    datatype 'a list = nil | :: of 'a * 'a list


I have never seen this called a "union", and that's certainly not what people mean when they speak of a "tagged union".


What do you think tagged unions are? What languages are you referring to?

F# calls these discriminated unions. OCaml calls them variants or tagged unions.

https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...

https://ocaml.org/docs/basic-data-types#variants


Huh. I was thinking about this in much lower level terms, it seems weird to me to define these structures recursively when how they're actually stored is thus omitted yet that's actually crucial, but I guess if you work in a sufficiently high level system you don't care about the implementation details.


It is exactly what they are. ‘Tagged’ or ‘discriminated’ unions are a set of named product types - the name being the tag/discriminator.


https://wikipedia.org/wiki/Pattern_matching since 1957

I feel ML is the most famous for it.




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

Search: