So say I have a class:
class C a where
reduce :: a -> Int
Now I want to pack it up in a data type:
data Signal = forall a. (C a) => Signal [(Double, a)]
Thanks to the existential quantification, I can call C methods on Signals, but Signals don’t expose a type parameter:
reduceSig :: Signal -> [(Double, Int)]
reduceSig (Signal sig) = map (second reduce) sig
Now since C has a number of methods my natural next step is to pull out the ‘reduce’ function so I can substitute any method:
mapsig :: (C a) => (a -> a) -> Signal -> Signal
mapsig f (Signal sig) = Signal (map (second f) sig)
Type error! Could not deduce (a1 ~ a). On further thought, I think what it’s saying is that ‘f’ is a function on some instance of C, but I can’t guarantee it’s the same instance of C as in the Signals, because the type parameters are concealed! I wanted it, I got it.
So does this mean it’s impossible to generalize reduceSig? I can live with this, but I’m so used to freely factoring out functions in haskell it feels strange to be obliged to write the boilerplate. On the other hand, I can’t think of any way to express that a type is equal to the type inside of Signal, short of giving Signal a type parameter.
What you need to express is that
f, likereduceused inreduceSig, can be applied to any type that is an instance ofC, as opposed to the current type, wherefworks on a single type that is an instance ofC. This can be done like so:You’ll need the
RankNTypesextension, as you often do when using existential types; note that the implementation ofmapsigis the same, the type has just been generalised.Basically, with this type,
mapsiggets to decide which a the function is called on; with your previous type, the caller ofmapsiggets to decide that, which doesn’t work, because onlymapsigknows the correct a, i.e. the one inside theSignal.However,
mapsig reducedoes not work, for the obvious reason thatreduce :: (C a) => a -> Int, and you don’t know that a is Int! You need to givemapsiga more general type (with the same implementation):i.e.,
fis a function taking any type that is an instance ofC, and producing a type that is an instance ofC(that type is fixed at the time of themapsigcall and chosen by the caller; i.e. while the valuemapsig fcan be called on any Signal, it will always produce a Signal with the same a as a result (not that you can inspect this from outside).)Existentials and rank-N types are very tricky indeed, so this might take a bit of time to digest. 🙂
As an addendum, it’s worth pointing out that if all the functions in
Clook likea -> rfor some r, then you would be better off creating a record instead, i.e. turninginto
These two Signal types are actually equivalent! The benefits of the former solution only appear when you have other data types that use
Cwithout existentially quantifying it, so that you can have code that uses special knowledge and operations of the specific instance ofCit’s using. If your primary use-cases of this class are through existential quantification, you probably don’t want it in the first place. But I don’t know what your program looks like 🙂