如何为幻像类型创建实例返回幻像类型?

时间:2014-10-28 06:24:28

标签: haskell typeclass phantom-types

我们有以下数据类型:

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不同。

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

5 个答案:

答案 0 :(得分:6)

不带函数依赖项的多参数类型类为我编译:

{-# 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的任何参数或结果类型有助于推断另一个,所以你有时可能需要一些恼人的类型注释来使用它,但除此之外应该工作。

编辑:我假设您希望能够使用Foo1 a和{{1}从Foo3 b转换为a不同的。如果我错了,那么如果将一个实例更改为

,那么带有单参数类的OP代码应该可以正常工作
b

答案 1 :(得分:4)

哇,另外两种方法很复杂。

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

  1. 将对象解构为非幻象参数。
  2. 重建对象。
  3. 利润。
  4. 在这种情况下,整个解决方案就像:

    一样简单
    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
    

答案 2 :(得分:3)

我不是百分百确定这是否是你想要的, 但你可以让编译器接受你尝试过的东西 使用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 - 实例:

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

答案 3 :(得分:1)

我正在回答我自己的问题,因为我找到了GHC 7.8.1提供的最新解决方案,即强制类强制函数的使用。

它具有以下优点:

  1. 编写的代码要少得多;
  2. 它并不暗示类型签名中的任何其他类型;
  3. 它是“安全的”(反对不安全的Coerce,在这种情况下也可以解决方案);
  4. 它的运行时间成本为零。
  5. 文档可在此处获得: 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)
    

答案 4 :(得分: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")

注意:所有这些复杂情况都在这里,因为实例必须转换&#34;非幻像&#34;键入Int到幻像类型。如果我们只处理幻像类型,我们可以做一些更简单的事情,例如:

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

instance ToFoo3 Foo1 Foo3 where
...