Zulip Chat Archive

Stream: general

Topic: Conditional on type belonging to a type class


DSB (Nov 26 2024 at 17:36):

Hello, folks. I'm trying to learn Lean for functional programming, and I was wondering if the following was possible. Consider the code below:

class MInterface (a : Type) where
  fm : a -> Int

instance :  MInterface Nat where
  fm x := x + 1
instance :  MInterface String where
  fm x := String.length (x) + 1

structure M where
  {T : Type}
  [inst : MInterface T]
  [repr : Repr T]
  val : T

instance : Repr M where
  reprPrec m n := @Repr.reprPrec m.T m.repr m.val n

def x : M := 1
def lx : List M := [1, "ok"]
#eval x -- 1
#eval lx -- [1, "ok"]

This M structure allows me to create lists having distinct types, as long as these types are all instances of some given class(es). All very good. Here is my "problem". In the code above, I've constrained M on [repr : Repr T], which allowed me to make M an instance of Repr.

Yet, I'd want to get rid of the [repr: Repr T] constraint, and instead make M an instance of Repr by providing a function that uses the repr for a given type if it belongs to such class, and otherwise it does something generic (like write the string "no-repr"). For example:

structure MyType where
  _1 : Int
  _2 : Int
instance :  MInterface MyType where
  fm m := m._1 + m._2

def m1 : MyType := MyType.mk 1 2
def l2 : List M := [1, "ok", m1] -- [1, "ok", "no-repr"]
#eval l2

This code fails, since MyType is not Repr, but I've commented what the desired output should be.
An even better output would be using the deriving Repr.

I mean, when I define MyType without the deriving Repr, Lean seems to infer it, since I can still use #eval. I was wondering if I could mimic this behavior.

Edward van de Meent (Nov 26 2024 at 17:44):

i don't think there's a safe way to do that, without doing some elaboration shenanigans.

Yaël Dillies (Nov 26 2024 at 17:46):

What about adding a default low priority Repr instance for all types? Something like

instance (priority := 1) (α : Type*) : Repr α where reprPrec m n := "no-repr"

Edward van de Meent (Nov 26 2024 at 17:47):

i'd recommend you inline the instance in a custom constructor

Edward van de Meent (Nov 26 2024 at 17:48):

otherwise, maybe something like [repr:Repr T := default_repr T] works? i don't know if instance synthesis or default argument takes precedence in this case though...

Alex Meiburg (Nov 26 2024 at 17:58):

structure M where
  {T : Type}
  [inst : MInterface T]
  val : T
  repr : Repr T := by first | infer_instance | exact defaultRepr

instance : Repr M where
  reprPrec m n := @Repr.reprPrec m.T m.repr m.val n

def defaultRepr {T : Type} : Repr T where
  reprPrec _ _ := "no-repr".quote

structure MyType where
  _1 : Int
  _2 : Int
instance :  MInterface MyType where
  fm m := m._1 + m._2

def m1 : MyType := MyType.mk 1 2

def l2 : List M := [M.mk 1, M.mk "ok", M.mk m1] -- [1, "ok", "no-repr"]
#eval l2

essentially does what you want.

Alex Meiburg (Nov 26 2024 at 17:58):

You won't be able to use the ⟨1⟩ notation though, not unless you make your own similar notation.

Kyle Miller (Nov 26 2024 at 18:00):

Edward van de Meent said:

maybe something like [repr:Repr T := default_repr T] works?

This isn't valid syntax, optional parameters only work for default binders (x : T := v)

Edward van de Meent (Nov 26 2024 at 18:01):

right, that makes sense

Edward van de Meent (Nov 26 2024 at 18:01):

it was just a guess

Kyle Miller (Nov 26 2024 at 18:11):

DSB said:

if it belongs to such class

If you're coming from other functional languages, just watch out that typeclasses in Lean aren't constraints — instead they're just a mechanism for automatically synthesizing an implicit argument. There's a not very strong sense of "belonging to a class" (one definition might be "Repr T is nonempty", but then every T belongs to "Repr" since it's trivial to write a "no repr" instance, and another might be "synthesis can find a Repr T instance", but that can change depending on what instances are available). This means that unless you do what Alex suggests, with trying to pin down a Repr instance manually, then from within the Repr M instance there's nothing you can do to ask for the Repr M.T instance.

Kyle Miller (Nov 26 2024 at 18:16):

DSB said:

This M structure allows me to create lists having distinct types

Could you expand on what you're doing? I'm not sure I'm following what's supposed to be distinct from what.

DSB (Nov 26 2024 at 18:28):

Thanks, @Alex Meiburg ! Why the ⟨ ... ⟩ notation becomes unavailable? Also, is there a way to use the deriving Repr instead of the "no-repr"?

DSB (Nov 26 2024 at 18:30):

@Kyle Miller , the idea is that M represents something like types that I can render in the screen. Thus, I wish to have a collection (list) of such types, and apply a single render function over the list.

I think, in summary, the idea is of ad-hoc polymorphisms. I want to create a collection of elements, all that have types with a render function, which can then be used individually.

Alex Meiburg (Nov 26 2024 at 18:30):

That notation sees that M can take two arguments, so it insists on providing both arguments. It doesn't accept the fact that it's got a default value.

Kyle Miller (Nov 26 2024 at 18:31):

(I've been wondering whether it's an oversight that anonymous constructor notation doesn't know about optParams (default values).)

DSB (Nov 26 2024 at 18:31):

Alex Meiburg said:

That notation sees that M can take two arguments, so it insists on providing both arguments. It doesn't accept the fact that it's got a default value.

How can one define his own notation? I see that the M.mk is passed to each element in the list. Can I define a quick notation that maps the M.mk to the elements somehow?

Kyle Miller (Nov 26 2024 at 18:42):

DSB said:

the idea is that M represents something like types that I can render in the screen. Thus, I wish to have a collection (list) of such types, and apply a single render function over the list.

I see, then in that case I think your original solution is best. It's capturing a "closure" of what Lean sees as all the instances, and it's sort of the best you can hope for, beyond perhaps making it easy to have a "no repr" repr in scope when it is needed — that is, Yael's solution is pretty much what you'd be using. Alex's solution is Yael's solution without making the "no repr" repr be a global instance.

Kyle Miller (Nov 26 2024 at 18:43):

I think it's worth thinking about why you have types that don't have the typeclass available design-wise. Can you instead ensure they all implement an instance for the class?

DSB (Nov 26 2024 at 18:49):

In the case of Repr, it is just a convenience. Let me explain the whole idea.
The structure M represents graphical primitive shapes. This means that if I create a type, say, Circle, I then create an instance of M for such Circle by providing a render : Circle -> Drawing. Another primitive type could be Rectangle or Square, etc. The idea is that I have these primitive types, each of which I know how to represent as a drawing in the screen. Think of how SVG renders figures.

Now, imagine I have a complex figure, where I have a list of many of such primitives. Hence, to render the whole list I map the render function to each element in the list (which are all instance of M, and therefore have a valid render).

I then have a function that flattens [Drawing] -> Drawing into a single drawing.
All good. Now, for convenience, it would be nice to have a repr for my list of primitives (i.e. values of List M). Just so one can check what is inside, before rendering. I could force the repr in the primitive types. But this would not be "nice" from a design point of view, because having a Repr is not actually part of the specification.

Kyle Miller (Nov 26 2024 at 18:51):

I think unfortunately that's what you have to live with though, since you're using repr so it's become part of the specification. (One thing to think about: there's no runtime representation for Type, so there's no way to look up a Repr instance at runtime, even using "unsafe" code.)

Alex Meiburg (Nov 26 2024 at 18:51):

In that case you could 'tighten' Yael's approach, to something like

instance (priority := 1) (α : Type*) [Drawable α] : Repr α where reprPrec m n := "no-repr"

(I'm assuming that Drawable is some class that specifies this render: T -> Drawing method.) Now all Drawable's have a default Repr they can use. But this won't leak over to other types.

Kyle Miller (Nov 26 2024 at 18:52):

If you know C++, you can think about M as being a way to associate a vtable to a value, and adding this repr field is how you can do the run-time polymorphism.

Alex Meiburg (Nov 26 2024 at 18:52):

To me, that feels much better than just doing it universal over all types. It's still localized to "your" types you're working with locally, and won't suddenly provide a Repr for e.g. some crazy category theory types.

DSB (Nov 26 2024 at 18:53):

The code by Yael gives me an error...
unexpected token ')'; expected term

The same error appears in yours implementation, @Alex Meiburg .

Kyle Miller (Nov 26 2024 at 18:53):

Just to check, that solution still involves putting a Repr instance in the M type, right @Alex Meiburg? The point of it is to avoid polluting the global Repr instances?

Alex Meiburg (Nov 26 2024 at 18:53):

Yes, exactly

Kyle Miller (Nov 26 2024 at 18:54):

@DSB Yael was using a mathlib extension. Here:

instance (priority := 1) (α : Type _) : Repr α where reprPrec m n := "no-repr"

Yaël Dillies (Nov 26 2024 at 18:56):

(apologies!)

DSB (Nov 26 2024 at 19:03):

Alex Meiburg said:

In that case you could 'tighten' Yael's approach, to something like

instance (priority := 1) (α : Type*) [Drawable α] : Repr α where reprPrec m n := "no-repr"

(I'm assuming that Drawable is some class that specifies this render: T -> Drawing method.) Now all Drawable's have a default Repr they can use. But this won't leak over to other types.

This Drawable would be the M?

Kyle Miller (Nov 26 2024 at 20:25):

@DSB With that instance, the idea would be

structure M where
  {T : Type}
  [inst : Drawable T]
  [repr : Repr T]
  val : T

Since there's a Drawable T instance around, that enables this particular instance.

DSB (Nov 26 2024 at 20:27):

Thank you all for the answers!

DSB (Nov 26 2024 at 20:28):

I guess my last question would be if this "no-repr" could instead be something like the deriving Repr. I mean, instead of a generic string, it somehow inspects the structure and gives something a bit more meaningful.

Kyle Miller (Nov 26 2024 at 20:31):

Are you types not the sorts of things you can do deriving Repr with? Or post-hoc do the deriving instance Repr for YourTypeName with?

Kyle Miller (Nov 26 2024 at 20:34):

Whatever it is, it has to be done at compile time and not at runtime. There's no runtime representation for types, so there is not much to introspect at runtime. (There's just enough structure that the runtime is able to free objects and decrement reference counts when reference counts go to zero, but I don't believe any of that is accessible to Lean programs, and I'm sure this is anything you'd want to see in a repr instance anyway.)


Last updated: May 02 2025 at 03:31 UTC