Reputation: 4570
I have a set of functions to convert between tuples and lists:
pack2 :: [a] -> (a, a)
pack2 (a:b:_) = (a, b)
unpack2 :: (a, a) -> [a]
unpack2 (a, b) = [a, b]
pack3 :: [a] -> (a, a, a)
pack3 (a:b:c:_) = (a, b, c)
unpack3 :: (a, a, a) -> [a]
unpack3 (a, b, c) = [a, b, c]
-- and so on
I want to generalize it a bit so that the following would be possible:
packN [1, 2, 3, 4] :: (Int, Int) -- => (1, 2)
unpackN (1, 2, 3) -- => [1, 2, 3]
I attempted to address it like this:
class Pack a b where
packN :: a -> b
unpackN :: b -> a
instance Pack [a] (a, a) where
packN = pack2
unpackN = unpack2
instance Pack [a] (a, a, a) where
packN = pack3
unpackN = unpack3
It does compile without any error (MultiParamTypeClasses, FlexibleInstances
are enabled), however, when attempted to use, it fails:
> unpackN (1, 2)
<interactive>:84:1:
Could not deduce (Pack a (t0, t1))
arising from the ambiguity check for ‘it’
from the context (Pack a (t, t2), Num t2, Num t)
bound by the inferred type for ‘it’:
(Pack a (t, t2), Num t2, Num t) => a
at <interactive>:84:1-14
The type variables ‘t0’, ‘t1’ are ambiguous
When checking that ‘it’
has the inferred type ‘forall a t t1.
(Pack a (t, t1), Num t1, Num t) =>
a’
Probable cause: the inferred type is ambiguous
How can I make it work?
Upvotes: 1
Views: 188
Reputation: 30103
unpack
has to somehow infer the return type from the input type, so let's look at ways to do that (pack
always takes [a]
as input, so there's no chance we can infer anything from that).
First, we can declare a functional dependency:
class Unpack tup a | tup -> a where
unpack :: tup -> [a]
Here we promise that we only write instances for Unpack
in such a way that a
is always unambiguously inferable from tup
. Thus, when we write unpack (a, b)
, the return type will be evident from the type of (a, b)
, and all will be well.
instance Unpack (a, a) a where
unpack (a, b) = [a, b]
Second, we can parametrize the class only by the tuple type, and compute the element type from it explicitly, using type families:
class Unpack tup where
type Elem tup
unpack :: tup -> [Elem tup]
instance Unpack (a, a) where
type Elem (a, a) = a
unpack (a, b) = [a, b]
However, there is one more thing to fix. Currently, unpack doesn't work without type annotations if the element type is polymorphic:
unpack (1, 2) -- type error
This is because numerals have type Num a => a
, and (1, 2)
could have type (Int, Float)
too, which is not what we specified in our instance. Our instance Unpack (a, a)
matches only when it is evident that we have the same concrete, monomorphic types in the tuple.
In general, an instance is chosen depending solely on the form of the instance heads (here the instance head is the tup
type in instance Unpack tup
), and only after that does GHC check whether the instance constraints hold. We can use this to our advantage: we can make sure that we have an instance even for the case when the types in the tuple are different, and then use the instance constraints to force the types to be the same nevertheless:
-- I assume we use the functional dependencies version here
instance a ~ b => Unpack (a, b) a where
unpack (a, b) = [a, b]
Now unpack
always works without annotations.
It could be a bit bothersome to write out all the type equality constraints for large tuples:
instance (a ~ b, b ~ c, c ~ d) => Unpack (a, b, c, d) a where
unpack (a, b, c, d) = [a, b, c, d]
Fortunately we can write a shorthand for the equalities using ConstraintKinds
and type families:
{-# LANGUAGE
ConstraintKinds, DataKinds, TypeOperators, TypeFamilies,
MultiParamTypeClasses, FunctionalDependencies, UndecidableInstances #-}
import GHC.Exts
type family AllSame (xs :: [*]) :: Constraint where
AllSame '[] = ()
AllSame '[a] = ()
AllSame (a ': b ': xs) = (a ~ b, AllSame (b ': xs))
...
instance AllSame [a, b, c, d] => Unpack (a, b, c, d) a where
unpack (a, b, c, d) = [a, b, c, d]
The thing to know here is that ConstraintKinds
works by treating the tuple type as conjunction for constraints, and ()
as the empty constraint that is always satisfied. AllSame
is just a type level function that expands to a constraint equivalent to what we wrote out by hand earlier.
Upvotes: 8
Reputation: 52280
Here is a version using type-families (maybe not in an idiomatic way) that solves your problem:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
module Test where
pack2 :: [a] -> (a, a)
pack2 (a:b:_) = (a, b)
unpack2 :: (a, a) -> [a]
unpack2 (a, b) = [a, b]
pack3 :: [a] -> (a, a, a)
pack3 (a:b:c:_) = (a, b, c)
unpack3 :: (a, a, a) -> [a]
unpack3 (a, b, c) = [a, b, c]
class Pack b where
type Element b :: *
packN :: [Element b] -> b
unpackN :: b -> [Element b]
instance Pack (a, a) where
type Element (a,a) = a
packN = pack2
unpackN = unpack2
instance Pack (a, a, a) where
type Element (a,a,a) = a
packN = pack3
unpackN = unpack3
so this will work:
unpackN ((1,2) :: (Int, Int))
> [1,2]
maybe this is a better example (as it does not share the generic numeral flaw ^^ ):
*Test> unpackN ('a','b')
"ab"
*Test> unpackN ('a','b','c')
"abc"
Upvotes: 2