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

Don't generate defunctionalization symbols for "helper" families used in class defaults and instance methods #608

Closed
RyanGlScott opened this issue Jun 18, 2024 · 1 comment

Comments

@RyanGlScott
Copy link
Collaborator

Consider this example:

{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeAbstractions #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -ddump-splices #-}
module Foo where

import Data.Singletons.TH

$(promote [d|
  class C a where
    m :: a -> b -> a
    m x _ = x
  |])

Conceptually, this will:

  • Promote the class C to PC with an associated type family named M.

  • M will have a type family default that looks like:

    type M x y = MHelper x y

    Where MHelper is defined to be:

    type MHelper :: a -> b -> a
    type family MHelper x y where
      MHelper x _ = x

What is interesting is that singletons-th defines defunctionalization symbols for MHelper. As a result, compiling the example above will generate a surprising amount of code:

Foo.hs:(13,2)-(17,5): Splicing declarations
    promote
      [d| class C_a7aq a_a7as where
            m_a7ar :: a_a7as -> b_a7at -> a_a7as
            m_a7ar x_a7au _ = x_a7au |]
  ======>
    class C_a7bv a_a7bx where
      m_a7bw :: a_a7bx -> b_a7by -> a_a7bx
      m_a7bw x_a7bz _ = x_a7bz
    type MSym0 :: forall a_a7bx
                         b_a7by. (~>) a_a7bx ((~>) b_a7by a_a7bx)
    data MSym0 :: (~>) a_a7bx ((~>) b_a7by a_a7bx)
      where
        MSym0KindInference :: SameKind (Apply MSym0 arg_a7bC) (MSym1 arg_a7bC) =>
                              MSym0 a6989586621679037421
    type instance Apply MSym0 a6989586621679037421 = MSym1 a6989586621679037421
    instance SuppressUnusedWarnings MSym0 where
      suppressUnusedWarnings = snd ((,) MSym0KindInference ())
    type MSym1 :: forall a_a7bx b_a7by. a_a7bx -> (~>) b_a7by a_a7bx
    data MSym1 (a6989586621679037421 :: a_a7bx) :: (~>) b_a7by a_a7bx
      where
        MSym1KindInference :: SameKind (Apply (MSym1 a6989586621679037421) arg_a7bC) (MSym2 a6989586621679037421 arg_a7bC) =>
                              MSym1 a6989586621679037421 a6989586621679037422
    type instance Apply (MSym1 a6989586621679037421) a6989586621679037422 = M a6989586621679037421 a6989586621679037422
    instance SuppressUnusedWarnings (MSym1 a6989586621679037421) where
      suppressUnusedWarnings = snd ((,) MSym1KindInference ())
    type MSym2 :: forall a_a7bx b_a7by. a_a7bx -> b_a7by -> a_a7bx
    type family MSym2 (a6989586621679037421 :: a_a7bx) (a6989586621679037422 :: b_a7by) :: a_a7bx where
      MSym2 a6989586621679037421 a6989586621679037422 = M a6989586621679037421 a6989586621679037422
    type M_6989586621679037425 :: a_a7bx -> b_a7by -> a_a7bx
    type family M_6989586621679037425 (a_a7bJ :: a_a7bx) (a_a7bK :: b_a7by) :: a_a7bx where
      M_6989586621679037425 x_a7bO _ = x_a7bO
    type M_6989586621679037425Sym0 :: (~>) a_a7bx ((~>) b_a7by a_a7bx)
    data M_6989586621679037425Sym0 :: (~>) a_a7bx ((~>) b_a7by a_a7bx)
      where
        M_6989586621679037425Sym0KindInference :: SameKind (Apply M_6989586621679037425Sym0 arg_a7bL) (M_6989586621679037425Sym1 arg_a7bL) =>
                                                  M_6989586621679037425Sym0 a6989586621679037430
    type instance Apply M_6989586621679037425Sym0 a6989586621679037430 = M_6989586621679037425Sym1 a6989586621679037430
    instance SuppressUnusedWarnings M_6989586621679037425Sym0 where
      suppressUnusedWarnings
        = snd ((,) M_6989586621679037425Sym0KindInference ())
    type M_6989586621679037425Sym1 :: a_a7bx -> (~>) b_a7by a_a7bx
    data M_6989586621679037425Sym1 (a6989586621679037430 :: a_a7bx) :: (~>) b_a7by a_a7bx
      where
        M_6989586621679037425Sym1KindInference :: SameKind (Apply (M_6989586621679037425Sym1 a6989586621679037430) arg_a7bL) (M_6989586621679037425Sym2 a6989586621679037430 arg_a7bL) =>
                                                  M_6989586621679037425Sym1 a6989586621679037430 a6989586621679037431
    type instance Apply (M_6989586621679037425Sym1 a6989586621679037430) a6989586621679037431 = M_6989586621679037425 a6989586621679037430 a6989586621679037431
    instance SuppressUnusedWarnings (M_6989586621679037425Sym1 a6989586621679037430) where
      suppressUnusedWarnings
        = snd ((,) M_6989586621679037425Sym1KindInference ())
    type M_6989586621679037425Sym2 :: a_a7bx -> b_a7by -> a_a7bx
    type family M_6989586621679037425Sym2 (a6989586621679037430 :: a_a7bx) (a6989586621679037431 :: b_a7by) :: a_a7bx where
      M_6989586621679037425Sym2 a6989586621679037430 a6989586621679037431 = M_6989586621679037425 a6989586621679037430 a6989586621679037431
    class PC a_a7bx where
      type family M (arg_a7bA :: a_a7bx) (arg_a7bB :: b_a7by) :: a_a7bx
      type M a_a7bF a_a7bG = Apply (Apply M_6989586621679037425Sym0 a_a7bF) a_a7bG

I claim that these defunctionalization symbols are unnecessary, and we can omit them entirely. I oversimplified things a little bit earlier when I said that the type family default looks like type M x y = MHelper x y. In reality, it looks like this:

type M x y = MHelperSym0 `Apply` x `Apply` y

Where MHelperSym0 is a defunctionalization symbol. The thing is, there's not a good reason to involve defunctionalization symbols here. In the context of class defaults (and instance methods), we know that the helper type family will be fully applied to all of its arguments, so no partial application of defunctionalization symbols is required. We could just as well generate type M x y = MHelper x y and avoid all of the gobs of code that arise due to defunctionalization.

RyanGlScott added a commit that referenced this issue Jun 19, 2024
`singletons-th` generates "helper" type families that contain the definitions
of class method defaults or instance methods. For example, this:

```hs
class C a where
  m :: a -> b -> a
  m x _ = x
```

Will be promoted to this:

```hs
class PC a where
  type M (x :: a) (y :: b) :: a
  type M x y = MHelperSym0 `Apply` x `Apply` y

type MHelper :: a -> b -> a
type family MHelper x y where
  MHelper x _ = x

type MHelperSym0 :: a ~> b ~> a
type MHelperSym1 :: a -> b ~> a
...
```

Generating defunctionalization symbols for `MHelper` is wasteful, however, as
we never really _need_ to partially apply `MHelper`. Instead, we can just
generate this code:

```hs
class PC a where
  type M (x :: a) (y :: b) :: a
  type M x y = MHelper x y
```

This takes advantage of the fact that when we apply `MHelper`, we always fully
apply it to all of its arguments. This means that we can avoid generating
defunctionalization symbols for helper type families altogether, which is a
nice optimization.

This patch implements that idea, fixing #608 in the process.
RyanGlScott added a commit that referenced this issue Jun 22, 2024
…atures

As noted in #608, you don't need to involve defunctionalization when promoting
class method defaults, as we always know that the promoted versions of the
defaults will be fully applied to all their arguments. In fact, there is
another situation in which we always know that a promoted type family will be
fully applied: the return type of a singled type signature. That it, if you
single this definition:

```hs
f :: a -> b -> a
```

You currently get this:

```hs
sF :: forall a b (x :: a) (y :: b).
      Sing x -> Sing y -> Sing (FSym0 `Apply` x `Apply` y)
```

But using `FSym0` in the return type of `sF` is silly, however, as we always
know that it will be fully applied to its arguments (`x` and `y`). As such, it
would be far more direct to instead generate the following:

```hs
sF :: forall a b (x :: a) (y :: b).
      Sing x -> Sing y -> Sing (F x y)
```

No defunctionalization required at all. This doesn't change the behavior of the
singled code at all, but it is more direct and will likely require less time to
typecheck due to fewer type family indirections being involved.

To accomplish this, the `lde_proms` field now maps term-level names to
`LetDecProm`s, where a `LetDecProm` consists of the promoted version of the
name and the local variables that it closes over. In this context, "promoted
version" means `F`, _not_ `FSym0`. Storing the non-defunctionalized name allows
us to fully apply `F` whenever it is convenient to do so, and in situations
where partial application may be required, we can always convert `F` to `FSym0`
before generating the partial application.
RyanGlScott added a commit that referenced this issue Jun 22, 2024
`singletons-th` generates "helper" type families that contain the definitions
of class method defaults or instance methods. For example, this:

```hs
class C a where
  m :: a -> b -> a
  m x _ = x
```

Will be promoted to this:

```hs
class PC a where
  type M (x :: a) (y :: b) :: a
  type M x y = MHelperSym0 `Apply` x `Apply` y

type MHelper :: a -> b -> a
type family MHelper x y where
  MHelper x _ = x

type MHelperSym0 :: a ~> b ~> a
type MHelperSym1 :: a -> b ~> a
...
```

Generating defunctionalization symbols for `MHelper` is wasteful, however, as
we never really _need_ to partially apply `MHelper`. Instead, we can just
generate this code:

```hs
class PC a where
  type M (x :: a) (y :: b) :: a
  type M x y = MHelper x y
```

This takes advantage of the fact that when we apply `MHelper`, we always fully
apply it to all of its arguments. This means that we can avoid generating
defunctionalization symbols for helper type families altogether, which is a
nice optimization.

This patch implements that idea, fixing #608 in the process.
RyanGlScott added a commit that referenced this issue Jun 22, 2024
…atures

As noted in #608, you don't need to involve defunctionalization when promoting
class method defaults, as we always know that the promoted versions of the
defaults will be fully applied to all their arguments. In fact, there is
another situation in which we always know that a promoted type family will be
fully applied: the return type of a singled type signature. That it, if you
single this definition:

```hs
f :: a -> b -> a
```

You currently get this:

```hs
sF :: forall a b (x :: a) (y :: b).
      Sing x -> Sing y -> Sing (FSym0 `Apply` x `Apply` y)
```

But using `FSym0` in the return type of `sF` is silly, however, as we always
know that it will be fully applied to its arguments (`x` and `y`). As such, it
would be far more direct to instead generate the following:

```hs
sF :: forall a b (x :: a) (y :: b).
      Sing x -> Sing y -> Sing (F x y)
```

No defunctionalization required at all. This doesn't change the behavior of the
singled code at all, but it is more direct and will likely require less time to
typecheck due to fewer type family indirections being involved.

To accomplish this, the `lde_proms` field now maps term-level names to
`LetDecProm`s, where a `LetDecProm` consists of the promoted version of the
name and the local variables that it closes over. In this context, "promoted
version" means `F`, _not_ `FSym0`. Storing the non-defunctionalized name allows
us to fully apply `F` whenever it is convenient to do so, and in situations
where partial application may be required, we can always convert `F` to `FSym0`
before generating the partial application.
@RyanGlScott
Copy link
Collaborator Author

Fixed in #609.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant