Zulip Chat Archive
Stream: new members
Topic: Error translating FPIL IO example from do to bind
mars0i (Mar 26 2024 at 19:48):
I'm at a stage (with Lean, Haskell, Idris, etc.) where I find it helpful to translate do
blocks into code that uses only >>=
. I want to see what the do
code reduces to. FPIL section 2.4 includes this example:
def fileStream (filename : System.FilePath) : IO (Option IO.FS.Stream) := do
let fileExists ← filename.pathExists
if not fileExists then
let stderr ← IO.getStderr
stderr.putStrLn s!"File not found: {filename}"
pure none
else
let handle ← IO.FS.Handle.mk filename IO.FS.Mode.read
pure (some (IO.FS.Stream.ofHandle handle))
This type checks with my configuration, as expected, but my translation of it doesn't:
def myFileStream (filename : System.FilePath) : IO (Option IO.FS.Stream) :=
filename.pathExists >=
λ fileExists => if not fileExists then
IO.getStderr >>=
λ stderr => stderr.putStrLn s!"File not found: {filename}"
else
IO.FS.Handle.mk filename IO.FS.Mode.read >>=
λ handle => pure (some (IO.FS.Stream.ofHandle handle))
There are two errors. On stderr.putStrLn s!"File not found: {filename}"
:
▶ 62:33-62:79: error:
type mismatch
IO.FS.Stream.putStrLn stderr (toString "File not found: " ++ toString filename ++ toString "")
has type
IO Unit : Type
but is expected to have type
BaseIO ?m.1830 : Type
On IO.FS.Handle.mk filename IO.FS.Mode.read
:
▶ 64:21-64:61: error:
application type mismatch
bind (IO.FS.Handle.mk filename IO.FS.Mode.read)
argument
IO.FS.Handle.mk filename IO.FS.Mode.read
has type
IO IO.FS.Handle : Type
but is expected to have type
BaseIO ?m.2145 : Type
It seems relevant that in the original FPIL version with do
, the types of IO.getStderr
and IO.FS.Handle.mk
are preceded by "IO" and a space. In my version, the types of the same functions are preceded by "BaseIO" and a space. Is therew some special Lean do
magic going on here, or am I making a silly mistake (not unlikely)?
I'm not sure what a keyword + space before a type means, but it seems as if without do
, BaseIO
is needed, but with do
, IO
is needed.
(Feel free to answer by pointing me to an online text or another thread! I tried searching here without success--not sure what search string to use.)
Timo Carlin-Burns (Mar 26 2024 at 19:52):
This might not be the problem, but why did the pure none
disappear in your translation?
Yaël Dillies (Mar 26 2024 at 19:52):
Do you know you can #print fileStream
to see what the do
block reduces to?
mars0i (Mar 26 2024 at 19:52):
Yeah, just noticed the missing pure none
. Hold on ....
mars0i (Mar 26 2024 at 19:55):
With pure none
the first error changes, but seems similar.
def myFileStream (filename : System.FilePath) : IO (Option IO.FS.Stream) :=
filename.pathExists >=
λ fileExists => if not fileExists then
IO.getStderr >>=
λ stderr => stderr.putStrLn s!"File not found: {filename}" >>=
λ _ => pure none
else
IO.FS.Handle.mk filename IO.FS.Mode.read >>=
λ handle => pure (some (IO.FS.Stream.ofHandle handle))
Here's the new error:
▶ 62:33-62:79: error:
application type mismatch
bind (IO.FS.Stream.putStrLn stderr (toString "File not found: " ++ toString filename ++ toString ""))
argument
IO.FS.Stream.putStrLn stderr (toString "File not found: " ++ toString filename ++ toString "")
has type
IO Unit : Type
but is expected to have type
BaseIO ?m.1867 : Type
mars0i (Mar 26 2024 at 19:56):
@Yaël Dillies , no! Didn't know.
mars0i (Mar 26 2024 at 19:58):
There are added liftM
s:
▶ 57:1-57:7: information:
def fileStream : System.FilePath → IO (Option IO.FS.Stream) :=
fun filename => do
let fileExists ← liftM (System.FilePath.pathExists filename)
if (!fileExists) = true then do
let stderr ← liftM IO.getStderr
IO.FS.Stream.putStrLn stderr (toString "File not found: " ++ toString filename ++ toString "")
pure none
else do
let handle ← IO.FS.Handle.mk filename IO.FS.Mode.read
pure (some (IO.FS.Stream.ofHandle handle))
which converts from one monad to another (as I would put it) according to liftM
's documentation. But still, not sure I understand what's going on.
Timo Carlin-Burns (Mar 26 2024 at 20:03):
BaseIO
is another monad which is like IO
without exceptions. Maybe one of the API functions returns a value in BaseIO
and the do
notation was automatically inserting coercions using MonadLift
?
mars0i (Mar 26 2024 at 20:14):
That sounds reasonable @Timo Carlin-Burns. Thanks. I'll look at it more later, then. (I might not like it. I know I'm in a minority, but in Haskell, I think do
hides what is really happening from novices. I was very confused by do
at first. As I said, it's a minority perspective. But packing more syntactic sugar into do
means it's harder to understand what it's doing. Oh well. Realistic, flexible IO is always going to be somewhat messy.)
mars0i (Mar 28 2024 at 13:31):
I'm trying to fix my do
-less function by using what I am finding using #print
on the original FPIL do
-based definition. This is making me hate Lean's do
. It causes what appears to be the same function appear to have different types when surrounded by do
from the type it appears to have outside of do
. How is that a good thing? It inserts additional calls in ways that I can't figure out how to reproduce in my code. Granted, I'm a beginner, but I'm looking at the #print
output and I can't figure out what it is doing. It's easy to translate Haskell's do
into what it is syntactic sugar for, but it's not easy to do that with Lean's do
. So what it is actually doing becomes confusing and mysterious. If it works, great, but if it ever doesn't, it may be difficult to diagnose. Functional programming is supposed to free us from side-effects that are not apparently on the face of the code. Lean's do
is doing something analogous to producing non-apparent side-effects, by rewriting code in ways that are not obvious. Saying this won't change anything.
Mauricio Collares (Mar 28 2024 at 13:34):
Did you take a look at https://lean-lang.org/papers/do.pdf?
mars0i (Mar 28 2024 at 13:34):
No, didn't know about that. Thanks @Mauricio Collares . I'll look at it.
mars0i (Mar 28 2024 at 21:58):
I apologize for being so rant-ey in my earlier post. I do appreciate the link to the article that Maurcio provided.
(I've skimmed the PDF, and feel it's not obvious that it clarifies what bothered me, but also that it wouldn't be worth my trouble at this stage to try to figure out whether the answer is there. It's OK. I can use do
in the ways suggested in teaching materials without trying to understand it. I didn't decide to learn Lean for the sake of do
. :smile: )
mars0i (Mar 28 2024 at 22:15):
I will say that copying do
boilerplate for the sake of building up intuitive pattern intuitions is going to have to be part of my strategy with do
. That's OK. (do
's semantics in Haskell weren't clear to me until I learned how to translate away do
. I know many people think that do
is easy to understand because "it's just like imperative languages", but I never felt that way, despite having more experience with imperative than functional languages--like most people. Everyone's different.)
mars0i (Mar 28 2024 at 22:41):
And it is good that I went through this experience with Lean's do
.
Henrik Böving (Mar 28 2024 at 23:05):
mars0i said:
I apologize for being so rant-ey in my earlier post. I do appreciate the link to the article that Maurcio provided.
(I've skimmed the PDF, and feel it's not obvious that it clarifies what bothered me, but also that it wouldn't be worth my trouble at this stage to try to figure out whether the answer is there. It's OK. I can use
do
in the ways suggested in teaching materials without trying to understand it. I didn't decide to learn Lean for the sake ofdo
. :smile: )
The point of the automated insertion of functions (namely the only automated insertion that is happening is lifting of monads) is that you don't want users to think about it. For example when I am doing meta programming I'm in MetaM
which is a StateT
+ ReaderT
+ CoreM
. When I call a function that operates just in CoreM
I don't want to have to think about why I'm suddenly getting a type error and that I have to remember inserting a liftM
. It should just work as its obvious that I can do this.
Henrik Böving (Mar 28 2024 at 23:07):
mars0i said:
That sounds reasonable Timo Carlin-Burns. Thanks. I'll look at it more later, then. (I might not like it. I know I'm in a minority, but in Haskell, I think
do
hides what is really happening from novices. I was very confused bydo
at first. As I said, it's a minority perspective. But packing more syntactic sugar intodo
means it's harder to understand what it's doing. Oh well. Realistic, flexible IO is always going to be somewhat messy.)
I also think that its fine for do
to hide what's going on. With the <-
arrow you do not need to know at all that it is about some bind operation that is defined in some funky way. Instead the mental model of <-
can just be "this performs some additional thing specific to the monad" In IO thats running an IO action, in State that's accessing and potentially modifying the state etc. A user does not really need to know how to desugar into bind sequences in order for this to work out.
mars0i (Mar 29 2024 at 02:54):
Thanks @Henrik Böving. That makes sense, and it's helpful to have that context. I kind of prefer to take a different approach, but this just isn't the best situation for my preferred strategy. It won't be a big deal to go along with the standard Lean do
procedure when I need IO, state, etc. I could say more about what I prefer and why, but it's not very interesting. There's no point to me going on about it.
Patrick Massot (Mar 29 2024 at 02:59):
Note that nobody forces you to use do
.
Kyle Miller (Mar 29 2024 at 03:44):
set_option pp.notation false
causes the pretty printer to stop using do
notation (among other notations)
mars0i (Mar 29 2024 at 16:15):
Thanks @Kyle Miller. That's extremely useful for what I was trying to understand.
Last updated: May 02 2025 at 03:31 UTC