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

爷,独闯天下 提交于 2019-12-22 04:15:10

问题


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.


回答1:


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.




回答2:


coerce from Data.Coerce can be pretty great for this sort of thing. You can use it to convert between different types with the same representation (like between a type and a newtype wrapper, or vice versa). For example:

λ coerce (3 :: Int) :: Sum Int
Sum {getSum = 3}
it :: Sum Int

λ coerce (3 :: Sum Int) :: Int
3
it :: Int

It was developed to solve the problem that it is cost-free to e.g. convert an Int into a Sum Int by applying Sum, but it isn't necessarily cost-free to e.g convert a [Int] to a [Sum Int] by applying map Sum. The compiler might be able to optimise away the traversal of the list spine from map or it might not, but we know that the same structure in memory can serve as either a [Int] or a [Sum Int], because the list structure doesn't depend on any properties of the elements and the element types have identical representation between those two cases. coerce (plus the role system) allows us to make use of this fact to convert between the two in a way that is guaranteed not to do any runtime work, but still have the compiler check that it's safe to do so:

λ coerce [1, 2, 3 :: Int] :: [Sum Int]
[Sum {getSum = 1},Sum {getSum = 2},Sum {getSum = 3}]
it :: [Sum Int]

Something that wasn't at all obvious to me at first is that coerce is not limited to coercing "structures"! Because all it's doing is allowing us to substitute types (including parts of compound types) when the representations are identical, it works just as well to coerce code:

λ addInt = (+) @ Int
addInt :: Int -> Int -> Int

λ let addSum :: Sum Int -> Sum Int -> Sum Int
|     addSum = coerce addInt
| 
addSum :: Sum Int -> Sum Int -> Sum Int

λ addSum (Sum 3) (Sum 19)
Sum {getSum = 22}
it :: Sum Int

(In the above example I had to define a monotype version of + because coerce is so generic the type system otherwise doesn't know which version of + I'm asking to coerce to Sum Int -> Sum Int -> Sum Int; I could instead have given an inline type signature on the argument to coerce, but that looks less tidy. Often in real usage the context is sufficient to determine the "source" and "target" types of the coerce)

I once wrote a library that provided a few different ways of paramterising types via newtypes, and provided similar APIs with each scheme. The modules implementing the APIs were full of type signatures and foo' = coerce foo style definitions; it felt really nice that I was barely doing any work other than stating the types that I wanted.

Your example (using mappend on Sum to implement addition, without having to explicitly convert back and forth) could look like:

λ let (+) :: Int -> Int -> Int
|     (+) = coerce (mappend @ (Sum Int))
| 
(+) :: Int -> Int -> Int

λ 3 + 8
11
it :: Int



回答3:


Yes, there's! It's a coerce function from base package. It allows to convert from newtype and to newtype automatically. GHC actually has a big chunk of theory behind coercions.

In relude I called this function under.

ghci> newtype Foo = Foo Bool deriving Show
ghci> under not (Foo True)
Foo False
ghci> newtype Bar = Bar String deriving Show
ghci> under (filter (== 'a')) (Bar "abacaba")
Bar "aaaa"

You can see the whole module here:

  • http://hackage.haskell.org/package/relude-0.2.0/docs/Relude-Extra-Newtype.html

It's also possible to implement custom functions for binary operators as well:

ghci> import Data.Coerce 
ghci> :set -XScopedTypeVariables 
ghci> :set -XTypeApplications 
ghci> :{
ghci| via :: forall n a . Coercible a n => (n -> n -> n) -> (a -> a -> a)
ghci| via = coerce
ghci| :}
ghci> :{
ghci| viaF :: forall n a . Coercible a (n a) => (n a -> n a -> n a) -> (a -> a -> a)
ghci| viaF = coerce
ghci| :}
ghci> via @(Sum Int) @Int (<>) 3 4
7
ghci> viaF @Sum @Int (<>) 3 5
8


来源:https://stackoverflow.com/questions/51915219/is-there-a-shorthand-for-operations-like-fromnewtype-f-tonewtype

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!