This is a dumb question that's been bugging me for a bit. Why can't I write a newtype with multiple parameters,
newtype A = A Int Int
while the tuple version is just fine?
newtype A = A (Int, Int)
the former is much nicer in things like pattern matching.
newtype A = A Int
creates a type isomorphic to Int
. That is, it behaves exactly like Int
w.r.t. e.g. bottom
, but under a different name.
This is in contrast with data A = A Int
which creates a lifted type that behaves differently from Int. There's another value added that is not in Int
: A undefined
(which is distinct from undefined::A
).
Now, newtype A = A (Int, Int)
creates a type isomorphic to (Int, Int)
. Which is, incidentally, exactly exactly what data A = A Int Int
does.
So if we admit newtype A = A Int Int
as being equivalent to newtype A = A (Int, Int)
, what do we have? newtype A = A Int Int
is equivalent to newtype A = A (Int, Int)
which is equivalent to data A = A Int Int
.
newtype A = A Int Int
is equivalent to data A = A Int Int
(so newtype
is redundant in this case), but
newtype A = A Int
is not equivalent to data A = A Int
(which is the whole point of having newtype
in the first place).
So we must conclude that newtype A = A Int Int
being equivalent to newtype A = A (Int, Int)
creates redundancy and inconsistency, and we're better off not allowing it.
There's probably no way to give newtype A = A Int Int
some other meaning which is free from these inconsistencies (or else it would be found and used I suppose ;)
Because a newtype
, roughly speaking, works like type
at runtime and like data
at compile-time. Each data
definition adds an extra layer of indirection--which, under normal circumstances, means another distinct place where something can be left as a thunk--around the values it holds, whereas newtype
doesn't. The "constructor" on a newtype
is basically just an illusion.
Anything that combines multiple values into one, or that gives a choice between multiple cases, necessarily introduces a layer of indirection to express that, so the logical interpretation of newtype A = A Int Int
would be two disconnected Int
values with nothing "holding them together". The difference in the case of newtype A = A (Int, Int)
is that the tuple itself adds the extra layer of indirection.
Contrast this with data A = A Int Int
vs. data A = A (Int, Int)
. The former adds one layer (the A
constructor) around the two Int
s, while the latter adds the same layer around the tuple, which itself adds a layer around the Int
s.
Each layer of indirection also generally adds a place where something can be ⊥, so consider the possible cases for each form, where ? stands for a non-bottom value:
For newtype A = A (Int, Int)
: ⊥
, (⊥, ?)
, (?, ⊥)
, (?, ?)
For data A = A Int Int
: ⊥
, A ⊥ ?
, A ? ⊥
, A ? ?
For data A = A (Int, Int)
: ⊥
, A ⊥
, A (⊥, ?)
, A (?, ⊥)
, A (?, ?)
As you can see from the above, the first two are equivalent.
On a final note, here's a fun demonstration of just how newtype
differs from data
. Consider these definitions:
data D = D D deriving Show
newtype N = N N deriving Show
What possible values, including all possible ⊥s, do each of these have? And what do you think the two values below will be?
d = let (D x) = undefined in show x
n = let (N x) = undefined in show x
Load them in GHC and find out!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With