Zulip Chat Archive
Stream: lean4
Topic: Associated types in classes
Tage Johansson (Apr 17 2023 at 09:50):
I wonder if there is any way to write Rust like associated types in Lean classes?
Imagine I have a class for monadic file system operations MonadFS
and a class for file read operations MonadFileRead
.
Then I could write the file system class like this:
class MonadFS (m : Type → Type _) where
-- Some file system related methods like:
ls : (dir : FilePath := ".") → m (Array IO.FS.DirEntry)
...
-- Type of monad for reading files.
mf : Type → Type _
monadInst : Monad mf
readInst : MonadFileRead mf
-- Perform file reading on a specific file.
readFile : FilePath → mf α → m α
The problem is that I want Monad m
and MonadFileRead m
to be instances and not explicit members of the class. The user of the class should not need to refer to monadInst
and readInst
when using the mf
monad.
I could make mf
as an outParam, and it works, but the class head gets really large and I will have to repeat it in every subclass of MonadFS
.
The problem is how to specify that mf
should have some instances. In Rust like syntax an associated type could be written:
type MF: Monad + MonadFileRead;
But I don't know if this is possible in Lean?
Kyle Miller (Apr 17 2023 at 10:32):
I haven't run this, but in principle I believe this might implement what you're asking:
class MonadFS (m : Type → Type _) where
-- Some file system related methods like:
ls : (dir : FilePath := ".") → m (Array IO.FS.DirEntry)
...
-- Type of monad for reading files.
mf : Type → Type _
[monadInst : Monad mf]
[readInst : MonadFileRead mf]
-- Perform file reading on a specific file.
readFile : FilePath → mf α → m α
attribute [instance] MonadFS.monadInst MonadFS.readInst
The square brackets in the MonadFS
declaration may or may not be correct; what these do is make it so that when you construct a MonadFS
then it will try to use typeclass inference to fill in monadInst
and readInst
automatically given mf
.
Setting the instance
attributes makes it so that you can use MonadFS.mf m
as a monad, for example. (Assuming I got the implicit/explicit arguments correct here of the top of my head.)
Eric Wieser (Apr 17 2023 at 10:43):
I think outParam
is probably the answer here in general
Eric Wieser (Apr 17 2023 at 10:44):
class MonadFS (m : Type → Type _) (mf : outParam $ (Type → Type _)) [Monad mf] [MonadFileRead mf] where
-- Some file system related methods like:
ls : (dir : FilePath := ".") → m (Array IO.FS.DirEntry)
...
-- Perform file reading on a specific file.
readFile : FilePath → mf α → m α
Eric Wieser (Apr 17 2023 at 10:45):
Then the caller just writes [Monad mf] [MonadFileRead mf] [MonadFS m mf]
if they're writing generic code
Tage Johansson (Apr 17 2023 at 10:54):
Miller's solution works. The only annoying thing is that I have to write MonadFS.mf m
instead of m.mf
.
def foo [MonadFS m] : MonadFileRead (m.mf) := inferInstance
Kyle Miller (Apr 17 2023 at 10:59):
Eric Wieser said:
Then the caller just writes
[Monad mf] [MonadFileRead mf] [MonadFS m mf]
if they're writing generic code
Sweeping tradeoffs under the rug using the word "just" makes lots of things easy :smile: Tage was asking how to avoid that ("I could make mf as an outParam, and it works, but the class head gets really large and I will have to repeat it in every subclass of MonadFS.") Outparams have the following nice properties: they make it more likely that mf
has the canonical instances (having a monadInst
field allows users to override the instance), they make it so one can refer to whatever mf
is directly (when it's a field, you generally have to refer to MonadFS.mf m
otherwise typeclass inference might not work), and subclass diamonds don't have any special considerations at all since the class doesn't actually hold a copy of the instances. I think for both it's fairly straightforward to add additional typeclass constraints to mf
in either subclasses or by users of MonadFS
.
Eric Wieser (Apr 17 2023 at 10:59):
Apologies, I totally missed the outParam sentence :(
Kyle Miller (Apr 17 2023 at 11:00):
Still, outparams are a very strong alternative, with the downside of being rather verbose.
Kyle Miller (Apr 17 2023 at 11:01):
@Tage Johansson With the way name resolution works, m.mf
can't work because it doesn't know that it should look at MonadFS
to resolve the mf
function. m
is merely a value that has an associated MonadFS
instance. The name resolver would have to go out of its way to see that there's an instance involving m
that has an mf
defined.
I think I remember some discussions about how to eventually support this though. There are at least a few places in mathlib where this feature would really help.
Tage Johansson (Apr 17 2023 at 11:02):
If I export mf
I can write m |> mf
though, so I think that's very acceptable.
Thanks for the help.
Tage Johansson (Apr 17 2023 at 11:12):
I don't think this "square-bracket"-syntax in classes/structures and the instance attribute thing is documented anywhere though. At least I couldn't find it.
Mario Carneiro (Apr 17 2023 at 11:33):
it should surely have some documentation, typeclasses are a rather core part of lean
Mario Carneiro (Apr 17 2023 at 11:34):
the manual has https://leanprover.github.io/lean4/doc/typeclass.html, although it's not particularly fleshed out
Mario Carneiro (Apr 17 2023 at 11:35):
I guess your point was about using square brackets in class declarations; this falls out of the fact that class declarations consist of binders and [inst : MyClass A]
is a binder
Alex Keizer (Apr 17 2023 at 11:51):
Mario Carneiro said:
[..] class declarations consist of binders [..]
I think it's this point that is not super clear, especially when writing the binders (as is idiomatic) like
structure Point (α : Type u) where
x : α
y : α
I know I didn't make the connection between x : α
syntax and binder syntax until I saw a structure defining its field with bracketed binders, like (x : α)
. Once that connection is made, trying out implicit and instance binders becomes more natural.
Eric Wieser (Apr 17 2023 at 12:50):
I think the key realization is probably that
structure Point (α : Type u) where
x : α
y : α
is roughly short for
inductive Point (α : Type u)
| mk (x : α) (y : α)
where now not only are the binders visible, but so is the function Point.mk
that they're being used for
Last updated: Dec 20 2023 at 11:08 UTC