Maybe Not [video]
youtube.com
youtube.com
Somewhat tangentially, it also keeps reminding me somehow of Alfred North Whitehead's concepts of reality as process flows https://en.wikipedia.org/wiki/Alfred_North_Whitehead#Whitehe...
"In summary, Whitehead rejects the idea of separate and unchanging bits of matter as the most basic building blocks of reality, in favor of the idea of reality as interrelated events in process. He conceives of reality as composed of processes of dynamic "becoming" rather than static "being", emphasizing that all physical things change and evolve, and that changeless "essences" such as matter are mere abstractions from the interrelated events that are the final real things that make up the world."
[1] https://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hi...
originalF :: x -> y
f :: Maybe x -> y
f None = newDefaultBehavior
f (Some x) = originalF x
If `Maybe` is an output then existing callers SHOULD be broken, because they must now handle a new possibility of failure they didn't before. The fact that this doesn't happen in Clojure is actually a source of pain for me.It's also rather interesting that Hickey also comes to the same conclusion that optionality doesn't belong in a data structure, but for slightly different reasons: https://news.ycombinator.com/item?id=17906171
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.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 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.
I meant what you read from the console can not be computed at compile time.
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.
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.And doing so is just what `join :: m ( m a ) -> m a` specialises to for `m = Maybe`!
collect :: Maybe (Maybe a) -> Either[Bool, a](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.)
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.
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.
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.
The problem with that is now you have to maintain two functions that do the same thing just to satisfy the type system.
> If `Maybe` is an output then existing callers SHOULD be broken,
He agrees with you on this, according to the talk
2. Ugh this is what I get for watching a video while coding on the side. You're right.
All that aside, in my experience the case of parameters becoming optional or return values losing nullability actually does not happen that much in practice. It's a bit of a strawman.
The problem with breaking changes is that the person who breaks things doesnt feel the pain.
In other words, I'm arguing that this kind of breaking change is (1) rare, and (2) ok anyway if you have a strong enough type system that the compiler will just tell you what callsites need modification, and the modification is simple (as in this case).
Rich Hickey has very strong feelings about breaking other people's code, and I tend to agree with him about: don't do it. Breaking other people's code is mean and unnecessary.
Paranoia about breaking code makes sense in languages that have few mechanisms for aiding in refactoring. It makes less sense when the language is acting as your teammate.
Talking about refactoring, I think it's much easier when you are working with a codebase that built by composing lots of small pure functions together. The place where it gets tricky is the part with side-effects where you have to carefully test it either manual or automate anyway. Type error will likely to be caught during that process. With Clojure Repl, we can test that out pretty quickly.
Look at the example again. The output was Maybe y "yesterday", "today" the output is y. Existing callers shouldn't have to change - there is no new possibility they need to handle - they no longer need to deal with the possibility of Nothing.
I'm just so glad that Clojure exists, it is doing all the good design things to make programming pleasant but it has a different approach from the usual suspects which is incredibly valuable!
Being dynamic and interactive is great for UI/UX design since that, especially, benefits from the tight feedback loop and I'm becoming more and more bullish on making the type system separate like this and thinking of it as synonymous with tests, it makes a lot of sense (separation of concerns...)
Also coming from mathematics I can see a analogy to the preference of working in open sets (so that you can always take a point arbitrarily close to the edge and still make an open neighborhood around it) and what he is calling an open system. This preference for open sets over dense ones seems counterintuitive at first sight but is the basis on which analysis is built. Type systems feel a bit 'dense' in the sense that they force you to write programs that are overspecified?
Anyway I just look forward to seeing experience accumulate with these different technologies so that we may learn more about how we ought to design systems !
One of his main points is that Maybe is context-dependent, and other aspects of schemas are as well. This is a great and underappreciated point.
It’s related to a subtle but important point that Google learned the hard way over many years with respect to protocol buffers. There’s an internal wiki page that says it better, but this argument has spilled out on to the Internet:
https://stackoverflow.com/questions/31801257/why-required-an...
In proto3, they removed the ability to specify whether a field is optional. People say that the reason is “backward compatibility”, which is true, but I think Hickey’s analysis actually gets to the core of the issue.
The issue is that the shape/schema of data is an idea that can be reused across multiple contexts, while optional/required is context-specific. They are two separate things conflated by type systems when you use constructs like Maybe.
When you are threading protocol buffers through long chains of servers written and deployed at different times (and there are many of these core distributed types at Google), then you'll start to appreciate why this is an important issue.
(copy of lobste.rs comment)