8

一种越频繁出现的模式,引入的类型安全就越多,newtype是将一个值(或多个值)投影到newtype包装器,执行一些操作,然后收回投影。一个普遍存在的例子是SumProduct幺半群:

λ x + y = getSum $ Sum x `mappend` Sum y
λ 1 + 2
3

我想可能withSumwithSum2自动为每个newtype. 或者也许Identity可以创建一个参数化,用于ApplicativeDo. 或者,也许还有其他一些我想不到的方法。

我想知道这方面是否有一些现有技术或理论。

PS   我不满意coerce,有两个原因:

  • 安全   我认为这不是很安全。在被指出它实际上是安全的之后,我尝试了一些事情并且我无法做任何有害的事情,因为当存在歧义时它需要类型注释。例如:

    λ 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’
    ...
    

    但我仍然不欢迎coerce,因为一旦习惯了伸手去拿,撕下安全标签并射杀某人太容易了。想象一下,在加密应用程序中,有两个值:x :: Prime Intx' :: Sum Int。我宁愿打字getPrimegetSum每次我使用它们时,也不愿coerce做任何事情,并且有一天会犯一个灾难性的错误。

  •   关于某些操作的速记,有用性coerce并没有带来太多好处。我的帖子的主要示例,我在这里重复:

    λ getSum $ Sum 1 `mappend` Sum 2
    3
    

    ——变成了类似这个尖刺怪物的东西:

    λ coerce $ mappend @(Sum Integer) (coerce 1) (coerce 2) :: Integer
    3
    

    ——这几乎没有任何好处。

4

3 回答 3

9

通过将 summands 放入列表并使用ala可用的函数here可以更好地处理您的“spiked monster”示例,该函数具有以下类型:

ala :: (Coercible a b, Coercible a' b') 
    => (a -> b) 
    -> ((a -> b) -> c -> b')   
    -> c 
    -> a' 

在哪里

  • a展开的基本类型。
  • b是包装的新类型a
  • a -> b是 newtype 构造函数。
  • ((a -> b) -> c -> b')是一个函数,它知道如何包装基本类型的值a,知道如何处理类型的值c(几乎总是 s 的容器a)并返回包装后的结果b'。在实践中,这个函数几乎总是foldMap.
  • a'展开的最终结果。解包由ala自己处理。

在你的情况下,它会是这样的:

ala Sum foldMap [1,2::Integer]

“ala”函数可以通过其他方式实现coerce,例如使用泛型来处理展开,甚至是镜头

于 2018-08-19T09:12:39.610 回答
6

是的,有!这是包中的一个coerce功能base。它允许自动转换newtype和转换newtype。GHC 实际上有很大一部分关于强制的理论。

relude我调用了这个函数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"

你可以在这里看到整个模块:

也可以为二元运算符实现自定义函数:

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
于 2018-08-19T07:11:17.727 回答
6

coerce来自Data.Coerce对于这类事情可能非常棒。您可以使用它在具有相同表示的不同类型之间进行转换(例如在类型和新类型包装器之间进行转换,反之亦然)。例如:

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

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

它的开发是为了解决例如通过应用将 a 转换为 a 是免费的问题,但例如通过应用将 a 转换Int为 a不一定Sum Int是免费Sum的。编译器可能能够优化列表脊椎的遍历,也可能不会,但是我们知道内存中的相同结构可以用作 a或 a ,因为列表结构不依赖于元素和元素类型在这两种情况下具有相同的表示。(加上角色系统[Int][Sum Int]map Summap[Int][Sum Int]coerce) 允许我们利用这个事实在两者之间进行转换,以保证不做任何运行时工作,但仍然让编译器检查这样做是安全的:

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

起初对我来说并不明显的是,coerce它不仅限于强制“结构”!因为它所做的只是允许我们在表示相同时替换类型(包括复合类型的一部分),所以它同样适用于强制代码

λ 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

(在上面的例子中,我必须定义一个单型版本,+因为coerce它是如此通用,否则类型系统不知道+我要求强制转换为哪个版本Sum Int -> Sum Int -> Sum Int;我可以在参数上给出一个内联类型签名coerce,但这看起来不太整洁。通常在实际使用中,上下文足以确定 ) 的“源”和“目标”coerce类型

我曾经写过一个库,它通过 newtypes 提供了几种不同的参数化类型的方法,并为每个方案提供了类似的 API。实现 API 的模块充满了类型签名和foo' = coerce foo样式定义;除了说明我想要的类型之外,我几乎没有做任何工作,这感觉真的很好。

您的示例(使用mappendonSum实现加法,无需显式来回转换)可能如下所示:

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

λ 3 + 8
11
it :: Int
于 2018-08-19T07:20:20.427 回答