Rust also has a Maybe type as well. You use it like this:
use core::io::{Reader,ReaderUtil};
use core::io::println;
fn main() {
let in = io::stdin().read_line();
println("INPUT:");
println(int::str(int::from_str(in)));
}
This code will produce an error, because `int::from_str` can fail. Imagine trying to convert a number from the string "foobar". $ make
rustc option.rs
option.rs.rs:125:23: 125:41 error: mismatched types: expected `int` but found
`core::option::Option<int>` (expected int but found enum core::option::Option)
option.rs:125 io::println(int::str(int::from_str(in)));
^~~~~~~~~~~~~~~~~~
error: aborting due to previous error
make: *** [build] Error 101
What Rust is saying here is that we don't have the right types: `int::str` takes an int, but `int::from_str` returns an `Option<int>`.We can make the compiler satisfied by pattern matching over the return value:
use core::io::{Reader,ReaderUtil};
use core::io::println;
fn main() {
let in = io::stdin().read_line();
match int::from_str(in) {
Some(number_string) => println(int::to_str(number_string)),
None => println("Hey, put in a number.")
}
}
Now, we explicitly handle both cases (Some(x) being a success, and None being the failure), and the compiler is happy.This is where the advantages of Maybe come into play. In dynamic languages, you don't get the advantage of always making sure you handle the degenerate case.
Now, what the Maybe _monad_ does is allow you to chain multiple things that work with Maybe together. I don't have a Rust example of this handy. Like "Do this, do that, do the other thing, and then let's see if that failed anywhere along the way."
Asynchronous execution is one particular case that I like as an example: You can implement promises (which represent async computations) in Haskell and such a Promise type will also be able to implement the monad type class. Because they are monads, you can use do notation:
-- Let's assume we want to fetch two large datasets asynchronously over the network and filter them
asyncSanitize :: Promise [String] -> Promise [String] -> Promise [String]
asyncSanitize p1 p2 = do
x <- p1
y <- p2
return (sanitize (x ++ y)) -- no callbacks!
which looks like the same code you would use for filtering two Maybe values using do notation. In fact, it is the same code, and if you leave out the type signature, then "asyncSanitize" will work for any monadic value: you could easily test your function using eg. Identity [String] (pure values), IO String (synchronous fetching), or decide to switch to some Database monad instead that fetches the values on demand from the Database. The code will not need to change at all.Think of Maybe as a container. It either "contains" a value, or it doesn't. The container itself is not usable except for passing around; it's the contained value that is important. Even if we know that the container is supposed to contain a number, we can't use the container to perform computations; every time we need the number, we need to open the container, peek inside, and decide what to do whether it's contains something or nothing.
In Haskell terms, this "peeking inside" is typically done with pattern matching, which is core to the type system. For example, this is a function that checks if a Maybe value has anything, and returns true or false:
hasValue x =
case x of
Just _ -> True
Nothing -> False
The line "Just _" is a kind of wildcard expression, meaning: "Check if x matches the pattern Just [something]".At the opposite end, we can have functions that produce Maybes (naive and simplified example, not working code):
fetchPage url =
do response <- performHttpRequest "GET" url
case (getStatusCode response)
200 -> return (Just (getBody response))
404 -> return Nothing
Notice how "Just" looks like a function call. That's because it is. It's a function that "constructs" a value of, in this case, type "Maybe String". The "Nothing" returned is also a value of type "Maybe String". Thanks to Haskell's type inference, this is known at compile time.In languages such as Java or Ruby we can build classes that are containers that act in a similar way. But those languages are limited in that "peek inside the container" logic needs to happen at runtime. In Haskell, the container's possible values are enforced entirely by the type system at compile time.
To really appreciate this, you really have to be familiar with statically typed languages such as C++ or Java. In such languages, you have "nil" or "null" or "NULL" values to represent "nothing". Any time you have a pointer, you have to explicitly check if the pointer is valid; if you try to use it, and it happens to be null, the program will blow up at runtime, but not at compile time.
In Haskell, there is no generic null value. The value "Nothing" on its own belongs to a specific type, such as "Maybe String", and won't work with another such as "Maybe Integer". Thus when we want to use "fetchPage", we are forced into a situation where must make a choice based on whether there is a value or not (again, naive example):
crawlPageRecursively url =
do contents <- fetchPage url
case contents of
Just s -> map crawlPageRecursively (map (\body -> getLinks body) s)
Nothing -> putStrLn ("Failed to fetch " ++ url)
Since Haskell doesn't have null pointers, and can never end up in a situation where you try to use a null where you expected a value, because its type system makes it impossible.This was my attempt at "explain it like I'm five" Haskell as carefully as I could. The real world is somewhat more complex, but not awfully so. As you say, too often haskellers will get into "formal academic theory", even when, in my opinion, it's not necessary. Haskell becomes more elegant and beautiful if you study and know group theory, but most people don't, and it really just obscures the language for ordinary, mortal programmers who are trying to learn it.
As for monads, it's true that Maybe is a monad, but think of it as a convenience, in the sense that this lets it play well with the rest of the monad infrastructure. You can use, and even implement, Maybe without involving the monad aspect.
My best suggestion regarding monads is to just start using the language; as DanWaterworth points out, monads are pretty much inexplicable for beginners until one really needs to use them. Haskell actually lets you use some monads (like IO) without requiring any deep understanding of what they are. Case in point: The above examples actually use the IO monad, concealed by the "do" and the "<-" syntax. But because this syntactic sugar makes the language look like an imperative language, you can use this stuff without too much deep knowledge about just exactly how it works under the hood.
Maybe is usually used as an example since it's the simplest non-trivial monad (and therefore also an applicative functor).
The point of eg. fmap etc. is that you can have regular Int -> Bool functions, and apply them to data with more structure than simple values. a Maybe String is not a String, but you can still transform one to uppercase by doing fmap (map toUpper) mstring. The fun part is that that exact same thing works on a list of strings, an IO String (fmap (map toUpper) getLine), a Tree of Strings, etc... It's fully generic over anything that can implement Functor; the exact behaviour is defined by the particular instance.
Applicative is just an extension to the idea, where the function applied itself can have this extra structure, and when combined with an infix fmap you get to do
To see why this works has to do with currying. Just stare at the types of fmap and <* > long enough and you should see it eventually.a Monad is yet more powerful because it allows a previous computation to affect what follows in an arbitrary manner (Applicative doesn't), allowing things like state, asynchronous execution, continuations, and countless other things. It's such an useful abstraction that Haskell provides the do syntax for using it.
Edit: In languages like Java where all types are nullable, if you uppercase a String, but that String is null, your program breaks, even though the type system claims that you have a String when in reality all types are Maybes, and you are forced to always check for nulls manually or just rely on users to not give you nulls, even though it would be trivial for the compiler to check something like that statically if the language were expressive enough to allow it.