The Lean Language Reference

14.2. Lifting Monads🔗

When one monad is at least as capable as another, then actions from the latter monad can be used in a context that expects actions from the former. This is called lifting the action from one monad to another. Lean automatically inserts lifts when they are available; lifts are defined in the MonadLift type class. Automatic monad lifting is attempted before the general coercion mechanism.

🔗type class
MonadLift.{u, v, w}
  (m : semiOutParam (Type uType v))
  (n : Type uType w) :
  Type (max (max (u + 1) v) w)

Computations in the monad m can be run in the monad n. These translations are inserted automatically by the compiler.

Usually, n consists of some number of monad transformers applied to m, but this is not mandatory.

New instances should use this class, MonadLift. Clients that require one monad to be liftable into another should instead request MonadLiftT, which is the reflexive, transitive closure of MonadLift.

Instance Constructor

MonadLift.mk.{u, v, w}

Methods

monadLift : {α : Type u} → m αn α

Translates an action from monad m into monad n.

Lifting between monads is reflexive and transitive:

  • Any monad can run its own actions.

  • Lifts from m to m' and from m' to n can be composed to yield a lift from m to n. The utility type class MonadLiftT constructs lifts via the reflexive and transitive closure of MonadLift instances. Users should not define new instances of MonadLiftT, but it is useful as an instance implicit parameter to a polymorphic function that needs to run actions from multiple monads in some user-provided monad.

🔗type class
MonadLiftT.{u, v, w} (m : Type uType v)
  (n : Type uType w) :
  Type (max (max (u + 1) v) w)

Computations in the monad m can be run in the monad n. These translations are inserted automatically by the compiler.

Usually, n consists of some number of monad transformers applied to m, but this is not mandatory.

This is the reflexive, transitive closure of MonadLift. Clients that require one monad to be liftable into another should request an instance of MonadLiftT. New instances should instead be defined for MonadLift itself.

Instance Constructor

MonadLiftT.mk.{u, v, w}

Methods

monadLift : {α : Type u} → m αn α

Translates an action from monad m into monad n.

Monad Lifts in Function Signatures

The function IO.withStdin has the following signature:

IO.withStdin.{u} {m : Type Type u} {α : Type} [Monad m] [MonadFinally m] [MonadLiftT BaseIO m] (h : IO.FS.Stream) (x : m α) : m α

Because it doesn't require its parameter to precisely be in IO, it can be used in many monads, and the body does not need to restrict itself to IO. The instance implicit parameter MonadLiftT BaseIO m allows the reflexive transitive closure of MonadLift to be used to assemble the lift.

When a term of type n β is expected, but the provided term has type m α, and the two types are not definitionally equal, Lean attempts to insert lifts and coercions before reporting an error. There are the following possibilities:

  1. If m and n can be unified to the same monad, then α and β are not the same. In this case, no monad lifts are necessary, but the value in the monad must be coerced. If the appropriate coercion is found, then a call to Lean.Internal.coeM is inserted, which has the following signature:

    Lean.Internal.coeM.{u, v} {m : Type u Type v} {α β : Type u} [(a : α) CoeT α a β] [Monad m] (x : m α) : m β
  2. If α and β can be unified, then the monads differ. In this case, a monad lift is necessary to transform an expression with type m α to n α. If m can be lifted to n (that is, there is an instance of MonadLiftT m n) then a call to liftM, which is an alias for MonadLiftT.monadLift, is inserted.

    liftM.{u, v, w} {m : Type u Type v} {n : Type u Type w} [self : MonadLiftT m n] {α : Type u} : m α n α
  3. If neither m and n nor α and β can be unified, but m can be lifted into n and α can be coerced to β, then a lift and a coercion can be combined. This is done by inserting a call to Lean.Internal.liftCoeM:

    Lean.Internal.liftCoeM.{u, v, w} {m : Type u Type v} {n : Type u Type w} {α β : Type u} [MonadLiftT m n] [(a : α) CoeT α a β] [Monad n] (x : m α) : n β

As their names suggest, Lean.Internal.coeM and Lean.Internal.liftCoeM are implementation details, not part of the public API. In the resulting terms, occurrences of Lean.Internal.coeM, Lean.Internal.liftCoeM, and coercions are unfolded.

Lifting IO Monads

There is an instance of MonadLift BaseIO IO, so any BaseIO action can be run in IO as well:

def fromBaseIO (act : BaseIO α) : IO α := act

Behind the scenes, liftM is inserted:

fun {α} act => liftM act : {α : Type} → BaseIO α → EIO IO.Error α#check fun {α} (act : BaseIO α) => (act : IO α)
fun {α} act => liftM act : {α : Type} → BaseIO α → EIO IO.Error α
Lifting Transformed Monads

There are also instances of MonadLift for most of the standard library's monad transformers, so base monad actions can be used in transformed monads without additional work. For example, state monad actions can be lifted across reader and exception transformers, allowing compatible monads to be intermixed freely:

def incrBy (n : Nat) : StateM Nat Unit := modify (· + n) def incrOrFail : ReaderT Nat (ExceptT String (StateM Nat)) Unit := do if ( read) > 5 then throw "Too much!" incrBy ( read)

Disabling lifting causes an error:

set_option autoLift false def incrBy (n : Nat) : StateM Nat Unit := modify (. + n) def incrOrFail : ReaderT Nat (ExceptT String (StateM Nat)) Unit := do if ( read) > 5 then throw "Too much!" type mismatch incrBy __do_lift✝ has type StateM Nat Unit : Type but is expected to have type ReaderT Nat (ExceptT String (StateM Nat)) Unit : TypeincrBy ( read)
type mismatch
  incrBy __do_lift✝
has type
  StateM Nat Unit : Type
but is expected to have type
  ReaderT Nat (ExceptT String (StateM Nat)) Unit : Type

Automatic lifting can be disabled by setting autoLift to false.

🔗option
autoLift

Default value: true

Insert monadic lifts (i.e., liftM and coercions) when needed.

14.2.1. Reversing Lifts

Monad lifting is not always sufficient to combine monads. Many operations provided by monads are higher order, taking an action in the same monad as a parameter. Even if these operations are lifted to some more powerful monad, their arguments are still restricted to the original monad.

There are two type classes that support this kind of “reverse lifting”: MonadFunctor and MonadControl. An instance of MonadFunctor m n explains how to interpret a fully-polymorphic function in m into n. This polymorphic function must work for all types α: it has type {α : Type u} m α m α. Such a function can be thought of as one that may have effects, but can't do so based on specific values that are provided. An instance of MonadControl m n explains how to interpret an arbitrary action from m into n, while at the same time providing a “reverse interpreter” that allows the m action to run n actions.

14.2.1.1. Monad Functors

🔗type class
MonadFunctor.{u, v, w}
  (m : semiOutParam (Type uType v))
  (n : Type uType w) :
  Type (max (max (u + 1) v) w)

A way to interpret a fully-polymorphic function in m into n. Such a function can be thought of as one that may change the effects in m, but can't do so based on specific values that are provided.

Clients of MonadFunctor should typically use MonadFunctorT, which is the reflexive, transitive closure of MonadFunctor. New instances should be defined for MonadFunctor.

Instance Constructor

MonadFunctor.mk.{u, v, w}

Methods

monadMap : {α : Type u} → ({β : Type u} → m βm β) → n αn α

Lifts a fully-polymorphic transformation of m into n.

🔗type class
MonadFunctorT.{u, v, w} (m : Type uType v)
  (n : Type uType w) :
  Type (max (max (u + 1) v) w)

A way to interpret a fully-polymorphic function in m into n. Such a function can be thought of as one that may change the effects in m, but can't do so based on specific values that are provided.

This is the reflexive, transitive closure of MonadFunctor. It automatically chains together MonadFunctor instances as needed. Clients of MonadFunctor should typically use MonadFunctorT, but new instances should be defined for MonadFunctor.

Instance Constructor

MonadFunctorT.mk.{u, v, w}

Methods

monadMap : {α : Type u} → ({β : Type u} → m βm β) → n αn α

Lifts a fully-polymorphic transformation of m into n.

14.2.1.2. Reversible Lifting with MonadControl

🔗type class
MonadControl.{u, v, w}
  (m : semiOutParam (Type uType v))
  (n : Type uType w) :
  Type (max (max (u + 1) v) w)

A way to lift a computation from one monad to another while providing the lifted computation with a means of interpreting computations from the outer monad. This provides a means of lifting higher-order operations automatically.

Clients should typically use control or controlAt, which request an instance of MonadControlT: the reflexive, transitive closure of MonadControl. New instances should be defined for MonadControl itself.

Instance Constructor

MonadControl.mk.{u, v, w}

Methods

stM : Type uType u

A type that can be used to reconstruct both a returned value and any state used by the outer monad.

liftWith : {α : Type u} → (({β : Type u} → n βm (MonadControl.stM m n β)) → m α) → n α

Lifts an action from the inner monad m to the outer monad n. The inner monad has access to a reverse lifting operator that can run an n action, returning a value and state together.

restoreM : {α : Type u} → m (MonadControl.stM m n α) → n α

Lifts a monadic action that returns a state and a value in the inner monad to an action in the outer monad. The extra state information is used to restore the results of effects from the reverse lift passed to liftWith's parameter.

🔗type class
MonadControlT.{u, v, w} (m : Type uType v)
  (n : Type uType w) :
  Type (max (max (u + 1) v) w)

A way to lift a computation from one monad to another while providing the lifted computation with a means of interpreting computations from the outer monad. This provides a means of lifting higher-order operations automatically.

Clients should typically use control or controlAt, which request an instance of MonadControlT: the reflexive, transitive closure of MonadControl. New instances should be defined for MonadControl itself.

Instance Constructor

MonadControlT.mk.{u, v, w}

Methods

stM : Type uType u

A type that can be used to reconstruct both a returned value and any state used by the outer monad.

liftWith : {α : Type u} → (({β : Type u} → n βm (stM m n β)) → m α) → n α

Lifts an action from the inner monad m to the outer monad n. The inner monad has access to a reverse lifting operator that can run an n action, returning a value and state together.

restoreM : {α : Type u} → stM m n αn α

Lifts a monadic action that returns a state and a value in the inner monad to an action in the outer monad. The extra state information is used to restore the results of effects from the reverse lift passed to liftWith's parameter.

🔗def
control.{u, v, w} {m : Type uType v}
  {n : Type uType w} [MonadControlT m n]
  [Bind n] {α : Type u}
  (f :
    ({β : Type u} → n βm (stM m n β)) →
      m (stM m n α)) :
  n α

Lifts an operation from an inner monad to an outer monad, providing it with a reverse lifting operator that allows outer monad computations to be run in the inner monad. The lifted operation is required to return extra information that is required in order to reconstruct the reverse lift's effects in the outer monad; this extra information is determined by stM.

This function takes the inner monad as an implicit parameter. Use controlAt to specify it explicitly.

🔗def
controlAt.{u, v, w} (m : Type uType v)
  {n : Type uType w} [MonadControlT m n]
  [Bind n] {α : Type u}
  (f :
    ({β : Type u} → n βm (stM m n β)) →
      m (stM m n α)) :
  n α

Lifts an operation from an inner monad to an outer monad, providing it with a reverse lifting operator that allows outer monad computations to be run in the inner monad. The lifted operation is required to return extra information that is required in order to reconstruct the reverse lift's effects in the outer monad; this extra information is determined by stM.

This function takes the inner monad as an explicit parameter. Use control to infer the monad.

Exceptions and Lifting

One example is Except.tryCatch:

Except.tryCatch.{u, v} {ε : Type u} {α : Type v} (ma : Except ε α) (handle : ε Except ε α) : Except ε α

Both of its parameters are in Except ε. MonadLift can lift the entire application of the handler. The function getBytes, which extracts the single bytes from an array of Nats using state and exceptions, is written without Lean.Parser.Term.do : termdo-notation or automatic lifting in order to make its structure explicit.

set_option autoLift false def getByte (n : Nat) : Except String UInt8 := if n < 256 then pure n.toUInt8 else throw s!"Out of range: {n}" def getBytes (input : Array Nat) : StateT (Array UInt8) (Except String) Unit := do input.forM fun i => liftM (Except.tryCatch (some <$> getByte i) fun _ => pure none) >>= fun | some b => modify (·.push b) | none => pure () Except.ok #[1, 58, 255, 2]#eval getBytes #[1, 58, 255, 300, 2, 1000000] |>.run #[] |>.map (·.2)
Except.ok #[1, 58, 255, 2]

getBytes uses an Option returned from the lifted action to signal the desired state updates. This quickly becomes unwieldy if there is more than one way to react to the inner action, such as saving handled exceptions. Ideally, state updates would be performed within the tryCatch call directly.

Attempting to save bytes and handled exceptions does not work, however, because the arguments to Except.tryCatch have type Except String Unit:

def getBytes' (input : Array Nat) : StateT (Array String) (StateT (Array UInt8) (Except String)) Unit := do input.forM fun i => liftM (Except.tryCatch (getByte i >>= fun b => failed to synthesize MonadStateOf (Array UInt8) (Except String) Additional diagnostic information may be available using the `set_option diagnostics true` command.modifyThe (Array UInt8) (·.push b)) fun e => failed to synthesize MonadStateOf (Array String) (Except String) Additional diagnostic information may be available using the `set_option diagnostics true` command.modifyThe (Array String) (·.push e))
failed to synthesize
  MonadStateOf (Array String) (Except String)

Additional diagnostic information may be available using the `set_option diagnostics true` command.

Because StateT has a MonadControl instance, control can be used instead of liftM. It provides the inner action with an interpreter for the outer monad. In the case of StateT, this interpreter expects that the inner monad returns a tuple that includes the updated state, and takes care of providing the initial state and extracting the updated state from the tuple.

def getBytes' (input : Array Nat) : StateT (Array String) (StateT (Array UInt8) (Except String)) Unit := do input.forM fun i => control fun run => (Except.tryCatch (getByte i >>= fun b => run (modifyThe (Array UInt8) (·.push b)))) fun e => run (modifyThe (Array String) (·.push e)) Except.ok (#["Out of range: 300", "Out of range: 1000000"], #[1, 58, 255, 2])#eval getBytes' #[1, 58, 255, 300, 2, 1000000] |>.run #[] |>.run #[] |>.map (fun (((), bytes), errs) => (bytes, errs))
Except.ok (#["Out of range: 300", "Out of range: 1000000"], #[1, 58, 255, 2])