When writing an instance of a type class in Haskell, there are two things that can guide us to the correct implementation: types and type class laws.
What does the type say?
One remarkable property of the type system used in Haskell (and some other functional languages as well) is that given a sufficiently polymorphic function
type, there are surprisingly few ways it can be implemented. The intuition is
that since a polymorphic function knows nothing about the types it operates on,
there are no interesting operations it can apply to values of these types.
The simplest example is a function of type a -> a. It must produce a value
of type a as its output; how can it come up with one? Since Haskell doesn't
provide a way to conjure a value of arbitrary type out of thin air1,
the function must use its input value of type a. What can it do with it? Since
it's a value of an arbitrary type, the answer is "nothing" - it can only return
it as its result. Thus, the only reasonable function of type a -> a is the
identity function. A more thorough explanation of this idea and further examples can be found in a famous paper by P. Wadler.
Let's go back to the example at hand. The types of pure and <*> in the
Applicative instance for MyTriple are
pure :: a -> MyTriple a
(<*>) :: MyTriple (a -> b) <*> MyTriple a -> MyTriple b
In pure, to construct MyTriple a, we need a triple of values of type a.
Since we are given one value of type a and we have no way of producing any
other (just like in the a -> a function), the only way to implement it is as
pure x = MyTriple (x, x, x)
Operator <*> is more interesting. In this case:
MyTriple (f, g, h) <*> MyTriple (x, y, z) = ...
-- f, g, h :: a -> b
-- x, y, z :: a
we are given three functions of type a -> b and three values of type a.
To construct MyTriple b, we need three values of type b. The only way
to get our hands on the value of type b is to apply one of f, g, h to
one of x, y, z. There is no unique way to do this; we can pair up
the functions and their arguments in an arbitrary fashion.
It follows that all the possible implementations of <*> for MyTriple are of the form
MyTriple (f1, f2, f3) <*> MyTriple (x1, x1, x3) = MyTriple (fi xp, fj xq, fk xr)
for some values of i, j, k and p, q, r between 1 and 3.
From the point of view of the type system, all these are correct.
In general, there may be multiple valid instances of applicative functor for a given type (e.g. list has two: the regular one and ZipList),
so it's not necessarily a problem. That being said, there is one more thing we
need to consider.
Applicative functor laws
The applicative functor laws are the following rules:
- Identity:
pure id <*> v = v
- Composition:
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
- Homomorphism:
pure f <*> pure x = pure (f x)
- Interchange:
u <*> pure y = pure ($y) <*> u
A valid instance of the applicative functor must satisfy all these equations.
Note that it is not enforced by the compiler - this responsibility lies with the programmer.
At this point, we need to figure out what additional constraints do the applicative functor laws put on the implementation of operator <*> for
MyTriple.
Let's start with the Identity. Using our definitions of pure and <*>, the
identity law requires that
MyTriple (x1, x1, x3) = MyTriple (id xp, id xq, id xr) = MyTriple (xp, xq, xr)
The only way for this to hold for all possible values of x's is
if p = 1, q = 2 and r = 3. This narrows down the set of possible definitions
of <*> to
MyTriple (f1, f2, f3) <*> MyTriple (x1, x1, x3) = MyTriple (fi x1, fj x2, fk x3)
so the components of the right argument of <*> cannot be permuted.
What about the left argument? The answer to that question is given by the interchange law. We have
MyTriple (f1, f2, f3) <*> pure x = MyTriple (fi x, fj x, fk x)
and by the interchange law this must be equal to
pure ($x) <*> MyTriple (f1, f2, f3) = MyTriple (fp x, fq x, fr x)
since in this case f functions assume the role of arguments of $x, that is
($x) f = f x. Combining these two, we see that
MyTriple (fi x, fj x, fk x) = MyTriple (fp x, fq x, fr x)
and again, the only way for this to hold regardless of f's and x is if i = p, j = q and k = r, so i = 1, j = 2, k = 3. The final version of operator <*>, the only one satisfying the applicative functor laws2,
is thus the simplest and most natural one:
MyTriple (f1, f2, f3) <*> MyTriple (x1, x1, x3) = MyTriple (f1 x1, f2 x2, f3 x3)
1 Apart from undefined & co.
2 Strictly speaking, we need to check the remaining two laws
(composition and homomorphism) before we are done