If you have followed the Haskell community much, you may have heard the phrase "programmable semicolon" in relation to monads. Of course, it is not obvious what is meant by such a thing, so I figured I'd write a short explanation for those already familiar with monads.
First, let's look at an example of how the semicolon works in Rust (because I don't know C):
fn main() {
let x = 13;
println!("{}", x)
}
The semicolon separates statements. This means that we can have
multiple statements within one function (here, we first have a let
statement
and then our returned value).
In Haskell, the closest thing to a semicolon is the >>
operator. We can write
something like the above Rust code in the following manner:
import Control.Monad.State.Lazy
import qualified Data.Map as M
main :: IO ()
main = evalStateT st mempty
st :: StateT (M.Map String Int) IO ()
st = modify (M.insert "x" 13) >>
(liftIO . print =<< gets (M.lookup "x"))
The first thing we should note is that >>
is more generic - the type signature
is (>>) :: m a -> m b -> m b
, and we can use it for state monad actions.
That's where part of the "programmable semicolon" comes from: in Rust, we have
statements, and they are executed in sequence; in Haskell, we have monadic
actions, and they are strung together with functions. In Rust, the programmer
has no way of changing this sequencing behavior; ;
has to be a compiler
built-in and hence it forces an implicit assumption about how programs
work onto all programs written with in Rust.
This is a subtle point. After all, what is to stop the
programmer from writing monads once Rust has higher-kinded types? What Haskell
has - that Rust, Elm, PureScript, Scala, and F# don't - is laziness. This
allows us to write (>>) :: m a -> m b -> m b
as a function.
This is not possible when the arguments are evaluated strictly; as an
illustration consider the following program:
import System.Exit
main :: IO ()
main =
exitSuccess >>
undefined
Had >>
been strict in its arguments, it would have failed immediately.
Hence, laziness allows us to use plain functions to manipulate actions, avoiding
the need for compiler built-ins like ;
.
All this might also help to explain another Haskell maxim: "Haskell is the best imperative language." In Haskell (or Idris), functions are truly first-class, to the point where we can use them in places traditionally assumed to be the domain of the compiler.