What can a family type do, that classes with several parameters and functional dependencies cannot

I played with TypeFamilies , FunctionalDependencies and MultiParamTypeClasses . And it seems to me that TypeFamilies does not add any specific features compared to the other two. (But not vice versa). But I know that type families really like it, so I feel like something is missing:

an β€œopen” relationship between types, such as a conversion function, which is not possible with TypeFamilies . Done using MultiParamTypeClasses :

 class Convert ab where convert :: a -> b instance Convert Foo Bar where convert = foo2Bar instance Convert Foo Baz where convert = foo2Baz instance Convert Bar Baz where convert = bar2Baz 

Surprising communication between types, such as a type of safe type pseudo-duck print engine that would normally run with a standard type. Done using MultiParamTypeClasses and FunctionalDependencies :

 class HasLength ab | a -> b where getLength :: a -> b instance HasLength [a] Int where getLength = length instance HasLength (Set a) Int where getLength = S.size instance HasLength Event DateDiff where getLength = dateDiff (start event) (end event) 

A bijective relationship between types, for example, for an unboxed container that can be executed through TypeFamilies with a data family, although then you need to declare a new data type for each contained type, for example, with newtype , Either this, or with a family of injective types, which , it seems to me, not available until GHC 8. Done using MultiParamTypeClasses and FunctionalDependencies :

 class Unboxed ab | a -> b, b -> a where toList :: a -> [b] fromList :: [b] -> a instance Unboxed FooVector Foo where toList = fooVector2List fromList = list2FooVector instance Unboxed BarVector Bar where toList = barVector2List fromList = list2BarVector 

And finally, the surjective relationship between the two types and the third type, such as the python2 or java style split function, which can be done with TypeFamilies , also using MultiParamTypeClasses . Done using MultiParamTypeClasses and FunctionalDependencies :

 class Divide abc | ab -> c where divide :: a -> b -> c instance Divide Int Int Int where divide = div instance Divide Int Double Double where divide = (/) . fromIntegral instance Divide Double Int Double where divide = (. fromIntegral) . (/) instance Divide Double Double Double where divide = (/) 

Another thing I should add is that it seems that FunctionalDependencies and MultiParamTypeClasses also quite a bit compressed (for the examples above anyway), since you only need to write the type once, t the type name of the dummy type should appear which you must enter for each instance, for example using TypeFamilies :

 instance FooBar LongTypeName LongerTypeName where FooBarResult LongTypeName LongerTypeName = LongestTypeName fooBar = someFunction 

vs

 instance FooBar LongTypeName LongerTypeName LongestTypeName where fooBar = someFunction 

Therefore, if I am not convinced of the other, it really seems that I simply should not worry about TypeFamilies and use exclusively FunctionalDependencies and MultiParamTypeClasses . Since, as far as I can tell, this will make my code shorter, more consistent (one smaller extension to take care of), and also give me more flexibility, e.g. with open-type relationships or bijective relationships (maybe the latter is a GHC 8 solver) .

+5
source share
2 answers

Here's an example of where TypeFamilies really shines compared to MultiParamClasses with FunctionalDependencies . In fact, I urge you to come up with an equivalent MultiParamClasses solution, even one that uses FlexibleInstances , OverlappingInstance , etc.

Consider the problem of type level substitution (I came across a specific version of this in Quipper in QData.hs ). Essentially, you want to recursively replace one type with another. For example, I want to be able to

  • replace Int with Bool in Either [Int] String and get Either [Bool] String ,
  • replace [Int] with Bool in Either [Int] String and get Either Bool String ,
  • replace [Int] with [Bool] in Either [Int] String and get Either [Bool] String .

In general, I want the usual concept of level substitution. With a closed type of the family, I can do this for any types (although I need an additional line for each constructor of a higher type - I settled on * -> * -> * -> * -> * ).

 {-# LANGUAGE TypeFamilies #-} -- Subsitute type `x` for type `y` in type `a` type family Substitute xya where Substitute xyx = y Substitute xy (kabcd) = k (Substitute xya) (Substitute xyb) (Substitute xyc) (Substitute xyd) Substitute xy (kabc) = k (Substitute xya) (Substitute xyb) (Substitute xyc) Substitute xy (kab) = k (Substitute xya) (Substitute xyb) Substitute xy (ka) = k (Substitute xya) Substitute xya = a 

And trying in ghci , I get the desired result:

 > :t undefined :: Substitute Int Bool (Either [Int] String) undefined :: Either [Bool] [Char] > :t undefined :: Substitute [Int] Bool (Either [Int] String) undefined :: Either Bool [Char] > :t undefined :: Substitute [Int] [Bool] (Either [Int] String) undefined :: Either [Bool] [Char] 

With that said, maybe you should ask yourself why I use MultiParamClasses and not TypeFamilies . From the above examples, everything except Convert is translated into family types (although you will need an extra line for the instance to declare type ).

And again, for Convert , I'm not sure if it is a good idea to define such a thing. A natural extension to Convert would be cases like

 instance (Convert ab, Convert bc) => Convert ac where convert = convert . convert instance Convert aa where convert = id 

which are also unsolvable for the GHC because they are elegant to record ...

To be clear, I'm not saying that using MultiParamClasses not used, just when possible you should use TypeFamilies - they allow you to think about type-level functions, not just relationships.

This old HaskellWiki page does an OK job to compare the two .

EDIT

Somewhat more contrasting and story I came across augustss blog

Family types grew out of the need to have type classes with related types. The latter is not strictly necessary, since it can be emulated using classes with several parameters, but this gives a much nicer notation in many cases. The same is true for type families; they can also be emulated by classes with several parameters. But MPTC gives a very logical programming style that performs type calculations; whereas the type of the family (which are only type functions that can match the pattern as arguments) is like functional programming.

Using closed type families adds extra strength that cannot be achieved by class classes. to get the same power from class classes that we will need to add a closed class type. That would be very helpful; this is what the chain gives you.

+3
source

Functional dependencies only affect the process of resolving constraints, while type families introduce the concept of non-syntactic equality of types, presented in the intermediate form of the GHC by coercion. This means that type families interact better with GADT. See this question for a canonical example of how functional dependencies fail here.

+1
source

All Articles