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

Proposal: allow automatic derivation of HFunctor instances. #170

Merged
merged 5 commits into from May 7, 2019

Conversation

patrickt
Copy link
Collaborator

@patrickt patrickt commented May 6, 2019

Generally speaking, the majority of effects do not require scoping,
and as a result we encounter often this definition of HFunctor:

instance HFunctor MyEffect where
  hmap _ = coerce

This definition is valid as long as there are no occurrences of a
higher-order (i.e. scoped) variable in the effect. The Coercible
typeclass allows us to express this in the type system:

default hmap :: Coercible (h m a) (h n a)
             => (forall x . m x -> n x)
             -> (h m a -> h n a)
hmap _ = coerce

By providing this as a valid default implementation of hmap, and
enabling DeriveAnyClass/DerivingStrategies in effect definitions,
we save cruft and boilerplate without any impact to backwards
compatibility or performance. DerivingStrategies dates to GHC 8.2,
so all targeted GHC’s will be fine with this.

Fixes #54.

Generally speaking, the majority of effects do not require scoping,
and as a result we encounter often this definition of `HFunctor`:

```haskell
instance HFunctor MyEffect where
  hmap _ = coerce
```

This definition is valid as long as there are no occurrences of a
higher-order (i.e. scoped) variable in the effect. The `Coercible`
typeclass allows us to express this in the type system:

```haskell
default hmap :: Coercible (h m a) (h n a)
             => (forall x . m x -> n x)
             -> (h m a -> h n a)
hmap _ = coerce
```

By providing this as a valid default implementation of `hmap`, and
enabling `DeriveAnyClass`/`DerivingStrategies` in effect definitions,
we save cruft and boilerplate without any impact to backwards
compatibility or performance.
@patrickt patrickt requested a review from robrix May 6, 2019 19:22
@patrickt
Copy link
Collaborator Author

patrickt commented May 6, 2019

I should mention that this was inspired by Polysemy’s implementation of effect handling. Polysemy also provides a default implementation of handle, but said implementation seems less useful to me, as only one of our effects (Fail) admits a definition of handle using coerce.

@robrix
Copy link
Contributor

robrix commented May 7, 2019

I should mention that this was inspired by Polysemy’s implementation of effect handling. Polysemy also provides a default implementation of handle, but said implementation seems less useful to me, as only one of our effects (Fail) admits a definition of handle using coerce.

@patrickt: see also #54 for a definition of handle for first-order effects.

Copy link
Contributor

@robrix robrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! I think we should try for Effect too while we’re at it (cf #54).

=> (forall x . m x -> n x)
-> (h m a -> h n a)
hmap _ = coerce
{-# INLINE hmap #-}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do the error messages look like when you try to use the default definition with a higher-order effect?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be worse, but it could be better:

src/Control/Effect/Writer.hs:28:1: error:
    • Couldn't match representation of type ‘m’ with that of ‘n’
        arising from a use of ‘Control.Effect.Carrier.$dmhmap’
      ‘m’ is a rigid type variable bound by
        the type signature for:
          hmap :: forall (m :: * -> *) (n :: * -> *) a.
                  (forall x. m x -> n x) -> Writer w m a -> Writer w n a
        at src/Control/Effect/Writer.hs:28:1-37
      ‘n’ is a rigid type variable bound by
        the type signature for:
          hmap :: forall (m :: * -> *) (n :: * -> *) a.
                  (forall x. m x -> n x) -> Writer w m a -> Writer w n a
        at src/Control/Effect/Writer.hs:28:1-37
    • In the expression: Control.Effect.Carrier.$dmhmap @(Writer w)

hmap _ = coerce
{-# INLINE hmap #-}
deriving stock Functor
deriving anyclass HFunctor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could also have done instance HFunctor Fail w/o a where clause, but I must admit I like the DerivingStrategies here.


instance HFunctor Fail where
hmap _ = coerce
{-# INLINE hmap #-}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we lose inlining with the default definition? Or does the INLINE pragma on the default definition apply to each derived instance?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The INLINE pragma applies to each derived instance 🎉

Copy link
Contributor

@robrix robrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the one more note!

@@ -19,9 +19,9 @@ class HFunctor h where
{-# INLINE fmap' #-}

-- | Higher-order functor map of a natural transformation over higher-order positions within the effect.
-- A definition for 'hmap' over first-order effects can be derived with the @-XDeriveAnyClass@ extension.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don’t actually have to use -XDeriveAnyClass; you can also do instance HFunctor MyEffect w/o a where clause. Might be worth noting that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call!

@patrickt patrickt merged commit a9605e9 into master May 7, 2019
@patrickt patrickt deleted the default-impls branch May 7, 2019 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

First-order effects admit an automatic definition of handle
2 participants