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

The sad thing is that Rich Hickey had some very good videos when Clojure was a new thing back in 2008–2009. Unfortunately, I've disagreed vehemently with most of his talks since then. In this case, it's completely illogical that a function `Maybe a -> b` should be callable as if it were a function `a -> b`. Do you want to know how I know? Because it would be just as illogical to allow a function `Vec a -> b` to be called as `a -> b`. And Rich must agree because Clojure itself does not support that!

I've learned that videos of his talks are just not worth my time.




Why is it illogical to say that a Maybe a -> b should be callable as if it were a -> b?

His point is that Maybe a should be composed of all the values of a, plus one more value, nil. A value of type a is a member of the set (nil + a). Why should having a more specific value reduce the things you can do with it? It breaks composition, fundamentally. It's like saying (+) works on integers, but not on 3. I'm saying this someone who really enjoys type systems, including haskell.


> His point is that Maybe a should be composed of all the values of a, plus one more value, nil

No, that's a simple union type. There are very good reasons for Maybe to be different than unions (Maybe can nest meaningfully, simple unions can't.)

Maybe Maybe a is valid, and often useful, type.

Of course, if you have a function of type a -> b and find out you need a more general Maybe a -> b, instead of a breaking change, you just write a wrapper function that produces the correct result for Nothing and delegates to the existing function for Some(a) and you're done without breaking existing clients.

(Now, I suppose, if you're u had something like Scala implicits available, having an implicit a -> Maybe a conversion might sometimes be useful, though it does make code less clear.)


I agree that there are reasons for Maybe a to be a different type from (a | nil) but there are also good reasons to prefer (a | nil). Like most things, it's a set of tradeoffs. What I appreciated about this talk was that he went into the benefits of thinking about types in this way. It's (relatively) common to see the benefits of Maybe a explained, but more rare to see the benefits of full union types explained.


My problem is that I don't know when to expect nil from a call because in Java null is part of every type and you can happily receive a null from anything, the compiler won't give you a warning. In OCaml I know what to expect because Some x | None is super simple to reason about. I can never receive a null a nil or other things that somehow satisfy the type requirements. Clojure is great with untyped programming everything is an Object after all but I still would like to see a reasonable thing like Erlang's {:ok,x} | {:error, error} or OCaml's Some x | None. It is not an accident that many languages that like reliability implemented it like that.


Yes, the default of a lot of languages, (Java, C, etc) where nil is implicitly a member of every other type is a bad default. But that's a separate question.


> Why is it illogical to say that a Maybe a -> b should be callable as if it were a -> b?

Fundamentally because it would require you to conjure up a value of type b from nowhere when the Maybe a is Nothing. If we view the function type as implication this would not be valid logically without some way of introducing that value of type b.

You could imagine some function from Nothing -> b that could rescue us. But since it only handles one case of the Maybe type, it is partial (meaning it could give undefined as an answer). There is basically two total functions that we could change it to:

   * Maybe a -> b in which case we are back where we started.
   *  Unit -> b which essentially is just b, which can be summed up as meaning we need some kind of default value to be available at all times.
So to be able to call Maybe a -> b as a -> b you would need some default value available at all the call sites for a -> b

Now this is only "illogical" because we don't postulate a value of type b to be used as this default.

> It's like saying (+) works on integers, but not on 3

No, it's like saying (+) must work on all integers AND a special value nil that is not like any other integers, but somehow included in them and all other data types. We can't do anything with this nil value since it doesn't carry any data, so in the case of (+) we would essentially have to treat it as an identity element.

This is good though, since (+) has 0 as an identity element, so we can just treat nil as a 0 when we encounter (+). However, when we want to define multiplication we still need to treat nil as an identity element (since it still doesnt carry any data), except the identity element for multiplication is 1. This would be repeated for every new function that deals with integers.

So by mashing together Maybe and Integer we have managed to get a frankenstein data type with an extra element nil which sometimes means 0 and sometimes means 1.

Why not just decompose them into Maybe and Integer and supply the default argument with a simple convertion function like fromMaybe?

(FWIW, I actually agree with Hickey that using Maybe in api design is problematic and I've encountered what he's talking about. But while that might be an argument for where he wants to take Clojure, it's not an argument for dismissing type theory the way he does.)


You got it backwards. These problems arise when you want to use an (a -> b) function as (Maybe a -> b), not vice versa.


Yeah, you're right, I got confused when interpreting the parent comment. Thanks for pointing it out!

I guess I overlooked it because the other way is so logically trivial, since it basically boils down to A -> B => (A || Nothing) -> B, which is just an or-introduction. So if you wanna implement Maybe generically the "work" lies on the other side.

But since Hickey's argument sort of is that we shouldn't implement Maybe generically, I guess my argument here becomes circular. (Begging the question maybe?)


> I guess I overlooked it because the other way is so logically trivial, since it basically boils down

Yeah, that's (part of) Hickey's point. That the "best" type systems fail this test, and require manual programmer work to solve this problem. Again, I'm saying this as someone who really appreciates Haskell.


I think Rich does not like Some x | None because he does not like simple pattern matching too much. This is why Clojure does not have a first class pattern matching syntax (you can emulate, and there is a library and it is just X amount of lines, etc. but still).

In this regard I really like OCaml:

    let get_something = function
      | Some x -> x
      | None   -> raise (Invalid_argument "Option.get")
This is very simple to understand and reason about and very readable. The examples Rich was trying to make in the video I could not tell the same about. He kind of lost me with transducers and the fact that Java interop with Java 8 features is rather poor.


I was surprised it was a seperate library in Clojure and doesn’t seem to be something that gets used much. Puts me off that it’s missing one of the most attaractive features of functional languages.


Because it would be just as illogical to allow a function `Vec a -> b` to be called as `a -> b`. And Rich must agree because Clojure itself does not support that!

Maybe Clojure's standard library just isn't that focused on vectors? Python stdlib doesn't support this as well, but NumPy does.




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

Search: