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
Conversation
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.
I should mention that this was inspired by Polysemy’s implementation of effect handling. Polysemy also provides a default implementation of |
@patrickt: see also #54 for a definition of |
There was a problem hiding this 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 #-} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
src/Control/Effect/Fail.hs
Outdated
hmap _ = coerce | ||
{-# INLINE hmap #-} | ||
deriving stock Functor | ||
deriving anyclass HFunctor |
There was a problem hiding this comment.
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 #-} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🎉
There was a problem hiding this 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!
src/Control/Effect/Carrier.hs
Outdated
@@ -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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
Generally speaking, the majority of effects do not require scoping,
and as a result we encounter often this definition of
HFunctor
: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:
By providing this as a valid default implementation of
hmap
, andenabling
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.