Understanding a rank 2 type alias with a class constraint

痴心易碎 提交于 2019-12-18 02:42:30

问题


I have code that frequently uses functions that look like

foo :: (MyMonad m) => MyType a -> MyOtherType a -> ListT m a

To try to shorten this, I wrote the following type alias:

type FooT m a = (MyMonad m) => ListT m a

GHC asked me to turn on Rank2Types (or RankNTypes), but didn't complain when I used the alias to shorten my code to

foo :: MyType a -> MyOtherType a -> FooT m a

By contrast, when I wrote another type alias

type Bar a b = (Something a, SomethingElse b) => NotAsBar a b

and used it in a negative position

bar :: Bar a b -> InsertTypeHere

GHC loudly yelled at me for being wrong.

I think I have an idea of what's going on, but I'm sure I could get a better grasp from your explanations, so I have two questions:

  • What are the type aliases actually doing/what do they actually mean?
  • Is there a way to get the terseness in both cases?

回答1:


There are essentially three parts to a type signature:

  1. variable declarations (these are usually implicit)
  2. variable constraints
  3. the type signature head

These three elements essentially stack. Type variables must be declared before they can be used, either in constraints or elsewhere, and a class constraint scopes over all uses within the type signature head.

We can rewrite your foo type so the variables are explicitly declared:

foo :: forall m a. (MyMonad m) => MyType a -> MyOtherType a -> ListT m a

The variable declarations are introduced by the forall keyword, and extend to the .. If you don't explicitly introduce them, GHC will automatically scope them at the top level of the declaration. Constraints come next, up to the =>. The rest is the type signature head.

Look at what happens when we try to splice in your type FooT definition:

foo :: forall m a. MyType a -> MyOtherType a -> ( (MyMonad m) => ListT m a )

The type variable m is brought into existence at the top level of foo, but your type alias adds a constraint only within the final value! There are two approaches to fixing it. You can either:

  • move the forall to the end, so m comes into existence later
  • or move the class constraint to the top

Moving the constraint to the top looks like

foo :: forall m a. MyMonad m => MyType a -> MyOtherType a -> ListT m a

GHC's suggestion of enabling RankNTypes does the former (sort of, there's something I'm still missing), resulting in:

foo :: forall a. MyType a -> MyOtherType a -> ( forall m. (MyMonad m) => ListT m a )

This works because m doesn't appear anywhere else, and it's right of the arrow, so these two mean essentially the same thing.

Compare to bar

bar :: (forall a b. (Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere

With the type alias in a negative position, a higher-rank type has a different meaning. Now the first argument to bar must be polymorphic in a and b, with appropriate constraints. This is different from the usual meaning, where bars caller chooses how to instantiate those type variables. It's not

In all likelihood, the best approach is to enable the ConstraintKinds extension, which allows you to create type aliases for constraints.

type BarConstraint a b = (Something a, SomethingElse b)

bar :: BarConstraint a b => NotAsBar a b -> InsertTypeHere

It's not quite as terse as what you hoped for, but much better than writing out long constraints every time.

An alternative would be to change your type alias into a GADT, but that has several other consequences you may not want to bring in. If you're simply hoping to get more terse code, I think ConstraintKinds is the best option.




回答2:


You can think of typeclass constraints essentially as implicit parameters -- i.e. think of

Foo a => b

as

FooDict a -> b

where FooDict a is a dictionary of methods defined in the class Foo. For example, EqDict would be the following record:

data EqDict a = EqDict { equal :: a -> a -> Bool, notEqual :: a -> a -> Bool }

The differences are that there can only be one value of each dictionary at each type (generalize appropriately for MPTCs), and GHC fills in its value for you.

With this in mind, we can come back to your signatures.

type FooT m a = (MyMonad m) => ListT m a
foo :: MyType a -> MyOtherType a -> FooT m a

expands to

foo :: MyType a -> MyOtherType a -> (MyMonad m => ListT m a)

using the dictionary interpretation

foo :: MyType a -> MyOtherType a -> MyMonadDict m -> ListT m a

which is equivalent by reordering of arguments to

foo :: MyMonadDict m -> MyType a -> MyOtherType a -> ListT m a

which is equivalent by the inverse of the dictionary transformation to

foo :: (MyMonad m) => MyType a -> MyOtherType a -> ListT m a

which is what you were looking for.

However, things do not work out that way in your other example.

type Bar a b = (Something a, SomethingElse b) => NotAsBar a b
bar :: Bar a b -> InsertTypeHere

expands to

bar :: ((Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere

These variables are still quantified at the top level (i.e.

bar :: forall a b. ((Something a, SomethingElse b) => NotAsBar a b) -> InsertTypeHere

), since you mentioned them explicitly in bar's signature, but when we do the dictionary transformation

bar :: (SomethingDict a -> SomethingElseDict b -> NotAsBar a b) -> InsertTypeHere

we can see that this is not equivalent to

bar :: SomethingDict a -> SomethingElseDict b -> NotAsBar a b -> InsertTypeHere

which would give rise to what you want.

It's pretty tough to come up with realistic examples in which a typeclass constraint is used at a different place than its point of quantification -- I have never seen it in practice -- so here's an unrealistic one just to show that that's what's happening:

sillyEq :: forall a. ((Eq a => Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (x == y)

Contrast to what happens if we use try to use == when we are not passing an argument to f:

sillyEq' :: forall a. ((Eq a => Bool) -> Bool) -> a -> a -> Bool
sillyEq' f x y = f (x == y) || x == y

we get a No instance for Eq a error.

The (x == y) in sillyEq gets its Eq dict from f; its dictionary form is:

sillyEq :: forall a. ((EqDict a -> Bool) -> Bool) -> a -> a -> Bool
sillyEq f x y = f (\eqdict -> equal eqdict x y)

Stepping back a bit, I think the way you are tersifying here is going to be painful -- I think you're wanting the mere use of something to quantify its context, where its context is defined as the "function signature where it is used". That notion has no simple semantics. You should be able to think of Bar as function on sets: it takes as arguments two sets and returns another. I don't believe there will be such a function for the use you are trying to achieve.

As far as shortening contexts, you may be able to make use of the ConstraintKinds extension which allows you to make constraint synonyms, so at least you could say:

type Bars a = (Something a, SomethingElse a)

to get

bar :: Bars a => Bar a b -> InsertTypeHere

But what you want still may be possible -- your names are not descriptive enough for me to tell. You may want to look into Existential Quantification and Universal Quantification, which are two ways of abstracting over type variables.

Moral of the story: remember that => is just like -> except that those arguments are filled in automatically by the compiler, and make sure that you are trying to define types with well-defined mathematical meanings.



来源:https://stackoverflow.com/questions/14585220/understanding-a-rank-2-type-alias-with-a-class-constraint

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!