Hacker News new | ask | show | jobs
by wyager 3440 days ago
> the more I've grown to appreciate the concept of Optionals

You may also enjoy a language that supports the generalization of Optionals, which are Algebraic Data Types (ADTs). Optionals are a single, limited application of ADTs.

Optionals allow you to shift null pointers (and null pointer exceptions) to the type level. So instead of having to worry about a function returning null, you just deal with Optionals whenever a function returns one.

There's another ADT, usually called "Either" or "Result", that allows you to shift exceptions to the type level. You can see from the type of the function what kind of exception it might return, and you deal with exceptions like any other value instead of via a special exception handling mechanism.

3 comments

One of the things that makes Optional so pleasant in Swift is the syntax support. This includes optional-chaining, if-let and guard-let unwrapping, and some convenient operators.

For example, in Haskell, by default you can't compare an Optional with the value it wraps: `5 == Just 5` fails. But in Swift this works like you would want.

All that is to say that Options in Swift are a bit nicer than what you could get with pure ADTs. It's a similar story for Swift's syntax-level error handling vs type-level monadic error handling. (The downside of course is that the compiler has to be specially taught about these - but maybe you want that anyways, e.g. for Rust's null-pointer optimization.)

> This includes optional-chaining,

Haskell has bind (>>=) and do-syntax, rust has `and_then`. This is pretty standard with any ADT-supporting language.

> if-let

Most languages with ADT support have good pattern matching that subsumes if-let syntax and allows you to do other things as well. Swift's pattern matching, such as it is, is a bit of a mess.

> guard-let unwrapping

Haskell has `guard` and `MonadFail`, which address the use cases for this in a more principled way. `guard` for boolean matching (like equality) and `MonadFail` for structural matching (like pattern matching on a constructor).

Rust has (?) and `try`, which are syntactic sugar around a simple match/return statement that addresses most use cases of guard-let.

Obviously there are going to be small differences between this implementations, but Swift doesn't really excel here.

> `5 == Just 5` fails.

As it probably should. Supporting such implicit casting could lead to some obvious confusion and buggy behavior. Ideally, the fact that "a == b" typechecks should indicate that "a" and "b" are of the same type.

In Haskell, you would just do `Just 5 == x` if you want to check that something is, in fact, `Just 5`. If that really wasted a lot of space somehow, you can define something like `x ==: y = Just x == y`.

Rust copied 'if let' from Swift (~2 years ago) despite having decent pattern matching; community consensus today is that it's highly worthwhile as a feature. There have also been proposals to add 'guard let'. So, while I don't have enough Swift experience to judge its ADT support overall, I wouldn't cite those features as evidence that it's wanting. They might not be as natural in traditional FP languages like Haskell, but they seem to fit pretty well in the stylistic niche Rust and Swift (among other languages) have carved out.

edit: FWIW, there's some overlap between '?' and 'guard let', but not that much. Using hypothetical Rust syntax, they'd overlap in this case:

    guard let Some(x) = foo else {
        return Err(GettingFoo);
    }
better expressed as

    let x = foo.ok_or(GettingFoo)?;
…but if you want to return something other than a Result, or the ADT being matched on is something custom rather than Option/Result, there's no good way to make '?' do the job.
> Rust copied 'if let' from Swift (~2 years ago) despite having decent pattern matching; community consensus today is that it's highly worthwhile as a feature.

Rust copied 'if let' and made it far nicer and more capable by allowing arbitrary fallible pattern matches to be the condition. Then Swift 2 copied that improvement back with 'if case'.

? is bind for Either monad if you squint.

`context(e?)` becomes `e >>= \x -> context(x)`

You mean `e >>= context` ? ;)
Hah!

I should have been clear about CPSing things first so I didn't need to eta-expand :)

Optional chaining is syntactically much more lightweight than bind and do-syntax. >>= and `and_then` have the annoying property of reversing the order of function calls. `if let` is also more pleasant to read - note you can destructure multiple values naturally (no tuple required) and incorporate boolean tests.

It's totally fair to point out that Haskell is more principled - users can build Haskell's Maybe, but not Swift's Optional.

> Rust has (?) and `try`, which are syntactic sugar around a simple match/return statement that addresses most use cases of guard-let

They aren't really comparable. Rust's `try!` addresses one case: you have a Result and want to propagate any error to the caller. This is closest to Swift's `try`. But Swift's `guard` is much more flexible: it allows destructuring, boolean tests, multiple assignment, etc., and it also allows arbitrary actions on failure: return, exit(), throw, etc., with the compiler checking that all paths result in some sort of exit.

In practice this is used for all sorts of early-outs, not just error handling. It neatly avoids `if` towers-of-indenting-doom. I think the best analog is Haskell's do-notation.

There's the same tradeoff here. Rust's try! is a macro that anyone can build, while Swift's `guard` is necessarily built into the language. But any Swift programmer will tell you how much they appreciate this feature, unprincipled though it may be. Use it for a while and you become a believer!

> As it probably should. Supporting such implicit casting could lead to some obvious confusion and buggy behavior. Ideally, the fact that "a == b" typechecks should indicate that "a" and "b" are of the same type.

The principled stand! Both languages make the right decision for their use case. Swift's Optionals are much more commonly encountered than Haskell's Maybe (because of Cocoa), and so the language's syntax design optimizes for dealing with Optional. They're more central to Swift than its ADTs.

>in Haskell, by default you can't compare an Optional with the value it wraps: `5 == Just 5` fails.

    -- Probably nothing like in Swift
    instance (Num a , Integral a) => Num (Maybe a) where
        fromInteger x = Just (fromIntegral x)
        
    main = print $ 5 == (Just 5)
This only works because you're working with number literals here, which have the very liberal type "Num a => a". If I replace your main function by

  check :: Int -> Bool
  check x = x == (Just x)

  main = print (check 5)
I get a compile error:

  Main.hs:7:17: error:
      • Couldn't match expected type ‘Int’ with actual type ‘Maybe Int’
      • In the second argument of ‘(==)’, namely ‘(Just x)’
        In the expression: x == (Just x)
        In an equation for ‘check’: check x = x == (Just x)
Right, it was just a fun little "Well Actually" moment.

http://tirania.org/blog/archive/2011/Feb-17.html

That is possibly the most condescending thing I've ever read. Whatever the author tried to make themselves less abrasive, it didn't work very well.
> `5 == Just 5` fails. But in Swift this works like you would want

Why in the world would you want those two to be equal when they obviously don't represent the same thing?

That doesn't make sense, not even if they have the exact same memory representation, in which case I'm pretty sure it has been a compromise, which would mean you're still dealing with `null` with some lipstick on it, making that type behave inconsistently with other types in the language.

This kind of misguided convenience is exactly why equality tests in most languages are in general a clusterfuck.

> Why in the world would you want those two to be equal when they obviously don't represent the same thing?

This is the difference between normal people and theoretical computer scientists, summarized in one sentence.

As suggested by the existence of this monstrosity, "theoretical computer scientists" have it right on this one.

https://dorey.github.io/JavaScript-Equality-Table/

>Why in the world would you want those two to be equal when they obviously don't represent the same thing?

Because I care for intended use, not ontology.

That is not the intended use of Option/Maybe, the whole point of an `Option[A]` is to be different from `A`.
Not the intended use of Option[A] -- the intended use of A. Option is just a safeguard, and for the check with A that capability is not needed at all (if it's Just A it can ...just equal to A).
Option isn't a safeguard, it expresses missing values in a way that doesn't violate the Liskov Substitution Principle, otherwise you might as well work with `null` along with some syntactic sugar.

And they are different because the types say so. By allowing them to be equal, you're implicitly converting one into the other. That's weak typing by definition, a hole in the type system that can be abused.

So why have types at all? Dynamic typing is much more convenient and Clojure deals with nulls by conventions just fine.

Swift supports ADTs and implements its Optional type as an ADT.
> Swift supports ADTs

True, but perhaps not practically relevant. The pattern matching in Swift isn't much better than what you would get with a tagged C union, which is why no one really uses it very heavily. The Optional type in Swift gets a lot of special compiler support, which is indicative of the fact that the broader language isn't very friendly towards using ADTs to structure data.

But you're right, I should have clarified in my comment that Swift does have a basic degree of support for ADTs.

> The pattern matching in Swift isn't much better than what you would get with a tagged C union

In what way? Pattern matching in Swift is quite powerful; definitely better than C's switch statement. This blog post is a good overview of its capabilities. https://appventure.me/2015/08/20/swift-pattern-matching-in-d...

I don't know what kind of Swift code you're writing, but you're very, very wrong when you say "no one really uses it heavily". Pattern matching is in fact quite powerful and is used heavily in any codebase that isn't simply a straight porting of Obj-C idioms.

> The Optional type in Swift gets a lot of special compiler support

Only the ?. optional chaining operator (and technically the ! postfix operator too, though that could have been implemented in the standard library instead of being special syntax and in fact I'm not really sure why it isn't just a stdlib operator). And probably some logic in the switch exhaustiveness checker so it knows nil is the same as Optional.none. Everything else (including the actual pattern matching against the "nil" keyword) is done in a way that can be used by any type, not just Optionals (nil for pattern matching specifically evaluates to a stdlib type called _OptionalNilComparisonType which implements the ~= operator so it can be compared against any Optional in a switch, and you could do the same for other ADTs too, assuming they have a reasonable definition for nil).

In what way is Swift pattern matching not "much better than what you would get with a tagged C union"?

enum in Swift has pattern matching support, can have properties, adopt protocols, etc.

I would disagree with your assessment that "nobody uses them".

Swift does fully support ADTs, and can easily model result types, they just chose to have catchable exceptions as well.