有没有一种简写方式来表示像 `fromNewtype . f . toNewtype` 这样的操作?

8
每当使用 `newtype` 引入类型安全性时,一种常见的模式是将一个或多个值投影到 `newtype` 包装器中,进行一些操作,然后再收回投影。一个普遍的例子是 `Sum` 和 `Product` 半群:
λ x + y = getSum $ Sum x `mappend` Sum y
λ 1 + 2
3

我想象一组函数,比如withSumwithSum2等等,可以自动为每个newtype滚动出来。或者可能创建一个参数化的Identity,供ApplicativeDo使用。或者可能还有其他我没想到的方法。
我想知道这方面是否有先前的艺术或理论。
附言:我对coerce不满意,原因有两个。
  • 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.


2
关于你对 coerce 作为缩写的批评,请看我的回答。你不是强制输入和输出(这确实比应用 newtype 构造器/获取器好一点,但并没有多少优势)而是强制转换你想要调用的函数,使其具有不同的类型。至于安全性,它与手动展开一样安全或不安全。如果你想让这变得不可能,请勿导出 newtype 构造器,那么 coerce 也不起作用。如果你能搞清楚coerce,你也能搞清楚手动 newtype 展开。 - Ben
我同意你对 coerce 的不安 -- 尽管 coerce 不会做任何你不能以其他安全方式完成的事情。特别地,除非它们的构造函数在作用域内,否则 newtypes 是不可强制转换的。因此,如果你正确地使用智能构造器(例如 smart constructors),coerce 就不会对你造成伤害。 - luqui
3个回答

9

你的“尖刺怪物”示例最好将加数放入列表中,并使用这里可用的ala函数,其类型为:

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

在这里:

  • a 是未封装的基础类型。
  • b 是封装 a 的新类型。
  • a -> b 是新类型构造函数。
  • ((a -> b) -> c -> b') 是一个函数,它知道如何封装基础类型 a 的值,并知道如何处理类型为 c(几乎总是包含 a 的容器)的值并返回封装后的结果 b'。实际上,这个函数几乎总是使用 foldMap
  • a' 是最终的未封装结果。解封装由 ala 自己处理。

针对您的情况,应该是这样的:

ala Sum foldMap [1,2::Integer]

"coerce并非实现ala函数的唯一方式,例如可以使用泛型处理解包,甚至可以使用lenses。"

1
很高兴在这里看到提到了 coercible-utils! :) 不知道您是否可以将链接更改为 http://hackage.haskell.org/package/coercible-utils-0.0.0/docs/CoercibleUtils.html#v:ala 呢?这样更清楚地表明 coercible-utils 可以从 Hackage 获取。 - sjakobi

6

是的,有这个功能!它是来自base包的coerce函数。它允许自动从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

我应该提到 coerce 不适合此目的,因为它违反了类型安全。 - Ignat Insarov
@IgnatInsarov 为什么 coerce 违反类型安全性?你确定你不是在想 unsafeCoerce 吗? - David Young
4
这是一个普遍的误解。coerce 是完全安全的。比如说,你无法将整数强制转换为字符串。只有使用 unsafeCoerce 才能实现。 - Shersh
@DavidYoung并不是完全违背我的意愿。只是我不相信自己能够很好地应对它。请看我在问题后附加的附言。 - Ignat Insarov
@IgnatInsarov 可以使用 coerce 实现类似 via @Sum @Int (<>) 3 4 的函数,其结果为 3 + 4 - Shersh
显示剩余2条评论

6

coerce来自于Data.Coerce,非常适用于此类问题。您可以使用它在具有相同表示的不同类型之间进行转换(例如,在类型和新类型包装器之间进行转换,或者反之亦然)。例如:

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

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

它的开发旨在解决这样一个问题:将Int转换为Sum Int,可以通过应用Sum实现免费,但通过应用map Sum[Int]转换为[Sum Int]则不一定是免费的。编译器可能能够优化掉map中列表脊柱的遍历,也可能不能,但我们知道内存中的同一结构可以作为[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 的“来源”和“目标”类型。
我曾经写过一个库,提供了几种不同的方式通过新类型来参数化类型,并为每个方案提供了类似的API。实现API的模块充满了类型签名和 foo' = coerce foo 类型的定义;感觉很好,因为除了声明所需的类型外,我几乎没有做任何工作。
你的例子(在 Sum 上使用 mappend 实现加法,而不必显式地进行转换)可以像这样:
λ let (+) :: Int -> Int -> Int
|     (+) = coerce (mappend @ (Sum Int))
| 
(+) :: Int -> Int -> Int

λ 3 + 8
11
it :: Int

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接