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

If x represented a set with some values

  x = {RED, GREEN, BLUE}
It would be really swell if the optional type `Maybe x` were a strict superset:

  Maybe x = {RED, GREEN, BLUE, None}
This means functions that map `Maybe x -> y` can accept values of type `x` without issue.

But in Haskell (and OCaml) it's not. What we actually have is something more like

  Maybe x = {Some(RED), Some(GREEN), Some(BLUE), None}
which is a totally different set from `x`, which is why functions which shouldn't break, ultimately do.



The downside of that (which can be provided by union types) is that `Maybe (Maybe X) == Maybe X` which may or may not be what you're after. The particular weakness is the interplay with generics where `forall t. Maybe t` depends upon the `Maybe` and the `t` not interacting. If you've got `forall t. t | null` then this type may secretly be the same as `t`---and if you handle nulls then it might conflate meanings unintentionally.


So you're saying that the one Nothing has a different meaning than the other Nothing, and that this is something that anybody would want? Or that we have a Nothing and a Just Nothing? I'm sorry, but I don't get how that would be considered good design in any context. Do you have any example where this would be considered sensible?


Having a situation where one handles both `Nothing` and `Just Nothing` in the same context should be rare.

But you might be writing some functions on some abstract data structure with a type parameter `a` (say it’s a graph and users can tag the vertices with values of type `a`). And (maybe just internally) there are situations in your algorithms where a value might be absent, so you use `Maybe a`. If `Nothing == Just Nothing`, your Users can’t use a maybe type for `a` anymore because your algorithm wouldn’t be able to distinguish between its `Nothing`s and the user’s.


I am.

In any concrete context, Maybe (Maybe A) could _probably_ be simplified to just Maybe A as we expect. Alternatively, we could be in a situation where there are two notions of failure (represented here as Nothing and Just Nothing) in which case we'd be better off simplifying to Either Error A where Error covers you multiplicity of error cases.

But while these are all obvious in a concrete context, what we often are doing is instead compositional. If I want to write code which works over (Maybe a) for some unknown, library-user-declared type `a` then I may very well end up with a Maybe (Maybe Int) that I can't (and mustn't) collapse.

As a concrete example, consider the type of, say, a JSON parser

    ParserOf a = Json -> Maybe a
We hold the notion of failure internal to this type so that we can write

    fallback : ParserOf a -> ParserOf a -> ParserOf a
which tries the first parser and falls back to the second if the first results in error. We might also want to capture these errors "in user land" with a combinator like

    catch : Parser a -> Parser (Maybe a)
If we unwrap the return type of `catch` we get a Maybe (Maybe a) out of a compositional context (we can't collapse it). Additionally, the two forms of failure are distinct: one is a "handled" failure and the other is an unhandled failure.


Similarly, a polymorphic function may want to put values of an unknown type in a Map or MVar. From the outside it may be completely reasonable to call that function on Maybe Foo, which would mean a Maybe (Maybe Foo) somewhere internally and I would struggle to call that bad design.


> In any concrete context, Maybe (Maybe A) could _probably_ be simplified to just Maybe A as we expect.

And doing so is just what `join :: m ( m a ) -> m a` specialises to for `m = Maybe`!


That's just one way to do it. Another might be

    collect :: Maybe (Maybe a) -> Either[Bool, a]


But you mentioned simplifying `Maybe (Maybe a)` to `Maybe a`, which your `collect` doesn't do (at least not directly).

(Also, shouldn't `Either[Bool, a]` (which I don't know how to make sense of) be `Either Bool a`? Even with this signature, I'm not sure what the implementation would be, and Hoogle doesn't turn up any obviously correct results ( https://www.haskell.org/hoogle/?hoogle=collect https://www.haskell.org/hoogle/?hoogle=Maybe+%28Maybe+a%29+-... ), but that's probably my fault.)


I think the obvious implementation is `maybe (Left False) (maybe (Left True) Right)`

If we have a value then we clearly have both layers. If we don't have a value then we need to distinguish Nothing from Just Nothing by way of the book.


Oh, I see; we're thinking of `Maybe a` as `a + ()`, and then identifying `(a + ()) + ()` with `a + (() + ())` and then `() + ()` with `Bool`. Thanks!


Right, exactly!


Imagine you're working on a compiler. You need to represent compile-time computed value of type Maybe Int (e.g. you are precomputing nullable integers).

You see 1 + null. So you have add: Maybe Int -> Maybe Int -> Maybe Int, that takes two precomputed values, and returns new precomputed value for the operation result.

However, you can't precompute Console.readInt().

For some expression, you can either be able to compute value at compile time, or not.

What is the output type of compileTimeCompute: Expr -> ???


I don't understand your example. What does compile-time computed stuff have to do with readInt()?

I get that it might be possible to do that, use a Maybe Maybe T. But it's like an optional<bool> in C++. It can be done, it's just not a good idea. So if you design your system not to allow that in the first place, nothing of value was lost.

If you have specific error cases that you want to communicate, like "what was read from the console didn't parse as an int" as opposed to "the computation didn't find a result", then using the two values "Nothing" and "Just Nothing" as the two distinct values to encode that is not a sound design. Either you have meaning, or you have Nothing. Nothing shouldn't have any meaning attached to it.


> "what was read from the console didn't parse as an int"

I meant what you read from the console can not be computed at compile time.


Yep that's why as Hickey says people are excited about union types. But in the context of a refactor or a change to an API where you care about breaking existing users, there's a perfectly valid way forward.

Basically I don't think Option types are as unwieldy as Hickey is making them about to be. Indeed Dotty (one of the examples in the talk) will be keeping Option even in the presence of union types (mainly because union types fall down with nested Options and with polymorphism).

That being said Haskell (and Scala) is annoying when it comes to trying to combine different kinds of errors together, if you want an error type more expressive than Option.

OCaml here (and Purescript) have a much better story with polymorphic variants.


In OCaml you have polymorphic sum and product types, so the function

    val f : [`Red | `Green | `Blue | `None ] -> y
Would accept a value of type [`Red | `Green | `Blue ] quite fine.

Though I'd still prefer a haskell false positive to a clojure's false negative. Also, if something is considered a string, I expect it to be any string but NULL.




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

Search: