Search code examples
haskellnewtypecoerce

Is there a shorthand for operations like `fromNewtype . f . toNewtype`?


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.


Solution

  • 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 as) 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.