I agree that this technique works well — as long as you need to use only one Maybe at a time. As soon as you want to combine multiple computations, each of which returns a Maybe, this quickly becomes horrible and cumbersome:
case parseNumber str0 of
Nothing -> Left "parse error"
Just str1 ->
case parseChar '=' str1 of
Nothing -> Left "parse error"
Just str2 ->
case parseNumber str2 of
Nothing -> Left "parse error"
Just str3 -> …
Monads are, quite simply, a way of abstracting this repeated structure.And of course, then you get some data structures which can’t be unwrapped at all. The standard example is Promises as seen in e.g. JavaScript, but there are others.
More precisely, the monad interface allows abstracting this particular repeated structure in much the same way an iterator allows abstracting the repeated process of "get the next thing", but monad does not exist for Maybe just as iterators do not exist for arrays.
It's easy to make the mistake of focusing on the Maybe portion of that repeated code snippet, but the value of the monad interface in that code snippet is everything other than the Maybe portion, that pattern of descending into a computation over and over again with a closure, that fits into a number of data structures and an even larger number of data structures that can be coerced into fitting into that pattern without much effort.
Moreover, Maybe is a bad way of understanding the monad interface and pattern because it does not fully use the monad interface. It doesn't take advantage of the way the Maybe value is passed through to thread any additional values through. While a valid example of a monad interface usage, it is also a degenerate one that makes for a difficult lens to understand the interface.
Another benefit to using Maybes is that you avoid the rigamarole of null checks at every call site where you want to use the value. If you have a function that returns null or a value, whenever you call that function you'll always have to add an if guard to validate it's not null. If it is, that function itself may return null, and callers to it will again have to implement the same check.
I wrote a JS implementation of the Option object and the readme has lots of specific examples about these benefits: https://github.com/sbernheim4/excoptional and a few blog posts on these ideas[0][1]
[0] https://sambernheim.com/#/blog/the-engineers-schrodingers-ca... [1] https://sambernheim.com/#/blog/building-a-monad
return myList?.deleteFirst?.takeFirst?.name
you could also say: if (myList != null){
firstDeleted = myList.deleteFirst;
if (firstDeleted != null){
newFirst = myList.takeFirst;
if( newFirst != null){
return name;
}
}
}
but I find the first one much easier to think about.Same reason why you iterate over an array instead of assigning each value to a temporary variable: you have less code.
For example for the list monad the transformation could be applied in parallel or not at all if the list is only partially consumed; for the IO monad you could perform every operation asynchronously in an event loop instead of blocking.
Here's a pretty odd one:
data Needs r a = Needs { execute :: (a -> r) -> r }
In other words, a value x :: Needs Int String is a computation which can return an Int if you provide it a means to convert a String into an Int. You might conceptually say that it will give us a "complex" or composite "view" into values of type String given a "simple" or "atomic" view. For instance, length (execute x) length :: Int
This type seems as though it almost certainly "contains" a value of type a (or String in the concrete example above). It might actually contain many such values. Here are three examples, the first two are the cases mentioned above. The third is similar to the None case with Maybes. ex1 :: Needs (\view -> view "Hello, world") -- contains one string
ex2 :: Needs (\view -> view "Hello, " + view "world") -- contains two strings
ex3 :: Needs (\view -> 0) -- contains no strings
We could also imagine more exotic manipulations occurring inside of the Needs. For example ex4 :: Needs (\view -> let n = view "Hello" in view (repeat n "world, ") \* n + 10)
It should also be clear that there's no way to "unwrap" a value of type Needs R A. This is obviously true if R and A are different types. You actually could imagine using mutable state, like a mutable vector v, to view all the As stored inside of a Needs (execute ex4) (\a -> { push v a; () }) -- not really Haskell syntax
But this throws away all of that repeat and * and + nonsense in ex4. It's not really the same as a true "unwrap".But Needs is still a monad.
pure :: a -> Needs r a
pure a = Needs (\view -> view a)
(>>=) :: Needs r a -> (a -> Needs r b) -> Needs r b
m >>= f = Needs (\view -> execute m (\a -> execute (f a) view))
So, Needs is an example of a Monad which cannot in general be unwrapped. Working with it is a little complex in most cases, but the monad operations help to consolidate a couple very common, useful operations on Needs.Finally, I'll give up the ghost and note that Needs is known as the Continuation-Passing Monad. It helps you write code in CPS style without having to actually, deliberately pass continuations all around. This can be really useful for a few dumb-programmer tricks, but it's also a pretty useful framework for building embedded domain-specific languages.
I get how Maybe types can be useful for parsing something or doing IO but once you get your Maybe value, wouldn't be the first action to unwrap it? If it is Nothing you can either provide a default value or just stop right there. I don't see the sense of dragging the Nothing further done with you and having to deal with potential nothingness further done the stack even if you have the tools to make it look nice.
Maybe there are other functors where this makes more practical sense?