Zulip Chat Archive

Stream: lean4

Topic: lifting transformers


Chris Lovett (Aug 27 2022 at 03:58):

Trying to understand transformers and port https://mmhaskell.com/monads/transformers to lean, there is something I don't quite yet follow in the following code. I find validUserName is of type Except String String and not just String.

def readUserName : ExceptT String IO String := do
  IO.println "Please enter your username:"
  let stdin  IO.getStdin
  let str  stdin.getLine
  if str.length > 5 then
    return str.trim
  else throw "Username must be more than 5 characters"

def main : IO Unit := do
  try
    let validUserName  readUserName
    IO.println s!"Hello, {validUserName}!"
  catch e =>
    IO.println s!"Error: {e}"

I tried also readUserName .run but it doesn't help. So it seems I needs some sort of extra lift here to force the try/catch to do something useful here. I was able to fix it with my liftIO function, but why is this necessary, and is there some easier way to do this?

open EStateM

def liftIO (t : Except String α) : IO α := do
  match t with
  | .ok r => Result.ok r
  | .error s => Result.error s

def main : IO Unit := do
  try
    let validUserName  readUserName >>= liftIO
    IO.println s!"Hello, {validUserName}!"
  catch e =>
    IO.println s!"Error: {e}"

The liftIO now cases the error case to go to the catch block which is what I wanted?

Mario Carneiro (Aug 27 2022 at 04:11):

try ... catch desugars to tryCatch, which does not change the type in the body of the try block. So it can't be used to eliminate an Except ... wrapper around the type

Chris Lovett (Aug 27 2022 at 04:26):

Ok, I figured it had something to do with my main type of IO String not matching so I moved the code to a new function and now it works fine:

def login : ExceptT String IO String:= do
  try
    let username  readUserName
    let email  readEmail
    let password  readPassword
    return s!"Hello, {username}, we are logging you in...!"
  catch e =>
    throw e

def main : IO Unit := do
  IO.println ( login)

Mario Carneiro (Aug 27 2022 at 04:26):

The simplest fix to your code is to remove the ExceptT like so:

def readUserName : IO String := do
  IO.println "Please enter your username:"
  let stdin  IO.getStdin
  let str  stdin.getLine
  if str.length > 5 then
    return str.trim
  else throw "Username must be more than 5 characters"

Chris Lovett (Aug 27 2022 at 04:28):

Woah, what? IO is not "Except" as far as I know so where did that throw come from, something to do with that tricky up arrow ?

Mario Carneiro (Aug 27 2022 at 04:28):

IO is a MonadExcept

Chris Lovett (Aug 27 2022 at 04:28):

So demonstrating monad transformers with ExceptT String IO String is a bad idea then?

Mario Carneiro (Aug 27 2022 at 04:28):

but the error type is IO.Error so you need to coerce it from String

Mario Carneiro (Aug 27 2022 at 04:29):

How about using a simple monad as the base, like StateM Nat?

Chris Lovett (Aug 27 2022 at 04:30):

So is a notation for coercion? Where is it defined?

Mario Carneiro (Aug 27 2022 at 04:30):

https://leanprover-community.github.io/mathlib4_docs///Init/Coe.html

Chris Lovett (Aug 27 2022 at 04:31):

Found it, thanks: syntax:1024 (name := coeNotation) "↑" term:1024 : term

Chris Lovett (Aug 27 2022 at 04:34):

Hmmm, ok, the haskell chapter uses OptionT IO String which I also found a bit contrived, given IO can already return errors. So I'll switch to something more like what I wrote in https://github.com/leanprover/lean4-samples/tree/main/SimpleMonads#monad-composition like functions of type StateT Nat (ExceptT String Id) Float.

Mario Carneiro (Aug 27 2022 at 04:34):

@Leonardo de Moura In this code:

example : IO Unit := throw "hello"
-- failed to synthesize instance
--   MonadExcept String IO

Is this expected / intended behavior? throw has the type

throw [MonadExcept ε m] : ε  m α

where ε is an outParam of MonadExcept, so there is a competition between using the ε from the argument (which is String) and the ε inferred from the MonadExcept instance (which is IO.Error).

Mario Carneiro (Aug 27 2022 at 04:35):

I think that simple monads like StateT and ExceptT are easier to understand if you are building things from scratch

Chris Lovett (Aug 27 2022 at 06:01):

I notice mmhaskell uses this:

class MonadTrans t where
  lift :: (Monad m) => m a -> t m a

And the book has these lift commands to access functions on inner monads, but I'm not finding the need for that in Lean? For example, here I'm able to access throw without any lift keywords:

abbrev Arguments := List String

def hasSwitch (name: String) : ReaderT Arguments (Except String) Bool := do
  let args  read
  if args.contains s!"--{name}" then
    return true
  else
    throw s!"Command line argument --{name} not found"

I assume lifting works differently in lean with MonadLift and all that - which seems to operate more on return values - lifting them to match some larger monad at the call site?

Sebastian Ullrich (Aug 27 2022 at 08:03):

Yes, automatic monad lifting is described in https://leanprover.github.io/papers/do.pdf#page=18

Leonardo de Moura (Aug 27 2022 at 14:28):

Mario Carneiro said:

Leonardo de Moura In this code:

example : IO Unit := throw "hello"
-- failed to synthesize instance
--   MonadExcept String IO

Is this expected / intended behavior? throw has the type

throw [MonadExcept ε m] : ε  m α

where ε is an outParam of MonadExcept, so there is a competition between using the ε from the argument (which is String) and the ε inferred from the MonadExcept instance (which is IO.Error).

The type class resolution procedure failed when trying to check whether the synthesized value type had the expected type. Here:
https://github.com/leanprover/lean4/blob/766afdd0bc6c6480113b6dd91033fea871028fcd/src/Lean/Meta/SynthInstance.lean#L695
I agree this is a different kind of failure. Moreover, it does not give the caller the opportunity to do something about it (e.g., adding a coercion).
We can get more information using set_option trace.Meta.synthInstance true. BTW, the new tracing infrastructure that @Gabriel Ebner implemented looks great. We get:
image.png
Then, we can expand the trace and get
image.png


Last updated: Dec 20 2023 at 11:08 UTC