Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support inferred superclass instances #481

Open
dan-zheng opened this issue Jan 22, 2021 · 5 comments
Open

Support inferred superclass instances #481

dan-zheng opened this issue Jan 22, 2021 · 5 comments
Labels
language / concrete syntax String -> AST shovel-ready design is done, just need to build it

Comments

@dan-zheng
Copy link
Collaborator

dan-zheng commented Jan 22, 2021

Motivation

Haskell has fine-grained typeclass hierarchies. Consider:

class Semigroup a where
  (<>) :: a -> a -> a

class Semigroup m => Monoid m where
  mempty :: m

  -- Defining mappend is unnecessary, it copies from Semigroup.
  mappend :: m -> m -> m
  mappend = (<>)

I want to implement Monoid for [a]. To do so, I must provide two instances: one for Monoid, and one for all the superclasses (Semigroup).

instance Semigroup [a] where
  (<>) = (++)

instance Monoid [a] where
  mempty = []

When implementing a typeclass with a long chain of superclasses, it may be nice to define all methods within the instance for the leaf typeclass — instead of writing one instance per typeclass in the chain.

Design

In Dex, we could support inferred superclass instances. Consider the following typeclass hierarchy:

-- Semigroup → Monoid → Group → Abelian
-- Other hierarchies: ... → SemiRing → Ring.

-- Defines "combine" operation.
-- Laws:
-- - Associativity: x <> (y <> z) = (x <> y) <> z
interface Semigroup a
  (<>) : a -> a -> a

-- Defines unit operation.
-- Laws:
-- -  Left identity: munit <> x == x
-- - Right identity: x <> munit == x
interface [Semigroup a] Monoid a
  munit : a

-- Defines inverse operation.
-- Laws:
-- - x <> inverse x == munit
-- - inverse x <> x == munit
interface [Monoid a] Group a
  invert : a -> a

-- Defines commutativity law.
-- Laws:
-- - x <> y == y <> x
interface [Group a] Abelian a

The proposed feature is: we can directly provide an instance for Abelian Float like so, inferring superclass instances:

instance Abelian Float where
  munit = 0.
  (<>) = \x y. x + y
  invert = \x. -x

An extension is to allow explicit declarations of superclass instances if desired, so they can be found in the source code:

-- Tentative syntax: comma-separate explicit superclass instance declarations.
instance Semigroup Float, Monoid Float, Group Float, Abelian Float where
  munit = 0.
  (<>) = \x y. x + y
  invert = \x. -x

Alternatives considered

Don't do it: favor clarity in source code

One downside of inferred superclass instances is that the superclass instances no longer appear in the source code. With instance Abelian Float: we cannot grep for instance Semigroup Float in the source code.

This concern seems mitigated with proper tooling support:

  • In dex web and rendered HTML pages: hover tooltips on the instance declaration could show inferred superclass instance information.
  • In IDEs: jump-to-definition on typeclass methods (from inferred superclass instances) should jump to the correct location. Everything can be traced back to source code (the explicit instance definition) so there are no gaps in coverage.
@cristianurlea
Copy link

I strongly favour more verbose syntax and would discourage implicit behaviour. Hiding the inferred derivation by default could result in very subtle issues for the end-user. It's worth pointing out Deriving Via or, How to Turn Hand-Written Instances into an Anti-pattern as the closest Haskell equivalent.

@duvenaud
Copy link
Contributor

duvenaud commented Jan 22, 2021

Nicely written. Just so I'm sure I understand correctly, should your Abelian Float instance also have invert = \x. -x?

@dan-zheng
Copy link
Collaborator Author

Nice catch, thanks @duvenaud.

@danieldjohnson
Copy link
Collaborator

An interesting property of inferred superclass instances: it allows refining the class hierarchy without breaking user code. In particular you could lift operators from an interface into a new parent interface, and user definitions would remain valid (now defining two instances).

@dougalm
Copy link
Collaborator

dougalm commented Feb 15, 2023

This is a good idea and we should do it. Especially now that our class hierarchies are growing deeper, with the recent addition of the Data class.

Let's implement the first version Dan proposed, where the superclass names are implicit. I like Daniel's argument that it lets you refine the class hierarchy without breaking user code.

As for implementation, I think we can do it entirely within Inference.hs. We currently fail in checkInstanceBody if we come across a superclass dictionary we can't synthesize. Instead, we can just emit a DictHole. Then once we're back in inferTopUDecl, before synthInstanceDef, we can create instances for each superclasses we couldn't find, using the same set of methods and instance binders. Those synthetic instances might require their own superclasses so we'll do this recursively, taking care not to visit the same superclass twice through different paths.

@dougalm dougalm added the shovel-ready design is done, just need to build it label Feb 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language / concrete syntax String -> AST shovel-ready design is done, just need to build it
Projects
None yet
Development

No branches or pull requests

5 participants