You can find this article as a literate Haskell file here.
Definitions carried over from part one:
newtype Decode k v a = Decode{ decode :: Map k v -> a }
newtype Encode k v a = Encode{ encode :: a -> Map k v }
data Codec k v a = Codec{ co :: Encode k v a, dec :: Decode k v a }
I first saw the following technique used with optparse-applicative
, though I
cannot remember where. Here is a typical-looking type that might represent the
values of all a program's command-line arguments:
data Opt1 = Opt1{ verbose :: Bool,
file :: FilePath,
jobs :: Natural }
opt1 = Opt1{ verbose = True, file = "/tmp/xyz.hs", jobs = 4 }
Suppose we wrap each of the three fields in the Identity
constructor. This, of
course, achieves nothing, apart from giving us something to refer to when we
think about the alteration that follows.
data Opt2 = Opt2{ verbose :: Identity Bool,
file :: Identity FilePath,
jobs :: Identity Natural }
opt2 = Opt2{ verbose = Identity True,
file = Identity "/tmp/xyz.hs",
jobs = Identity 4 }
Now instead of Identity
, we'll make that a type parameter f
.
data Opt3 f = Opt3{ verbose :: f Bool,
file :: f FilePath,
jobs :: f Natural }
opt3 = Opt3{ verbose = Identity True,
file = Identity "/tmp/xyz.hs",
jobs = Identity 4 }
Notice that still apparently we've done very little. The definition of opt3
is
exactly like that of opt2
, only now its type is Opt3 Identity
rather than
Opt2
. But what this shift has done is now let us use type constructors other
than just Identity
. For example, Opt.Parser
:
opt3Parser :: Opt3 Opt.Parser
opt3Parser =
Opt3{ verbose = Opt.switch (Opt.short 'v'),
file = Opt.strOption (Opt.short 'f'),
jobs = Opt.option Opt.auto (Opt.short 'j') }
Whereas Opt3 Identity
consists of three values -- Bool
, FilePath
, and
Natural
-- Opt3 Opt.Parser
consists of three parsers: an Opt.Parser Bool
,
an Opt.Parser FilePath
, and an Opt.Parser Natural
.
The question is then how to use such a thing, because I do not want three
parsers; I want them combined into one parser that shall have Opt3 Identity
as
its result type.
multiplyParser :: Opt3 Opt.Parser -> Opt.Parser (Opt3 Identity)
multiplyParser = undefined
However, it now feels like we have made some progress, because we can at least express the composition of parsers (in a way that generalizes to contravariant and invariant functors), even if we do not yet have to means to evaluate the expression.
The parsing example may seem superfluous because Opt.Parser
is already
Applicative
, and so there is little need to improve upon the faculties for
composition that it already has. But notice that we can insert other types of
f
as well, including invariant functors such as Codec k v
that do not admit
Applicative
composition.
opt3Codec :: Opt3 (Codec Char String)
opt3Codec = Opt3{ verbose = verboseCodec, file = fileCodec, jobs = jobsCodec }
verboseCodec = charStringCodec 'v' (Iso show (== "True"))
fileCodec = charStringCodec 'f' (Iso id id)
jobsCodec = charStringCodec 'k' (Iso show (fromMaybe 0 . readMaybe))
data Iso a b = Iso (a -> b) (b -> a)
charStringCodec :: Char -> Iso a String -> Codec Char String a
charStringCodec k (Iso encodeString decodeString) =
Codec{ co = Encode \v -> Map.singleton k (encodeString v),
dec = Decode \m -> decodeString (Map.findWithDefault "" k m) }
Again, now we have a way to express the product of three codecs, but we are still missing a function to multiply these three factors into a single codec.
multiplyCodec :: Opt3 (Codec k v) -> Codec k v (Opt3 Identity)
multiplyCodec = undefined
The next time I saw higher-kinded products was Oliver Charles presenting the
rel8
library, which includes a neat trick that I will include now. This
business of having to insert the Identity
data constructor into each field
expression when we changed from Opt1
to Opt2 Identity
is a bit irksome. The
trick lets us eliminate it.
type Factor :: (Type -> Type) -> Type -> Type
type family Factor f a where
Factor Identity a = a
Factor f a = f a
data Opt4 f = Opt4{ verbose :: Factor f Bool,
file :: Factor f FilePath,
jobs :: Factor f Natural }
The family instance Factor Identity a = a
provides a special handling of the
case where f = Identity
, stating that this is merely an alias for a
itself.
The second instance, Factor f a = f a
, says that for all other f
,
Factor f a
should be read as simply f a
, which is what we had before.
Values of Opt4 Identity
can now be written without an Identity
term, exactly
as we wrote opt1
.
opt4 :: Opt4 Identity
opt4 = Opt4{ verbose = True, file = "/tmp/xyz.hs", jobs = 4 }
And the codec definitions for Opt3
and Opt4
are the same.
opt4Codec :: Opt4 (Codec Char String)
opt4Codec = Opt4{ verbose = verboseCodec, file = fileCodec, jobs = jobsCodec }
Now we must return to how to define the two "multiply" functions.
Opt4 Opt.Parser -> Opt.Parser (Opt4 Identity)
Opt4 (Codec k v) -> Codec k v (Opt4 Identity)
More generally, something that looks like:
multiply :: forall ( factors :: (Type -> Type) -> Type )
( functor :: Type -> Type ).
factors functor -> functor (factors Identity)
multiply = undefined
In our latter example, the factors
are Opt4
(representing the three option
factors verbose
, file
, and jobs
) and the functor
is Codec k v
.
But that's a job for another day.