Zulip Chat Archive

Stream: general

Topic: Question about the IO monad


Dhruv Bhatia (Apr 04 2024 at 07:26):

I haven't used lean since Lean3 and have been trying to get back into it. I'm currently trying to relearn how monads work and was playing around with some toy code which is throwing errors at me, but I can't for the life of me understand how to fix it:

def main : IO Unit := do
  IO.println "enter a line of text:"
  let stdin  IO.getStdin
  let input  stdin.getLine
  let some fifth  input[6]? | IO.println "input not long enough"
  IO.println fifth

I think it's pretty clear what I want the code to do - it should read in a user entered line of text, and then attempt to print it's 5th character, provided it is long enough to have a 5th character.

The error shows up at input[6]?and says

type mismatch
  input[6]?
has type
  Option ?m.280 : Type
but is expected to have type
  IO ?m.274 : Type

I suppose the error is caused because bind wants to take in an IO (Option String), not an Option String, which, fair enough. But if that's the case, does that mean I can never nest the Option monad into the IO monad? I saw this in the lean manual:

def showUserInfo (getUsername getFavoriteColor : IO (Option String)) : IO Unit := do
  let some n  getUsername | IO.println "no username!"
  IO.println s!"username: {n}"
  let some c  getFavoriteColor | IO.println "user didn't provide a favorite color!"
  IO.println s!"favorite color: {c}"

and I thought: "a-ha! If I just write a function of type String -> IO (Option String) that takes a string and optionally returns it's 5th character, maybe I can fix this". Here's what I tried:

def fifth (str : String) : IO (Option String) := do
  IO.println s!"input: {str}" -- this is just to demonstrate that it is using the IO monad
  return (str[6]?) -- this is the part I really care about

def main : IO Unit := do
  IO.println "enter a line of text:"
  let stdin  IO.getStdin
  let input  stdin.getLine
  let some f  fifth input | IO.println "Input not long enough"
  IO.println f

However, while now main has no complaints, inexplicably str[6]? also throws an error:

failed to synthesize instance
  GetElem String Nat String (?m.358 str x✝)

which I have no clue how to interpret! If someone could please explain what I can do to make this code do what I want, I would really appreciate it! Thanks :)

Eric Wieser (Apr 04 2024 at 07:43):

In your first example, you want let some fifth := _ not let some fifth ← _; the latter is for when you need to run a monad, but that's not what you have here

Dhruv Bhatia (Apr 04 2024 at 08:01):

Just tried

def main : IO Unit := do
  IO.println "enter a line of text:"
  let stdin  IO.getStdin
  let input  stdin.getLine
  let (some fifth) := input[6]? | IO.println "Input not long enough"
  IO.println fifth

but there's still an error! At input[6]? it says

failed to synthesize instance
  GetElem String Nat ?m.244 ?m.245

which seems like a similar error to my second example above. Any idea what's going on here?

Arthur Adjedj (Apr 04 2024 at 08:07):

The String type doesn't have the necessary type-classes to use the foo[n] notation. You could either use the function input.get? ⟨6⟩, or add an instance for the notation. Such a notation would look something like this:

instance : GetElem String Nat Char (fun xs i => i < xs.1.length) where
  getElem xs i h := xs.data[i]

Dhruv Bhatia (Apr 04 2024 at 08:11):

Wow! I guess I never considered the possibility that this instance didn't already exist! Thanks for the help, this fixed it!

Kim Morrison (Apr 04 2024 at 10:06):

This is a bit sad. Does anyone know an explanation for why there isn't such an instance? Seems like something newcomers will try!

Mario Carneiro (Apr 04 2024 at 10:11):

Two things: (1) I think it should not take character positions, it should be a byte offset like everything else in the string API (this design is I think borrowed from Rust). But: (2) We don't currently have any way of determining whether a byte position is valid. This would need to be implemented in the C code.

Mario Carneiro (Apr 04 2024 at 10:13):

indexing by character position is slow because it requires counting up from the bottom of the string. As such I think it's okay that it has the relatively long spelling s.get (s.nextn 0 i)

Mario Carneiro (Apr 04 2024 at 10:15):

I believe Rust deliberately omits the str[i] indexing syntax though, because this issue with some positions not being valid is a footgun. It's almost never okay to just pick a number and index into the string there, and this is why String.Pos doesn't allow numerals other than 0

Kim Morrison (Apr 04 2024 at 10:26):

I wonder if we could do something crazy like provide a dummy deprecated GetElem instance, so s[37]? always returned ? or something, but in doing so gave a message explaining what one should be doing.

Mario Carneiro (Apr 04 2024 at 10:40):

I would love this as an API designer

Mario Carneiro (Apr 04 2024 at 10:40):

like an erroring version of @[deprecated]

Eric Wieser (Apr 04 2024 at 10:40):

Does @[deprecated] work on instances?

Mario Carneiro (Apr 04 2024 at 10:40):

not really

Mario Carneiro (Apr 04 2024 at 10:41):

the thing that triggers the deprecation check is the ident parser

Mario Carneiro (Apr 04 2024 at 10:41):

so if you didn't write it directly you won't get a deprecation message

Mario Carneiro (Apr 04 2024 at 10:41):

which is kind of a problem when you are trying to use deprecation to find and convert all uses

Mario Carneiro (Apr 04 2024 at 10:42):

we should probably have a backup deprecation linter as an environment linter

Yaël Dillies (Apr 04 2024 at 10:43):

Is this related to deprecation not triggering when the lemma is used through dot notation?

Mario Carneiro (Apr 04 2024 at 10:43):

yes, but I think it does trigger in that case?

Yaël Dillies (Apr 04 2024 at 10:44):

I've encountered a few cases where it didn't this week, but I wouldn't be able to tell you where :thinking:

Mario Carneiro (Apr 04 2024 at 10:44):

just tested, still broken (on lean4 master too)

Eric Wieser (Apr 04 2024 at 10:51):

Can't the deprecation linter inspect the infotree somehow?

Mario Carneiro (Apr 04 2024 at 10:57):

it's not actually a linter, that's the thing

Mario Carneiro (Apr 04 2024 at 10:57):

it's the elab itself that does the check and pretends to be a linter

Mario Carneiro (Apr 04 2024 at 10:59):

I'm not exactly sure why it was implemented this way. Maybe @Sebastian Ullrich knows

Mario Carneiro (Apr 04 2024 at 11:00):

(this is also the source of another bug, mentioned in a TODO in the code: if the identifier is ambiguous and is resolved by the types, it may be too late to take back the deprecation warning if the second attempt is successful and the first attempt is to a deprecated constant)

Mario Carneiro (Apr 04 2024 at 11:03):

@[deprecated] def A.foo := 1
def B.foo := "hello"
open A B

#check (foo : String)
-- B.foo : String
-- `A.foo` has been deprecated

Sebastian Ullrich (Apr 04 2024 at 11:14):

It was probably easier to implement it this way but I wasn't involved

Mario Carneiro (Apr 04 2024 at 11:15):

I was thinking it might predate the linter system

Mario Carneiro (Apr 04 2024 at 11:18):

do you think it would be reasonable to (submit a PR to) convert it to a linter (and maybe add some other features to it in the process)? There were several independent suggestions for improvement just now here and in the other thread

Sebastian Ullrich (Apr 04 2024 at 12:46):

Gathering these in an RFC would be a good first step


Last updated: May 02 2025 at 03:31 UTC