Minnesota Extensible Language Tools Group
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Monads

Silver (finally) supports a limited form of monads, most notably for IO.
A monad is just a monoid in the category of endofunctors (what’s the problem?), but since that definition is really helpful, I’ll break it down here briefly.

A monad is, fundamentally, just an action of some sort, which also somehow wraps a value. Monads can be constructed and combined through the use of combinators. The two most important ones that make something a monad are bind and pure. pure simply wraps a provided value to create a monad object that does nothing. bind takes a monad object and a function from a value to a monad object, and somehow evaluates or ‘runs’ the first monad object, extracts the value, then calls the function with that value to construct a second monad object. This second object is then evaluated, and the value and is then returned. The purpose of this is to allow two monad actions to be sequenced (somehow), and the value results of each monad action to be able to be used in the next action. Note that I said ‘somehow’. The specifics of this can be quite complex and depends on each monad.

Maybe

The simplest example of a monad that Silver supports is for Maybe. pure simply wraps the value in a just, and bind simply allows a computation to be performed with the value of another maybe action, contingent on that action’s success.

Lists

Lists are somewhat similar, where pure simply wraps the value in a single-element list. bind maps the provided function over all elements in the first list, so it acts as sort of a ‘nested for each’ construct, compared to a ‘nested if’ provided by Maybe

State

State is a bit different, in that it has its own type, compared with the maybe and list monads, which work on existing types. State is essentially a pair of a value and some form of ‘state’ value that gets passed around that tracks things in the computations. bind sequences two state actions, passing the value of the first as the parameter to the second, and threading the state value from one to the next. In addition, there are provided functions ‘getState’, which extracts the current state value and returns it as the monadic value, and setState, which takes a new state value with which to replace the current state value.

State is actually represented as a nonterminal, with pureState, pureState, getState, and setState as the constructors. The evaluation happens by threading the state value through the tree in a synthesized and inherited attribute, and the monadic value is returned as another synthesized attribute. In order to actually run the computation involving state, functions runState or evalState can be called which provide the input attributes and then return the results.

IO

IO can be thought of as essentially similar to the State monad, except that instead of threading a value which can be accessed by the user, an ‘IO token’ is threaded. This is further described in the reference on the old IO system . The basic constructors for IO actions are productions that run the basic IO functions as represented in the library.

One issue with this is that the result of an IO action occurring isn’t the only side effect possible. Running a computation takes time, so it would be important to be able to force a specific ordering of evaluation to occur. Sometimes demanding the IO token from an action could cause other computation values to be demanded before demanding the input IO token. To avoid this issue, when the IO token or value out is demanded from a bindIO node, the IO token is demanded from the left child via unsafeTrace to force the IO actions to be performed before any intensive computations that would occur before the IO token out from the right child could be performed.

Do notation

Since endlessly nesting bind calls is annoying, we can use ‘do notation’, instead. At its simplest, a do expression looks like:

do {
  <action1>;
  val1 <- <action2>;
  val2 <- <action3 involving val1>;
  <action 4 involving val1 and 2>;
  return <expression involving val1 and 2>;
}

Each action has a monadic type, and the vals are names. This translates to a sequence of binds, of the expressions on the right hand side of <- and a lambda with val1 as a parameter and the translation of the rest as the body. Sometimes we want to ignore the value returned by an action, in which case the lambda has a dummy parameter that gets ignored. return is simply syntactic sugar for calling pure.

We sometimes may also wish to bind non-monadic values within a monadic computation. We may also wish to conditionally perform monadic actions, by nesting do-expressions inside of ifs. For example:

local result::IOMonad<Integer> = do {
  txt::String <- readFileM("file.txt");
  let isEmpty::Boolean = length(txt) == 0;
  if isEmpty then
    printM("Empty!\n)
  else pure(());
  if txt == "Hello" then do {
    printM("World");
    return 2;
  }
  else
    pure(length(txt));
};

The bind and pure operations for any particular monadic type are specified via type classes. Silver also implements GHC’s applicative do desugaring, using the map and ap methods of the Functor and Applicative type classes in place of bind where possible; this sometimes allows for better efficiency.

TODO

I’m not including the documentation for specific functions here, yet. This seems like it should be generated?