【问题标题】:Proposed deriving mechanism for Haskell为 Haskell 提出的推导机制
【发布时间】:2013-09-01 15:39:56
【问题描述】:

很抱歉,如果这个问题似乎经过深思熟虑,但我想知道是否可以在 Haskell 中为以下内容定义一致的语义:

derive Num String from Int where
    get = read
    set = show

derive Ord Bool from Integer where
    get = fromEnum
    set = toEnum

derive (Monad, Functor) Maybe from [] where
    get (Just x) = [x]
    get Nothing  = [ ]
    set [x]      = Just x
    set [ ]      = Nothing

我看不出为什么不这样做,而且在某些情况下它似乎会减少样板文件,但我不知道是否(如果是,那么容易)这是否可以实现。

编辑:

我的意图是例如第一个例子被替换成这样的:

instance Num String where
    get = read :: String -> Int
    set = show :: Int    -> String
    a + b = set $ get a + get b
    a * b = set $ get a * get b
    ...

【问题讨论】:

  • get? set?我认为你在问一些与 Haskell 所谓的推导完全无关的东西。你的意思是自动类型转换,对吧?这不是一个好主意。它剥夺了您从双向类型推导的好处,总而言之,它避免了比一些显式转换函数可以合理引入的更多样板。 — 此外,这些是特别不好的例子,因为这些类型甚至不是近似同构的。对于get "bla" :: Intset (-1) :: Boolset [1,2,3] :: Maybe Int 之类的问题,您将如何处理?
  • 我认为应该可以在编译时用简单的实例声明替换声明,因此类型系统应该保持不变,尽管我同意这个提议与 Haskell 的派生机制完全无关;也许改名是为了。
  • 什么声明应该被什么替换?你能举例说明你想如何使用它吗?
  • 第一个示例的 get 函数无疑是部分的,我的意图是抛出模式匹配失败,但 get 和 set 永远不会暴露给用户,因此您的其他两个示例无关紧要。
  • 我还是不太明白你对这一切有什么需求。 — 您提议的instance Num String 很好地显示了另一个问题:如果这样的事情在用户看到的情况下发生,那么他们将无法控制中间类型。为什么会是Int?用户可能需要"3.14159" + "2.71828"。为什么会是Double?用户可能需要"10"^"800" - 1。为什么Integer,他们可能需要"4 :+ 1" / "15"。等等等等。

标签: haskell syntax types macros syntactic-sugar


【解决方案1】:

您在这里描述的本质上是通过同构定义类实例。这显然可以通过定义 getset 函数来实现,不过为了更清楚,我们称它们为 tofro

toSI   :: String -> Integer
fromSI :: Integer -> String

instance Num String where
  a + b = fromSI $ (toSI a) + (toSI b)
  abs   = fromSI . abs . toSI
  ...

这些确实是很容易编写的定义,并且通过自动提升类实例化超过(to, fro) 来减少样板文件可能有一些价值,但是这个系统必须仔细遵循许多规则,以免造成污染带有垃圾的全局类型类实例空间。

特别是,我们可能会要求(to, fro) 形成同构。这意味着双向的往返都是身份。换一种说法,这意味着给定任何整数n,我不应该以任何方式区分(froSI (toSI n))n(嗯,计算速度之类的一些事情被忽略了)。此外,对于 any 字符串 s,我必须具有相同的属性:toSI (froSI s) 必须与 n 无法区分。

第二个显然失败了,因为"I am not a number!" 在往返过程中抛出了一个错误。在纯代码中抛出错误很危险的原因有很多,而对于类型类,这种危险会继续存在并污染任何曾经导入您的代码的人的代码。

您可能会指出,这只是因为并非所有字符串都是有效数字。似乎fromSI . toSI 总是很麻烦,但toSI . fromSI 应该可以工作。也许它只会影响诸如实例化instance Num String 之类的事情,如果我们改为使用我们的(toSI, froSI) 对来为Integer 派生一些instanceString 就会处于有利位置。 也许吧。

让我们试试吧。 StringMonoid 的一个实例,看起来像这样

instance Monoid [a] where       -- if a ~ Char then this is String
  mempty = []
  mappend as bs = as ++ bs

如果我们通过mappend = toSI . mappend . fromSI 实现mappend,它会给我们“连接整数”之类的

(1 <> 2) <> 3   ==   123
1 <> (2 <> 3)   ==   123
(0 <> 1) <> 1   ==   11
0 <> (1 <> 1)   ==   11

如果我们小心地定义"" -&gt; 0 而不是让它失败,那么我们也可以得到一个有用的mempty

mempty = toSI mempty

这似乎应该运作良好。它确实是Integer 中的Monoid 继承自其与String 的“单向同构”(想想为什么我必须在这里使用Integer,而不是Int)。更具体地说,我们无法通过从 Monoid 类型类中的函数构建的任何测试来区分 toSI (fromSI n)n,因此进行此映射“足够好”。

但随后我们又遇到了另一个问题。 Integer 已经有一个 Monoid 实例。它已经有大约 10 个,最流行的是乘法和加法

instance Monoid Integer            instance Monoid Integer
  mempty  = 0                        mempty  = 1
  mappend = (+)                      mappend = (*)

因此,通过选择这些实例中的任何一个作为 Monoid 的 canonical 类型类实例,我们会丢失大量信息。更准确的说法是,Integer 通过其与String 的“单向同构”(又名“撤回”)变为Monoid,但也通过其剥离以仅具有加法,也可通过其剥离以仅具有乘法.

我们真的想保留这些信息,这就是为什么 Monoid 包定义了 SumProduct 这样的东西,这表明这个 Integer 已经专门使用它的 Addition 属性。

归根结底,这正是您将类型类实例提升到整个同构之上的问题。通常,类型有很多同构和缩回,可以以这种方式滥用,并且很难有真正规范的、合规的实例。当您找到一个时,将其显式写出来通常是值得的,即使您最终使用同构来这样做。

如果没有规范的选择,您可以使用 newtype 之类的工具和大量库来快速访问您的 newtype 层“下方”的内容,从常见的 GeneralizedNewtypeDeriving 扩展一直到 @ 987654321@ 或直接启发的Iso, au and auf 以及lens 的整个Wrapped, ala, alaf 机制。

基本上,这种机制已经到位,可以更轻松地丰富地讨论通过各种同构继承的实例,尤其是那些由newtype 诱导的实例。

【讨论】:

    猜你喜欢
    • 2016-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    • 2021-12-26
    • 2013-08-19
    相关资源
    最近更新 更多