Hackle's blog
between the abstractions we want and the abstractions we get.
If you are looking for a good example of taking something simple and turning it into something indistinguishable from magic, look no further, I have just the thing: the Const
type.
The definition of Const
is a simple one, as from Data.Functor.Const
:
newtype Const a b = Const { getConst :: a }
Note the second type parameter b
is not used in the constructor. For example, Const "a"
, which is of type Const [Char] b
.
One interesting effect of this, is that b
can be interpreted as any type, we can try this out with GHCi
.
Data.Functor.Const> :t (Const "a" :: Const [Char] Int)
(Const "a" :: Const [Char] Int) :: Const [Char] Int
Data.Functor.Const> :t (Const "a" :: Const [Char] Bool)
(Const "a" :: Const [Char] Bool) :: Const [Char] Bool
Due to such strangeness as with the ignored b
, Const
is called a phantom type
.
With the above definition, its Functor
instance is pretty routine but equally interesting.
instance Functor (Const m) where
fmap _ (Const v) = Const v
Remember fmap :: (a -> b) -> (F a) -> (F b)
, the above implementation thus specializes to type fmap :: (a -> b) -> (Const x a) -> (Const x b)
, however, both a
and b
are discarded, so (a -> b)
has nothing to act on, and is thus also discarded and replaced with _
.
Data.Functor.Const> (+1) <$> Const "a"
Const "a"
Data.Functor.Const> length <$> Const "a"
Const "a"
We can throw any function at it, in vain, a Const
value is resistant to fmap
.
The Applicative instance is even more interesting. (Note for simplicity this is different than the definition in Prelude)
instance Monoid m => Applicative (Const m) where
pure _ = Const mempty
Const x <*> Const y = Const (x `mappend` y)
m
is required to be a Monoid
. Why? Well, remember the instance specializes to
<*> :: Const x (a -> b) -> Const x (a) -> Const x b
As with the Functor
instance, (a -> b)
, a
and b
are ignored all together. But different than the Functor
instance, we now have two values x
and y
to dispose of. What do we do with them? We can't just throw them away, or just keep one of them (we CAN but it'll be like cheating), One easy solution is just to somehow combine them, and what better way to express that than with Monoid
?
Data.Functor.Const> Const "a" <*> Const "b"
Const "ab"
-- but not this as Char is not a monoid
Data.Functor.Const> :t Const 'a' <*> Const 'b'
<interactive>:1:1: error:
_ No instance for (Monoid Char) arising from a use of '<*>'
_ In the expression: Const 'a' <*> Const 'b'
Hence the Monoid
constraint. Makes sense.
Now the introduction is over, let's look at an intriguing use of Const
, with the lens
library as described here. Say we have a Person
type.
data Person = Person { name :: String } deriving (Show, Eq)
And to recap the famous Lens
definition, double primed to avoid conflict,
type Lens'' s t a b = forall f. Functor f => (a -> f b) -> s -> f t
We can define a lens lname
to focus on name
of Person
. Note fname
is a function that act on the name
field.
lname :: Lens'' Person Person String String
lname fname p = (\n -> p { name = n }) <$> fname (name p)
This innocuous lname
updates a Person
record, but wrapped in a Functor
- as a matter of fact, any Functor. As a trivial example, we pass in Identity.reverse
to reverse the name.
> runIdentity $ lname (Identity . reverse) $ Person "Hackle"
Person {name = "elkcaH"}
Pretty straightforward, right? What is less so, is when we pass in Const
instead,
> getConst $ lname Const $ Person "Hackle"
"Hackle"
We get the name back, and the name only! What happened to the Person
? Shouldn't it have been modified and returned? Well, it's been thrown away, thanks to Cosnt
. Let's recap the implementation of lname
,
lname fname p = (\n -> p { name = n }) <$> fname (name p)
and put Const
in place of fname
, with a bit of partial application, we get,
lname p = (\n -> p { name = n }) <$> Const (name p)
Now remember the implementation of fmap
for Const
? It will keep the name
value through fmap
as it's resistant to it.
Clever right? For one thing, it certainly fits the grand scheme of lens
pretty well.
Identity
and Const
as oneIt's well known that for lens
, Identity
is used for setting / overriding of values, and Const
for viewing values (as shown above). However, a pretty well hidden secret is, they can be seen as two sides of the same coin. Which coin, you ask? Just the good old tuple, I say.
Taking a tuple (a, b)
, Const
can be seen as only needing a
, as in (a, undefined)
; Identity
, on the other hand, only uses b
, so it's isomorphic to (undefined, b)
.
To see how this works let's make a couple of helper functions,
mkIdentity a = (undefined, a)
mkConst a = (a, undefined)
Then we can use them in place of Identity
and Const
as follows:
> snd $ lname mkIdentity $ Person "Hackle"
Person {name = "Hackle"}
>
> fst $ lname mkConst $ Person "Hackle"
"Hackle"
You would have noticed that (a, b)
is both a Functor
and an Applicative
- that's why we get no complaints from GHCi
.
Be warned though, when trying them out in GHCi
, it won't work without applying snd
and fst
, as undefined
will kick in. Thanks to laziness, if we avoid touching undefined
in the tuple, there would be no exception.
The acute reader would have been screaming already - why use undefined
at all? Just duplicate the value for the tuple as (a, a)
! And we don't have to worry about GHCi
blowing up. Indeed that works.
> dup = \a -> (a, a)
> lname (dup . reverse) $ Person "Hackle"
("elkcaH",Person {name = "elkcaH"})
> fst $ lname (dup . reverse) $ Person "Hackle"
"elkcaH"
> snd $ lname (dup . reverse) $ Person "Hackle"
Person {name = "elkcaH"}
This of course is no coincidence. In essence Identity
, Const
and (a, b)
are all product types and therefore have much in common. In practice we'd still be using Const
and Identity
as they are safer and more expressive, but if you find them confusing, then the above understanding may be helpful.
As with most things in Haskell, there is no real magic with Const
, it's all about solid reasoning around simple, solid ideas. However, the use of such basic ideas, when combined with one another, can appear quite extraordinary.
To spot and use such trickery, we'll also need to build an intuition around it - which requires much practice; or at times, shortcuts may come in handy, such as tuple for Identity
and Const
.