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 Ints, while the latter adds the same layer around the tuple, which itself adds a layer around the Ints.
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