Something to watch for while using record updating syntax
Let’s pop open GHCi and construct a record with a type tag.
We’ll want to be able to add some extra type annotations
(thing :: type
) in places we can’t normally, so let’s turn
on ScopedTypeVariables
:set -XScopedTypeVariables
We want to make a type X
with a tag t
.
t
lets us differentiate between X
s by keeping
information in the type, but without having to store any additional
values. X
also has a record with some stuff inside it, but
the kind of stuff in X
isn’t super relevant.
data X t = X { c :: Char, d :: Double }
Now that we have an X
type, let’s make a value of type
X
.
myX :: X Int = X 'c' 1
Double-check the type of myX
. Looks like it’s still
hanging on to the Int
tag.
:t myX
myX :: X Int
However, what happens when we try to update the record? Looks like
Int
has gone away, leading to the much-less-informative
polymorphic t
.
:t myX { d = 2 }
= 2 } :: X t myX { d
We lost our tag due to the record update! This might be confusing in
real code, since we can do things like assign what was just previously
an X Int
to an X String
. Because the record
update doesn’t keep the tag around, the tag’s usefulness diminishes
completely in this case.
x2 :: X String = (myX :: X Int) { d = 2 }
To understand why this happens, let’s write a similar updater function ourselves.
replaceC
is a function that takes an X
and
replaces the c
inside. It acts just like a record
update.
X c d) newC = X newC d replaceC (
Also just like a record update, replaceC
doesn’t ensure
that the tag sticks around.
:t replaceC myX 'o'
'o' :: X t2 replaceC myX
Looking at its type, we can see why:
:t replaceC
replaceC :: X t1 -> Char -> X t2
The most general type for replaceC
allows
t1
and t2
to be different (and GHC loves to be
as general as it can). To say that they should be the same is an
additional restriction. However, in the case of keeping tags around,
it’s really nice not to lose the tag just because you updated some other
part of the data structure.
By adding a type signature specifying the restriction that
replaceC
should preserve t
, we get to keep
tags even when we replace bits of the record.
replaceC :: X t -> Char -> X t
X c d) newC = X newC d replaceC (
However, I wasn’t able to find a great way to do this tag
preservation while still using the record update syntax (which is
essentially just some syntactic sugar over a function kind of like
replaceC
). Hopefully someone knows a good trick for
this.