I have played around with TypeFamilies
, FunctionalDependencies
, and MultiParamTypeClasses
. And it seems to me as though TypeFamilies
doesn't add any concrete functionality over the other two. (But not vice versa). But I know type families are pretty well liked so I feel like I am missing something:
"open" relation between types, such as a conversion function, which does not seem possible with TypeFamilies
. Done with MultiParamTypeClasses
:
class Convert a b 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
Surjective relation between types, such as a sort of type safe pseudo-duck typing mechanism, that would normally be done with a standard type family. Done with MultiParamTypeClasses
and FunctionalDependencies
:
class HasLength a b | 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)
Bijective relation between types, such as for an unboxed container, which could be done through TypeFamilies
with a data family, although then you have to declare a new data type for every contained type, such as with a newtype
. Either that or with an injective type family, which I think is not available prior to GHC 8. Done with MultiParamTypeClasses
and FunctionalDependencies
:
class Unboxed a b | 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 lastly a surjective relations between two types and a third type, such as python2 or java style division function, which can be done with TypeFamilies
by also using MultiParamTypeClasses
. Done with MultiParamTypeClasses
and FunctionalDependencies
:
class Divide a b c | a b -> 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 = (/)
One other thing I should also add is that it seems like FunctionalDependencies
and MultiParamTypeClasses
are also quite a bit more concise (for the examples above anyway) as you only have to write the type once, and you don't have to come up with a dummy type name which you then have to type for every instance like you do with TypeFamilies
:
instance FooBar LongTypeName LongerTypeName where
FooBarResult LongTypeName LongerTypeName = LongestTypeName
fooBar = someFunction
vs:
instance FooBar LongTypeName LongerTypeName LongestTypeName where
fooBar = someFunction
So unless I am convinced otherwise it really seems like I should just not bother with TypeFamilies
and use solely FunctionalDependencies
and MultiParamTypeClasses
. Because as far as I can tell it will make my code more concise, more consistent (one less extension to care about), and will also give me more flexibility such as with open type relationships or bijective relations (potentially the latter is solver by GHC 8).
type F a = G (H (G a) (G a))
expressed through FunDeps requires several constraints, involving a few auxiliary type variables. When I read such constraints, I found myself to try to express them in a functional form. Perhaps this is because I'm more used to read functional code than prolog. – BailorFunctionalDependencies
orMultiParamTypeClasses
or their interaction? Or is it something more fundamental? I am really hoping for the former as it would be nice to not have to use a different and seemingly more verbose syntax half the time for the same thing just for performance reasons. – Lsdtype F a
ortype F a :: *
with no=
or I seetype F G = ...
with a concrete type immediately afterF
, why do you have a type variable afterF
AND an=
. I am somewhat new to this stuff. – Lsdtype
outside ofTypeFamilies
which seems like the only way to make your example work, then I don't see how anything is different betweenTypeFamilies
andFunctionalDependencies
, if there is any difference would it be alleviated byTypeSynonymInstances
? – LsdG,H
suitably defined. Then you can definetype family F a :: * ; type instance F a = G (H (G a) (G Bool))
-- or you could even do that through a type synonym (in this simple setting). Now, assume thatG,H
are instead typeclasses with fundeps:class G a b | a -> b ; instance ...
. How to achieve the same compositionF
does? You indeed can, but it is not very convienient nor readable, at least to me. – Bailorforall a. a
is the most polymorphic type possible, and you have already given an install forforall a. a
. At that point why not just usetype F a = G (H (G a) (G Bool))
and use it directly whenever needed? – Lsdtype F a = G (H (G a) (G Bool))
ifG,H
are families, but not if they are classes with fundeps. – BailorTypeFamilies
are more "functional" and thus more nestable / composable / flexible etc. – LsdHList
(existentially quantified). Addendum to the challenge: make sure you can convert it back. Now it may be possible to do this without type families, but I foundtype family Replicate (n :: Nat) a :: [*]
very helpful. – Bathulda