At Haskell eXchange 2019, Yves Parès was presenting his “porcupine” library, a library to help scientists run data pipelines using the power of Haskell’s arrows. At some stage, he said, “you know if you’re using a ‘records’ library, like Vinyl, you have to build your HList by appending RNil
at the end”. And I thought: No!
This is a very small thing that has been bugging me for some time. If I want to build a HList
, why do I have to append HNil
at the end? As soon as I’m appending 2 things together to form an HList
the whole type should be determined, isn’t it? Let’s work on a bit of code.
Here is the standard definition of a HList
in Haskell:
data HList (l::[*]) where
HNil :: HList '[]
HCons :: e -> HList l -> HList (e ': l)
-- example
myHList :: HList [Int, Text]
myHList = HCons 1 (HCons "Hello" HNil)
I can define a :+
operator to make the operation of appending an element a bit nicer:
infixr 5 +:
(+:) :: a -> HList as -> HList (a : as)
(+:) = HCons
myHList1 :: HList [Int, Text]
myHList1 =
1
+: "Hello"
+: HNil
I can also define an <+>
operator to append 2 HLists together:
-- :++ is a type-level operator (not defined here)
-- for appending 2 lists of types together (see the Appendix)
infixr 4 <+>
(<+>) :: HList as -> HList bs -> HList (as :++ bs)
(<+>) HNil bs = bs
(<+>) (HCons a as) bs = HCons a (as <+> bs)
list1 :: HList [Int, Text]
list1 = 1 +: "Hello" +: HNil
list2 :: HList [Double, Bool]
list2 = 2.0 +: True +: HNil
lists :: HList [Int, Text, Double, Bool]
lists = list1 <+> list2
All good so far, that’s a reasonable API. However we still need to specify HNil
every time we create a new HList
. Can we avoid it?
A more polymorphic operator
In order to avoid using HNil
we need to have an operator, let’s call it <:
, to know what to do when:
- adding one element to another:
a <: b
- adding one element to a
HList
:a <: bs
But even better we should be able to:
- append 2
HList
together:as <: bs
- append an element at the end of a
HList
:as <: b
We can already see that this operator can not be a straightforward Haskell functions, because the types of its first and second arguments are not always the same. Annoying. Wait, there’s a tool in Haskell to cope with variations in types like that: typeclasses!
infixr 5 <:
class AddLike a b c | a b -> c where
(<:) :: a -> b -> c
instance {-# OVERLAPPING #-} (asbs ~ (as :++ bs)) =>
AddLike (HList as) (HList bs) (HList asbs) where
(<:) = (<+>)
instance (abs ~ (a : bs)) => AddLike a (HList bs) (HList abs) where
(<:) = (+:)
instance AddLike a b (HList [a, b]) where
(<:) a b = a +: b +: HNil
instance (asb ~ (as :++ '[b])) => AddLike (HList as) b (HList asb) where
as <: b = as <+> (b +: HNil)
This AddLike
typeclass will deal with all the cases and now we can write:
a = 1 :: Int
b = "hello" :: Text
c = 2.0 :: Double
d = True :: Bool
ab = a <: b
bc = b <: c
abc = a <: bc
bca = bc <: a
abcd = ad <: cd
That’s it, one operator for all the reasonable cases.
Appendix
Here is the full code:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE PolyKinds #-}
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
module AddLikeApi where
import Protolude
data HList (l :: [Type]) where
HNil :: HList '[]
HCons :: e -> HList l -> HList (e ': l)
myHList :: HList [Int, Text]
myHList = HCons 1 (HCons "Hello" HNil)
infixr 5 +:
(+:) :: a -> HList as -> HList (a : as)
(+:) = HCons
myHList' :: HList [Int, Text]
myHList' =
1
+: "Hello"
+: HNil
-- * Appendix
infixr 4 <+>
(<+>) :: HList as -> HList bs -> HList (as :++ bs)
(<+>) HNil bs = bs
(<+>) (HCons a as) bs = HCons a (as <+> bs)
infixr 5 <:
class AddLike a b c | a b -> c where
(<:) :: a -> b -> c
instance {-# OVERLAPPING #-} (asbs ~ (as :++ bs)) =>
AddLike (HList as) (HList bs) (HList asbs) where
(<:) = (<+>)
instance (abs ~ (a : bs)) => AddLike a (HList bs) (HList abs) where
(<:) = (+:)
instance AddLike a b (HList [a, b]) where
(<:) a b = a +: b +: HNil
instance (asb ~ (as :++ '[b])) => AddLike (HList as) b (HList asb) where
as <: b = as <+> (b +: HNil)
type family (:++) (x :: [k]) (y :: [k]) :: [k] where
'[] :++ xs = xs
(x : xs) :++ ys = x : (xs :++ ys)
-- examples
list1 :: HList [Int, Text]
list1 = 1 +: "Hello" +: HNil
list2 :: HList [Double, Bool]
list2 = 2.0 +: True +: HNil
lists :: HList [Int, Text, Double, Bool]
lists = list1 <+> list2
a = 1 :: Int
b = "hello" :: Text
c = 2.0 :: Double
d = True :: Bool
ab :: HList [Int, Text]
ab = a <: b
bc :: HList [Text, Double]
bc = b <: c
cd :: HList [Double, Bool]
cd = c <: d
abc' :: HList [Int, Text, Double]
abc' = ab <: c
abc :: HList [Int, Text, Double]
abc = a <: bc
abcd :: HList [Int, Text, Double, Bool]
abcd = ab <: cd