Zulip Chat Archive

Stream: lean4

Topic: Puzzling type class behavior


Scott N. Walck (Apr 20 2024 at 22:32):

Hi Folks,

I'm trying to make a type class for numbers that can be added,
subtracted, multiplied, divided, and a few other things.

I thought I had succeeded, but the following inverse function is
not behaving as I had expected. I thought that when I use Float
as the particular type, then the division that occurs in the
inverse function would be floating point division. But since the
inverse of ten evaluates to zero, I'm thinking that maybe Lean is
doing Nat division for some reason and only subsequently
converting the result to a Float.

Can someone explain this behavior?

Thanks.

class Number (n : Type) extends Add n, Sub n, Mul n, Div n, Neg n,
  OfNat n 0, OfNat n 1, OfNat n 2, OfNat n 6 where

def inv [Number n] (x : n) : n := (1 : n) / x

instance : Number Float where

def ten : Float := 10

#eval inv ten    -- 0.000000, why?

Kevin Buzzard (Apr 20 2024 at 22:33):

Can you post a #mwe ? Your question is very hard to answer coherently otherwise [edit: thanks for editing the question :-) ]

Timo Carlin-Burns (Apr 20 2024 at 22:33):

(It looks like the beginning got chopped off in a copy-paste)

Timo Carlin-Burns (Apr 20 2024 at 22:43):

Lean has some counterintuitive behavior when you extend multiple structures which have projections with the same name. Here Number extends both OfNat n 0 and OfNat n 1, both of which have a projection ofNat, and only the OfNat n 0 ends up counting. You can see that (1 : n) is actually getting defined as (0 : n) through this example:

class Number (T : Type) extends OfNat T 0, OfNat T 1

instance : Number Float where

def Number.one {T : Type} [Number T] : T := 1

#eval (Number.one : Float) -- 0.000000

Timo Carlin-Burns (Apr 20 2024 at 22:46):

The way mathlib works around this is that they define two classes docs#Zero and docs#One which each have bidirectional instances with OfNat and docs#AddMonoidWithOne extends Zero T and One T instead of OfNat T 0 and OfNat T 1. You could do the same except have classes for Zero, One, Two, and Six.

Scott N. Walck (Apr 20 2024 at 22:50):

Wow. That's very helpful. Thanks.

Eric Wieser (Apr 21 2024 at 11:05):

This feels like a bug to me; it seems like Lean should refuse to merge base classes which have different types

Floris van Doorn (Apr 22 2024 at 12:00):

This also feels counterintuitive to me. But what exactly are you suggesting @Eric Wieser? @OfNat.ofNat T 0 _ and @OfNat.ofNat T 1 _ both have type T, and I'm not quite sure in what way this example is different to merging the mul fields when extending CommSemigroup and Monoid.

Eric Wieser (Apr 22 2024 at 12:01):

In that case the two base classes are both of type Mul

Eric Wieser (Apr 22 2024 at 12:02):

The one place that we might run into trouble here is with the dependently-typed base classes of Module, but I think that's easiest to answer by implementing and seeing what breaks

Eric Wieser (Apr 22 2024 at 12:03):

I guess this would be a breaking change; field unification would no longer be nominal, and would only happen via explicit diamond inheritance

Floris van Doorn (Apr 22 2024 at 12:04):

I expect there are some explicit fields (i.e. not through extend) in our algebraic hierarchy with the same name and type? And those would indeed break under your suggestion... But maybe that isn't too painful...

Eric Wieser (Apr 22 2024 at 12:20):

I guess this rule only needs to apply for data fields, prop fields can always be merged

Timo Carlin-Burns (Apr 22 2024 at 17:25):

So the desired behavior is that extending both OfNat T 0 and OfNat T 1 results in @[instance] fields toOfNat : OfNat T 0 and toOfNat_0 : OfNat T 1?

Timo Carlin-Burns (Apr 22 2024 at 17:26):

Making the field names unique seems a little awkward. I wonder if the new instance naming algorithm could be reused

Kyle Miller (Apr 22 2024 at 18:18):

At least you can always give your own names outside of extend, at the cost of needing to define the projection instances yourself. You do lose default values from pre-existing instances, but you can use autoParams as a hack:

class Number (n : Type) extends Add n, Sub n, Mul n, Div n, Neg n where
  (zero : n := by exact 0)
  (one : n := by exact 1)
  (two : n := by exact 2)
  (six : n := by exact 6)

instance (n : Type) [Number n] : OfNat n 0 := Number.zero
instance (n : Type) [Number n] : OfNat n 1 := Number.one
instance (n : Type) [Number n] : OfNat n 2 := Number.two
instance (n : Type) [Number n] : OfNat n 6 := Number.six

def inv {n : Type} [Number n] (x : n) : n := (1 : n) / x

instance : Number Float where

def ten : Float := 10

#eval inv ten
-- 0.100000

Kyle Miller (Apr 22 2024 at 18:21):

Making the field names unique seems a little awkward. I wonder if the new instance naming algorithm could be reused

Name mangling is an option, but a problem here is that these names need to be used in user code when defining instances (unless there is an expectation that these fields must come from preexisting instances).

Lean 3 had a feature where you could rename fields when you extended.

Kyle Miller (Apr 22 2024 at 18:22):

Oh right, I forgot about this design pattern, where you use encapsulation instead of extension:

class Number (n : Type) extends Add n, Sub n, Mul n, Div n, Neg n where
  [ofNatZero : OfNat n 0]
  [ofNatOne : OfNat n 1]
  [ofNatTwo : OfNat n 2]
  [ofNatSix : OfNat n 6]

instance (n : Type) [Number n] : OfNat n 0 := Number.ofNatZero
instance (n : Type) [Number n] : OfNat n 1 := Number.ofNatOne
instance (n : Type) [Number n] : OfNat n 2 := Number.ofNatTwo
instance (n : Type) [Number n] : OfNat n 6 := Number.ofNatSix

def inv {n : Type} [Number n] (x : n) : n := (1 : n) / x

instance : Number Float where

def ten : Float := 10

#eval inv ten
-- 0.100000

Kyle Miller (Apr 22 2024 at 18:24):

I wonder if the class command could have a way to say that certain fields are supposed to provide instances, to save needing to write these instance commands?

Timo Carlin-Burns (Apr 22 2024 at 18:53):

Is it accurate to say that this is exactly what extend desugars to, at least when the base structures have mutually exclusive fields? As long as we're not losing any special affordances that come from using extend, I think it would be a reasonable design to say that you must use this manual desugaring when you're extending distinct base structures which share a field with the same name. The nice thing about the desugared syntax is that it provides a natural way to specify the projection names.

Timo Carlin-Burns (Apr 22 2024 at 18:57):

Kyle Miller said:

I wonder if the class command could have a way to say that certain fields are supposed to provide instances, to save needing to write these instance commands?

Should we just make it the default that instance implicit class fields get projection instances automatically? I can't think of a reason you wouldn't want that, and if you really didn't, you could always get around it using class inductive

Eric Wieser (Apr 22 2024 at 23:30):

I think attribute [instance] Number.ofNatZero is the currently-idiomatic spelling?

Robert Maxton (Apr 25 2024 at 12:36):

Timo Carlin-Burns said:

Should we just make it the default that instance implicit class fields get projection instances automatically? I can't think of a reason you wouldn't want that, and if you really didn't, you could always get around it using class inductive

If we do get this, docs#CategoryTheory.Bundled would be a great first test subject, using it becomes nontrivially more convenient with an auto-instance for c.str.


Last updated: May 02 2025 at 03:31 UTC