【问题标题】:How to create instances for phantom types returning phantom type?如何为返回幻像类型的幻像类型创建实例?
【发布时间】:2014-12-23 11:42:50
【问题描述】:

让我们有以下数据类型:

data Foo1 a = Foo1
data Foo2 a = Foo2 (Foo3 a)
data Foo3 a = C1 (Foo1 a) | C2 Int

现在我们希望能够从 Foo1 或 Int 中获取 Foo3。 一个解决方案可能是使用类型类:

class ToFoo3 a where
    toFoo3 :: a -> Foo3 b -- Here start the problems with this phantom type b...

instance ToFoo3 (Foo1 b) where
    toFoo3 foo1 = C1 foo1

instance ToFoo3 Int where
    toFoo3 int = C2 int

这里编译器抱怨(正确!)它无法将 b 与 b1 匹配,因为类定义中 Foo3 的“b”与实例中 Foo1 的“b”不同。

有没有办法解决这个问题?

【问题讨论】:

  • 我尝试使用多参数类型类和函数依赖来解决它。但是,我被 Int 实例卡住了,因为它没有幻像类型,因此类型类的第二个参数是未定义的。

标签: haskell typeclass phantom-types


【解决方案1】:

经过多次尝试和失败,我终于得到了一个满意的答案! 诀窍是将函数依赖与 Int 类型的类型同义词一起使用。

{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses  #-}

data Foo1 a = Foo1
data Foo2 a = Foo2 (Foo3 a)
data Foo3 a = C1 (Foo1 a) | C2 (PhInt a)
data Foo4 = Foo4
type PhInt a = Int -- We use now a PhInt type instead of a type.

class ToFoo3 a b | a -> b where
    toFoo3 :: a -> b

instance ToFoo3 (Foo1 a) (Foo3 a) where
    toFoo3 foo1 = C1 foo1

-- The PhInt type allows us to specify that Foo3 must be generic as is
-- PhInt a.
instance ToFoo3 (PhInt a) (Foo3 a) where
    toFoo3 int = C2 int

test1 = toFoo3 Foo1
test2 = toFoo3 (3::Int)
test3 = toFoo3 (Foo1 :: Foo1 Foo4)

{-
This trick allows us to write a function which can take benefit of the
type class. The important point is that if you would try to do this
without having the "PhInt a" type instead of "Int", when using an integer
you would get as final result a value of type Foo3 Int.
-}
coerce :: ToFoo3 a (Foo3 b) => a -> (Foo3 b, String)
coerce a = (toFoo3 a, "hello")

注意:所有这些复杂性都在这里,因为实例必须将“非幻像”类型 Int 转换为幻像类型。如果我们只处理幻像类型,我们可以做一些更简单的事情,例如:

class ToFoo3 a (Foo3 b) where
    toFoo3 :: a b -> Foo3 b

instance ToFoo3 Foo1 Foo3 where
...

【讨论】:

  • 这绝对有效,但我完全不知道这是偶然还是有什么好的理由。因此,如果您对此有任何看法,欢迎您!
【解决方案2】:

我正在回答我自己的问题,因为我从 GHC 7.8.1 中找到了一个最近可用的解决方案,即使用 Coercible 类中的函数 coerce。

它具有以下优点:

  1. 编写的代码要少得多;
  2. 它并不暗示类型签名中有任何其他类型;
  3. 它是“安全的”(反对 unsafeCoerce,这在这种情况下也可能是一种解决方案);
  4. 它的运行时间成本为零。

文档可在此处获得: https://www.haskell.org/haskellwiki/GHC/Coercible

更多细节可以在出版物中找到: http://www.cis.upenn.edu/~eir/papers/2014/coercible/coercible.pdf

请注意,幻像类型的强制是强制明确解决的问题(参见出版物的第 2.2 段)。

在目前的情况下,它只需要一次调用强制函数就可以了!

-- We need to import Data.Coerce (no extensions are required).
import Data.Coerce

data Foo1 a = Foo1
data Foo2 a = Foo2 (Foo3 a)
data Foo3 a = C1 (Foo1 a) | C2 Int

class ToFoo3 a where
    toFoo3 :: a -> Foo3 b

{-|
We just need to apply the coerce function to the returned value.
Note: we could simplify this equation by adopting point free style:
> toFoo3 = coerce.C1
-}
instance ToFoo3 (Foo1 b) where
    toFoo3 foo1 = coerce $ C1 foo1

instance ToFoo3 Int where
    toFoo3 int = C2 int

我们现在可以运行一些测试(使用问题中显示的代码无法编译):

test1 = toFoo3 Foo1
test2 = toFoo3 (3::Int)

【讨论】:

  • 这个解决方案的问题是,在任何情况下,函数toFoo3处理的值的类型都会再次变为泛型。因此,如果您有一个类型 data Foo4 = Foo4,则以下等式 test3 = toFoo3 (Foo1 :: Foo1 Foo4) 将具有 Foo3 a 类型。在某些情况下这可能是可以接受的,但大多数时候,如果您使用幻像类型,您希望非泛型类型保持不变。
【解决方案3】:

哇,其他两种方法很复杂。

简单的解决方案是记住这些是 phantom 类型,您可以根据需要重新构建它们。例如,如果您有data Phantom x y = Phantom x,则存在cast (Phantom x) = Phantom x 类型的cast :: Phantom x y -> Phantom x z 函数,它使幻像类型再次泛型。做法是:

  1. 将对象解构为其非幻像参数。
  2. 重建对象。
  3. 利润。

在这种情况下,整个解决方案很简单:

instance ToFoo3 (Foo1 b) where
    toFoo3 _ = C1 Foo1

Foo2Foo3 也是如此,这是接下来的逻辑步骤:

instance ToFoo3 (Foo3 a) where
    toFoo3 (C1 x) = C1 Foo1
    toFoo3 (C2 i) = C2 i

instance ToFoo3 (Foo2 a) where
    toFoo3 (Foo2 x) = toFoo3 x

【讨论】:

  • 如此简单,我什至没有想到...非常优雅的解决方案。我能看到的唯一不方便的是,在复杂数据类型的情况下,需要很多类和实例(例如,如果 Int 将用于主要类型的不同类型部分)。但非常强大的方面是您不需要任何非常特殊的扩展,并且它使代码保持简单!
【解决方案4】:

我不能 100% 确定这是否是您想要的, 但是你可以让编译器接受你尝试过的东西 使用type families:

{-# LANGUAGE TypeFamilies #-}

module Stackoverflow where

data Foo1 a = Foo1
data Foo2 a = Foo2 (Foo3 a)
data Foo3 a = C1 (Foo1 a) | C2 Int

class ToFoo3 a where
    type T a :: *
    toFoo3 :: a -> Foo3 (T a)

instance ToFoo3 (Foo1 b) where
    type T (Foo1 b) = b
    toFoo3 foo1 = C1 foo1

instance ToFoo3 Int where
    type T Int = Int
    toFoo3 int = C2 int

如果你想通用从整数中得到Foo3,你可以添加另一个newtype/ToFoo3-instance:

newtype AInt a = AInt Int

instance ToFoo3 (AInt a) where
  type T (AInt a) = a
  toFoo3 (AInt int) = C2 int

这是一个简单的测试:

λ> :t toFoo3 (AInt 5) :: Foo3 Char
toFoo3 (AInt 5) :: Foo3 Char :: Foo3 Char

如果您好奇 - 使用 Int 的错误会如下所示:

λ> :t toFoo3 (5 :: Int) :: Foo3 Char

<interactive>:1:1:
    Couldn't match type `Int' with `Char'
    Expected type: Foo3 Char
      Actual type: Foo3 (T Int)
    In the expression: toFoo3 (5 :: Int) :: Foo3 Char

【讨论】:

  • 我怀疑这不是我们想要的,因为它无法将Int 转换为任意Foo3 a,即使所有Foo3 a 都有C2 构造函数。
  • @ØrjanJohansen 可能是真的 - 没有想到这一点 - 也许我可以通过更多的技巧rescue - 你已经使用多参数类型类提供了解决方案;)
  • 感谢您的修改和修改!这确实非常好。然后,使用起来并不总是那么容易,因为在某些情况下,您可能会看到“T”类型出现在函数签名中。但是,它在限制复杂类型的代码量方面具有很大的优势!
【解决方案5】:

一个多参数类型类没有一个函数依赖为我编译:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}

data Foo1 a = Foo1
data Foo2 a = Foo2 (Foo3 a)
data Foo3 a = C1 (Foo1 a) | C2 Int

class ToFoo3 a b where
    toFoo3 :: a -> Foo3 b

instance ToFoo3 (Foo1 b) b where
    toFoo3 foo1 = C1 foo1

instance ToFoo3 Int b where
    toFoo3 int = C2 int

按照我的理解,你不能在任何一个方向上都有功能依赖,因为Int 需要能够转换为任何Foo3 a 类型,而Foo1 a 也需要能够转换为相同的Foo3 a 类型。

当然这意味着你不能指望toFoo3 的任何参数或结果类型来帮助推断另一个,所以你有时可能需要大量的类型注释来使用它,但除此之外这应该可以工作.

编辑:我假设您希望能够在 ab 不同的情况下从 Foo1 a 转换为 Foo3 b。如果我错了,那么如果您将一个实例更改为

instance ToFoo3 (Foo1 b) where
    toFoo3 Foo1 = C1 Foo1

【讨论】:

  • 正如您所指出的,此解决方案涉及在使用 toFoo3 函数时对编译器的烦人提示:特别是如果您保留“Foo1 a”的幻像类型通用。所以,具体你不能写:test = toFoo3 Foo1.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多