A pattern that presents itself the more often the more type safety is being introduced via newtype
is to project a value (or several values) to a newtype
wrapper, do some operations, and then retract the projection. An ubiquitous example is that of Sum
and Product
monoids:
λ x + y = getSum $ Sum x `mappend` Sum y
λ 1 + 2
3
I imagine a collection of functions like withSum
, withSum2
, and so on, may be automagically rolled out for each newtype
. Or maybe a parametrized Identity
may be created, for use with ApplicativeDo
. Or maybe there are some other approaches that I could not think of.
I wonder if there is some prior art or theory around this.
P.S. I am unhappy with coerce
, for two reasons:
safety I thought it is not very safe. After being pointed that it is actually safe, I tried a few things and I could not do anything harmful, because it requires a type annotation when there is a possibility of ambiguity. For example:
λ newtype F = F Int deriving Show
λ newtype G = G Int deriving Show
λ coerce . (mappend (1 :: Sum Int)) . coerce $ F 1 :: G
G 2
λ coerce . (mappend (1 :: Product Int)) . coerce $ F 1 :: G
G 1
λ coerce . (mappend 1) . coerce $ F 1 :: G
...
• Couldn't match representation of type ‘a0’ with that of ‘Int’
arising from a use of ‘coerce’
...
But I would still not welcome coerce
, because it is far too easy to strip a safety label and
shoot someone, once the reaching for it becomes habitual. Imagine that, in a cryptographic
application, there are two values: x :: Prime Int
and x' :: Sum Int
. I would much rather
type getPrime
and getSum
every time I use them, than coerce
everything and have one day
made a catastrophic mistake.
usefulness coerce
does not bring much to the table regarding a shorthand for
certain operations. The leading example of my post, that I repeat here:
λ getSum $ Sum 1 `mappend` Sum 2
3
— Turns into something along the lines of this spiked monster:
λ coerce $ mappend @(Sum Integer) (coerce 1) (coerce 2) :: Integer
3
— Which is hardly of any benfit.
Your "spiked monster" example is better handled by putting the summands into a list and using the ala
function available here, which has type:
ala :: (Coercible a b, Coercible a' b')
=> (a -> b)
-> ((a -> b) -> c -> b')
-> c
-> a'
where
a
is the unwrapped base type.b
is the newtype that wraps a
.a -> b
is the newtype constructor.((a -> b) -> c -> b')
is a function that, knowing how to wrap values of the base type a
, knows how to process a value of type c
(almost always a container of a
s) and return a wrapped result b'
. In practice this function is almost always foldMap
.a'
the unwrapped final result. The unwrapping is handled by ala
itself.in your case, it would be something like:
ala Sum foldMap [1,2::Integer]
"ala" functions can be implemented through means other than coerce
, for example using generics to handle the unwrapping, or even lenses.