/salmon

Monads for Common Lisp

Primary LanguageCommon Lisp

https://github.com/HenryS1/salmon/actions/workflows/ci.yaml/badge.svg

Salmon

An extensible foundation for monads in Common Lisp.

Why does Lisp need Monads?

Monads provide a uniform interface to control flow for a variety of different computations.

Error handling, missing values, asynchronous computation, parsing and more can be modelled with monads.

The term monads is used somewhat loosely in the context of this library which only requires candidate monads to implement fmap and flatmap.

Comprehensions

Salmon uses an implementation of fmap and flatmap to provide a do syntax similar to do comprehensions in Haskell and for comprehensions in Scala.

In Haskell a monad comprehension looks like this:

do 
  a <- [1,2,3]
  let c = 5
  b <- [4,5]
  return (a + b + c)

The same pattern is expressed in salmon with

(mdo (a '(1 2 3))
     (let (c 5))
     (b '(4 5))
     (yield (+ a b c)))

The mdo macro desugars to nested applications of fmap and flatmap.

The analogy between generic methods and type classes

Lisp’s generic methods can be implemented on data separate from the definition of that data. This allows generic methods to fulfill a similar to type classes from Haskell. The main difference is that typeclasses force the implementor to provide all methods in a contract while generic methods can be implemented one by one for any data type.

Builtin implementations

Salmon comes with builtin monad implementations for

Maybe

Maybe provides a way to handle missing values. just for when a value is present and nil for a missing value. An mdo comprehension shortcircuits on nil.

  (mdo (a (maybe:just 15))
       (b (maybe:just 4))
       (yield (+ a b)))
;; #<MAYBE:JUST 19>

Try

Try wraps any conditions in a value to instead of having them propagate. The try macro can be used to handle any conditions. The value of the expression is a failure if a condition occurred and otherwise it is a success with the final value provided in the expression (try provides an implicit progn).

(try:try (let ((a 10) 
               (b 0))
           (/ a b)))
; #<TRY:FAILURE arithmetic error DIVISION-BY-ZERO signalled
; Operation was (/ 10 0).>

In an mdo comprehension try shortcircuits on failures.

  (mdo (a (try:success 15))
       (b (try:success 4))
       (yield (+ a b)))
;; #<TRY:SUCCESS 19>

List

A list monad comprehension is shortcuited by the empty list. The result of a list monad comprehension uses all combinations of values from the unwrapped lists.

  (mdo (a '(1 2 3))
       (b '(4 5 6))
       (yield (+ a b)))
;; (5 6 7 6 7 8 7 8 9)

Vector

A vector monad comprehension is shortcircuited by the empty vector. As with lists a vector monad comprehension uses all combinations of values from the unwrapped vectors.

  (mdo (a #(1 2 3))
       (b #(4 5 6))
       (yield (+ a b)))
;; #(5 6 7 6 7 8 7 8 9)

Either

The either monad provides a mechanism for error handling holding either an error, in the left case, or a value, in the right case.

  (mdo (a (right 10))
       (b (right 5))
       (yield (+ a b)))
;; #<RIGHT 15>
(mdo (a (right 10))
     (b (left "error"))
     (yield (+ a b)))
;; #<LEFT error>