Zulip Chat Archive
Stream: lean4
Topic: Monad `Of` classes
Mac (Jun 14 2021 at 03:41):
Many of the monad extensions classes (ex. MonadReader
, MonadState
, MonadExcept
) have Of
variants (ex. MonadReaderOf
, MonadStateOf
, MonadExceptOf
). The non-Of
variants are outParam
in their respective value type (ex. ρ
, σ
, ε
) whereas the Of
variants are not. For example:
class MonadReaderOf (ρ : Type u) (m : Type u → Type v) where
read : m ρ
class MonadReader (ρ : outParam (Type u)) (m : Type u → Type v) where
read : m ρ
This means, theoretically, there can be multiple instances of the Of
class that are disambiguated by their value. However, the instances of non-Of
variant are currently inferred from the instances of their Of
variant.
instance (ρ : Type u) (m : Type u → Type v) [MonadReaderOf ρ m] : MonadReader ρ m where
read := readThe ρ
This inhibits one from defining multiple instances of the Of
variant (ex. MonadReaderOf
) because this will create overlapping instances of the non-Of
variant (ex. MonadReader
) which clash in their outParam
(ex. ρ
).
I am curious why definitions like the following were not used instead:
@[defaultInstance]
instance (ρ : Type u) (m : Type u → Type v) [MonadReader ρ m] : MonadReaderOf ρ m where
read := read
This would allow there to be other instances of the Of
variant without clashing instances of the non-Of
variant while still defaulting the Of
variant to the one defined (if any) non-Of
variant.
I imagine there might be other considerations that I may be missing and would thus be interesting in learning what, if any, they are.
Sebastian Ullrich (Jun 14 2021 at 07:21):
Solution: just don't be afraid of overlapping instances :) . Mathlib users have long learned how to cope with them, and Lean explicitly supports you in it with support for instance priorities etc. It's not super elegant, but perfectly manageable in practice afaict.
Mario Carneiro (Jun 14 2021 at 07:22):
I also don't really understand the motivation behind these classes. Which should we implement, and how should generic lemmas look? What are the use cases for the Of version?
Sebastian Ullrich (Jun 14 2021 at 07:24):
The use case is accessing different instances of the same effect in a stack. https://github.com/leanprover/lean4/blob/27d52a352fad12ef04026ebe4becbb4f30bac645/src/Lean/Elab/Term.lean#L298
Sebastian Ullrich (Jun 14 2021 at 07:26):
For a specific, new monad you should implement the Of
version only. Since this instance will be "after" the transitive Of
instance, the non-Of instance will prefer it, i.e. access the outermost effect by default.
Sebastian Ullrich (Jun 14 2021 at 07:28):
I imagine lemmas as well should be written for the Of version since that is the more general one, then possibly copied to the non-Of version.
Mario Carneiro (Jun 14 2021 at 07:30):
Heh, I mean lemma and generic (higher order) program interchangeably :sweat_smile:
Mario Carneiro (Jun 14 2021 at 07:31):
I don't know how actually suitable for proving the monad stack things are. It's pretty rare to have something interesting to prove about them
Mac (Jun 14 2021 at 10:12):
Sebastian Ullrich said:
Solution: just don't be afraid of overlapping instances :) . Mathlib users have long learned how to cope with them, and Lean explicitly supports you in it with support for instance priorities etc. It's not super elegant, but perfectly manageable in practice afaict.
The problem here is not overlapping instances. They are fine and I like them. The problem is that the non-Of
don't support overlapping instances because of the outParam
. If you define a new overlapping Of
instance it will hide the current non-Of
variant and prevent its usage (without explicit providing the instance of the class).
Sebastian Ullrich (Jun 14 2021 at 10:17):
It's hard to follow your description without an example. If you want to declare a new Of
instance but not make it the default one for the non-Of
instance, lower its priority.
Mac (Jun 14 2021 at 10:25):
I was working on an #mwe example when you posted that. Here it is:
universe u
instance {α β : Type u} : MonadStateOf α (StateM (α × β)) where
get := do (<- get).1
set a := modify $ Prod.map.{u,u,u,u} (fun _ => a) id
modifyGet f := modifyGet fun st => f st.1 |>.map id
fun a => st.map (fun _ => a) id
def getState : StateM (String × Nat) (String × Nat) :=
get -- Errors: failed to synthesize instance `MonadState (String × Nat) (StateM (String × Nat))`
Thus, with the new instance, I can no longer get
the original state. This would not happen in the alternative formulation proposed above.
Mac (Jun 14 2021 at 10:26):
Also do note that the above example is rather trivial, but I did encounter this same issue in a more complex real world while working on coding the build system.
Mac (Jun 14 2021 at 10:27):
As a side note, I am also confused as to why providing explicit universes to Prod.map
is necessary.
Sebastian Ullrich (Jun 14 2021 at 12:12):
I don't think your proposal would work with the transitive [MonadLift]
Of
instances, so please use instance (priority := low)
Sebastian Ullrich (Jun 14 2021 at 12:12):
I don't know about the universe issue
Mac (Jun 14 2021 at 12:36):
Sebastian Ullrich said:
I don't think your proposal would work with the transitive
[MonadLift]
Of
instances, so please useinstance (priority := low)
Oh, by bad, didn't know you could specify instance priorities like that. Cool! I am curious about the MonadLift
concern. If possible, could you elaborate what you mean (i.e., what the issue is)?
Sebastian Ullrich (Jun 14 2021 at 12:56):
No, I'm afraid I can't elaborate on every posted proposal to show why I don't think it will work. The burden would be on you to show that you have considered all uses (or at least the ones pointed out) of the current design and how to do them in the new one. Or neither one of us does it and we accept the status quo :) .
Mac (Jun 14 2021 at 13:39):
That's understandable.
Last updated: Dec 20 2023 at 11:08 UTC