GADT类型变量

时间:2018-04-28 09:58:20

标签: haskell gadt

我有一个GADT构造函数表示相同的想法,但我需要其中两个,因为类型是灵活的,具体取决于上下文。看到这个人为的例子:

data A
data B
data C

data Thing a where
  AFoo :: String -> Thing A
  Bar  :: Float -> Thing A
  BFoo :: String -> Thing B
  Baz  :: Int -> Thing B
  Bah  :: Char -> Thing C

AFooBFoo表示相同的基础数据概念:一些带字符串的数据。但在这种情况下,它不止于此。理想情况下,AFooBFoo会合并到Foo,因为它们代表完全相同的东西,但我需要一个类似Thing A的类型和一个类似{{1}的类型}}。如前所述,可以使用Thing B的某些上下文需要Foo,而某些上下文需要Thing A,因此为了满足类型系统,每个需要存在一个构造函数。

我当然可以通过在需要时切换构造函数来编写一个“强制转换”到所需类型的函数:

Thing B

(同样适用于fooAsThingA :: Thing a -> Maybe (Thing A) fooAsThingA t@(AFoo _) = Just t fooAsThingA (BFoo s) = Just $ AFoo s fooAsThingA _ = Nothing

但这很丑陋,因为它需要fooAsThingB我必须传播,因为并非所有Maybe都可以成为Thing B s(事实上,只有Thing A可以)。

理想情况下,我想写一下:

BFoo

但我不清楚我代替data A data B data C data Thing a where Foo :: String -> Thing ?? Bar :: Float -> Thing A Baz :: Int -> Thing B Bah :: Char -> Thing C 的内容。据推测,它是表示??A的并集的某种方式(也许这需要更改此GADT的其他构造函数的类型,但这很好)。

为了清楚起见,如果以上内容有效,我希望给出以下功能:

B

然后,我将能够完成以下所有操作:

processThingA :: Thing A -> String
processThingB :: Thing B -> Int
processThingC :: Thing C -> Float

tl; dr在第一个代码段中,是否可以将processThingA $ Foo "Hello" processThingB $ Foo "World" processThingA $ Bar 3.14 processThingB $ Baz 42 processThingC $ Bah '\n' AFoo合并到BFooFoo类型的Thing A中?

修改:注意:Thing B还有EmptyDataDecls,而不是AB。我添加了Thing a作为示例。

1 个答案:

答案 0 :(得分:2)

可能的解决方案如下,即使我不认为它很优雅。

我们首先定义GADT和类型类为" A或B":

{-# LANGUAGE DataKinds, KindSignatures, GADTs, EmptyCase,
             ScopedTypeVariables #-}
{-# OPTIONS -Wall #-}

data Sort = A | B | C | D

data AorB (s :: Sort) where
  IsA :: AorB 'A
  IsB :: AorB 'B

class IsAorB (s :: Sort) where
  aorb :: AorB s

instance IsAorB 'A where aorb = IsA
instance IsAorB 'B where aorb = IsB

然后我们在我们的类型中利用我们的类型。这将导致Foo需要更多空间,因为它需要在运行时存储类型类字典。这是一个很小的开销,但它仍然是不幸的。另一方面,这也可以在运行时识别s中使用的Foo实际上是A还是B

data Thing (s :: Sort) where
  Foo :: IsAorB s => String -> Thing s 
  Bar :: Float -> Thing 'A
  Baz :: Int -> Thing 'B
  Bah :: Char -> Thing 'C

一些测试:

processThingA :: Thing 'A -> String
processThingA (Foo x) = x
processThingA (Bar x) = show x

processThingB :: Thing 'B -> Int
processThingB (Foo _) = 0
processThingB (Baz i) = i

一个主要的缺点是我们需要说服穷举检查我们的模式匹配是否正常。

-- This unfortunately will give a spurious warning
processThingC :: Thing 'C -> Float
processThingC (Bah _) = 42.2
-- To silence the warning we need to prove that this is indeed impossible
processThingC (Foo _) = case aorb :: AorB 'C of {}

processThingAorB :: forall s. IsAorB s => Thing s -> String
processThingAorB (Foo x) = x
processThingAorB (Bar x) = "Bar " ++ show x
processThingAorB (Baz x) = "Baz " ++ show x
-- Again, to silence the warnings
processThingAorB (Bah _) = case aorb :: AorB 'C of {}

test :: ()
test = ()
  where
  _ = processThingA $ Foo "Hello"
  _ = processThingB $ Foo "World"
  _ = processThingA $ Bar 3.14
  _ = processThingB $ Baz 42
  _ = processThingC $ Bah '\n'

这种技术不能很好地扩展。对于Thing的任何构造函数,我们需要一个自定义的GADT和类型类,它可以在某些" union"中生成任何标记。这仍然可以。要检查详尽性,我们需要利用所有这些类。

我认为这应该需要一定量的样板,但它仍然很重要。

在一般情况下,使用单例更简单,可以将s的值存储在构造函数Foo中,并避免使用所有自定义GADT和类型类。