简体   繁体   English

GADT 类型变量中的联合

[英]Union in GADT type variable

I have a GADT constructor representing the same idea, but I need two of them because the type is flexible depending on context.我有一个表示相同想法的 GADT 构造函数,但我需要其中两个,因为类型根据上下文灵活。 See this contrived example:请参阅这个人为的示例:

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

AFoo and BFoo represent the same underlying data concept: some data with a string. AFooBFoo代表相同的底层数据概念:一些带有字符串的数据。 But in this context, it's more than that.但在这种情况下,它不止于此。 Ideally AFoo and BFoo would be merged into Foo , because they represent exactly the same thing, but I need one that types like Thing A and one that types like Thing B .理想情况下, AFooBFoo将合并到Foo ,因为它们代表完全相同的东西,但我需要一个类型像Thing A类型和一个类型像Thing B类型。 As mentioned before, some contexts in which a Foo can be used require a Thing A and some require a Thing B , so to satisfy the type system one constructor needs to exist for each.如前所述,一些可以使用Foo上下文需要一个Thing A而一些需要一个Thing B ,所以为了满足类型系统,每个都需要一个构造函数。

I could of course write a function that "casts" to the desired type by switching the constructor if needed:如果需要,我当然可以通过切换构造函数来编写一个“强制转换”为所需类型的函数:

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

(And similarly for fooAsThingB ) (对于fooAsThingB也是fooAsThingB

But this is ugly, because it requires a Maybe that I have to propagate because not all Thing B s can become Thing A s (in fact, only BFoo can).但这很丑陋,因为它需要一个我必须传播的Maybe ,因为并非所有的Thing B都可以成为Thing A (实际上,只有BFoo可以)。

Ideally, I'd like to write:理想情况下,我想写:

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

But I'm unclear on what I'd put in place of the ??但我不清楚我用什么来代替?? . . Presumably it's some way of representing the union of A and B (and perhaps this requires changing the types of the other constructors for this GADT, but that's fine).大概它是表示AB联合的某种方式(也许这需要更改此 GADT 的其他构造函数的类型,但这很好)。

Just to be clear, if the above were valid, I'd expect given the following functions:需要明确的是,如果上述内容有效,我希望具有以下功能:

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

Then, I'd be able to do all of the following:然后,我将能够执行以下所有操作:

processThingA $ Foo "Hello"
processThingB $ Foo "World"

processThingA $ Bar 3.14
processThingB $ Baz 42

processThingC $ Bah '\n'

tl;dr In the first snippet, is it possible to merge AFoo and BFoo into a Foo that types as both Thing A and Thing B ? tl;dr 在第一个片段中,是否可以将AFooBFoo合并到一个类型为Thing AThing BFoo中?

edit: Note: there are other EmptyDataDecls than A and B that I use for Thing a .编辑:注意:除了AB ,还有其他EmptyDataDecls用于Thing a I added C as an example.我以C为例。

A possible solution is as follows, even I do not consider it elegant.一个可能的解决方案如下,即使我不认为它优雅。

We first define a GADT and a type class for being "A or B":我们首先为“A 或 B”定义一个 GADT 和一个类型类:

{-# 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

Then we exploit our type class in our type.然后我们在我们的类型中利用我们的类型类。 This will cause Foo to require some more space, since it needs to store the typeclass dictionary at runtime.这将导致Foo需要更多空间,因为它需要在运行时存储类型类字典。 It is a small amount of overhead, but it is still unfortunate.这是少量的开销,但仍然很不幸。 On the other hand, this will also make it possible to discern, at runtime, whether the s used in Foo is actually A or B .另一方面,这也可以在运行时辨别Foo使用的s实际上是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

Some tests:一些测试:

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

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

A main downside is that we need to convince the exhaustiveness checker that our pattern matching is OK.一个主要的缺点是我们需要让穷举检查器相信我们的模式匹配是可以的。

-- 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'

This technique does not scale very well.这种技术不能很好地扩展。 We need a custom GADT and typeclass for any constructor of Thing which can generate any tag in some "union".我们需要一个自定义的 GADT 和类型类,用于Thing任何构造函数,它可以在某个“联合”中生成任何标记。 This can still be OK.这仍然可以。 To check exhaustiveness, we would need to exploit all these classes.为了检查穷举性,我们需要利用所有这些类。

I think this should require a linear amount of boilerplate, but it still significant.我认为这应该需要线性量的样板,但它仍然很重要。

Perhaps using singletons is simpler, in the general case, to store the value of s in the constructor Foo , and avoid all the custom GADTs and typeclasses.也许使用单例更简单,在一般情况下,将s的值存储在构造函数Foo ,并避免所有自定义 GADT 和类型类。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM