为什么类型系统拒绝我看似有效的程序?

Why is the type system refusing my seemingly valid program?

注意这个程序:

class Convert a b where
    convert :: a -> b

data A = A deriving Show
data B = B deriving Show
data C = C deriving Show
data D = DA A | DB B | DC C deriving Show

instance Convert A A where convert A = A
instance Convert A B where convert A = B
instance Convert A C where convert A = C
instance Convert B A where convert B = A
instance Convert B B where convert B = B
instance Convert B C where convert B = C
-- There is no way to convert from C to A
instance Convert C B where convert C = B
instance Convert C C where convert C = C

get (DA x) = convert x
get (DB x) = convert x
get (DC x) = convert x

main = do
    print (get (DC C) :: B) -- Up to this line, code compiles fine.
    print (get (DB B) :: A) -- Add this line and it doesn't, regardless of having a way to convert from B to A!

存在从 C 转换为 B 以及从 B 转换为 A 的实例。然而,GHC 对前者进行类型检查,但对后者失败。经检查,似乎无法为 get 推断出足够通用的类型:

get :: (Convert A b, Convert B b, Convert C b) => D -> b

我想表达的是:get :: (Convert a_contained_by_D b) => D -> b,貌似不可能。有没有什么方法可以实现和编译一个函数来完成我的 get 尝试做的事情,而不改变其余的设置?

要使该代码正常工作,它必须与任何相同类型的参数一起工作。也就是说,如果

get (DB B) :: A

然后工作

get anyValueOfTypeD :: A

必须工作,包括

get (DC C) :: A

由于缺少从 C 到 A 的实例,无法工作。

第一行

get anyValueOfTypeD :: B

之所以有效,是因为我们确实拥有将 A、B、C 转换为 B 的所有三个实例。

我认为没有任何变通方法可以让您保留类型 D 的原样。相反,如果您可以更改它,则可以使用例如

data D a = DA a | DB a | DC a

(注意它与原来的有很大不同),甚至是 GADT

data D x where
  DA :: A -> D A
  DB :: B -> D B
  DC :: C -> D C

如果你的程序真的对你来说似乎有效,那么你就可以在[=118=中编写完成你想要的工作的get类型],不是手波。让我来帮助您改善您的手波并揭开您要求棍子上的月亮的原因。

What I want to express is: get :: (Convert a_contained_by_D b) => D -> b, which seems impossible.

如前所述,这并不像您需要的那样精确。确实,这就是 Haskell 现在给你的,

get :: (Convert A b, Convert B b, Convert C b) => D -> b

任何可以被 D 包含的 a 都需要一次一个,才能转换为 b。这就是为什么你得到 classic 系统管理员逻辑:不允许获得 D 除非他们都可以 b.

问题是您需要知道的状态不是 any old D 中可能包含的类型,而是特定中包含的类型D 您收到的输入。正确的?你要

print (get (DB B) :: A)  -- this to work
print (get (DC C) :: A)  -- this to fail

但是 DB BDC C 只是 D 的两个不同元素,就 Haskell 类型系统而言,每个类型 [=111] =]一切不同都是一样的。如果你想区分 D 的元素,那么你需要一个 D-pendent 类型。这是我用 handwave 写的。

DInner :: D -> *
DInner (DA a) = A
DInner (DB b) = B
DInner (DC c) = C

get :: forall x. pi (d :: D) -> (Convert (DInner d) x) => x
get (DA x) = convert x
get (DB x) = convert x
get (DC x) = convert x

其中 pi 是在 运行 时间传递的数据的绑定形式(与 forall 不同),但可能依赖于哪些类型(与 -> 不同)。现在约束不是在谈论任意 D 而是你手中的 d :: D,并且约束可以通过检查其 DInner.

来准确计算所需的内容

除了我的 pi.

什么都可以让它消失

遗憾的是,虽然pi正在从天而降,但它还没有落地。 None越少,不像月亮,用棍子就能够到。毫无疑问你会抱怨我正在更改设置,但实际上我只是将你的程序从大约 2017 年的 Haskell 翻译到 2015 年的 Haskell。你会 get 它回来,一个那天,我用手挥过的那种类型。

没有什么可以说的,但是可以唱歌

第 1 步。打开 DataKindsKindSignatures 并为您的类型构建单例(或让 Richard Eisenberg 为您完成)。

data A = A deriving Show
data Aey :: A -> * where  -- think of "-ey" as an adjectival suffix
  Aey :: Aey 'A           -- as in "tomatoey"

data B = B deriving Show
data Bey :: B -> * where
  Bey :: Bey 'B

data C = C deriving Show
data Cey :: C -> * where
  Cey :: Cey 'C

data D = DA A | DB B | DC C deriving Show
data Dey :: D -> * where
  DAey :: Aey a -> Dey (DA a)
  DBey :: Bey b -> Dey (DB b)
  DCey :: Cey c -> Dey (DC c)

想法是 (i) 数据类型成为种类,以及 (ii) 单例表征具有 运行 时间表示的类型级数据。因此,类型级别 DA a 存在于 运行 时间,前提是 a 存在,等等

第 2 步。猜猜谁会来 DInner。打开 TypeFamilies.

type family DInner (d :: D) :: * where
  DInner (DA a) = A
  DInner (DB b) = B
  DInner (DC c) = C

第 3 步。给你一些 RankNTypes,现在你可以写

get :: forall x. forall d. Dey d -> (Convert (DInner d) x) => x
--               ^^^^^^^^^^^^^^^^^^
-- this is a plausible fake of pi (d :: D) ->

第 4 步。尝试写 get 并搞砸了。我们必须在 运行 时间证据上匹配类型级别 d 是可表示的。我们需要它来获得专门用于计算 DInner 的类型级别 d。如果我们有适当的 pi,我们可以匹配具有双重职责的 D 值,但现在,匹配 Dey d

get (DAey x) = convert x   -- have x :: Aey a, need x :: A
get (DBey x) = convert x   -- and so on
get (DCey x) = convert x   -- and so forth

令人抓狂的是,我们的 xes 现在是单例,为了 convert,我们需要基础数据。我们需要更多的单例设备。

第5步。引入并实例化单例class,以"demote"类型级别值(只要我们知道它们的运行时间代表)。同样,Richard Eisenberg 的 singletons 库可以从中提取模板-Haskell 样板,但让我们看看发生了什么

class Sing (s :: k -> *) where   -- s is the singleton family for some k
  type Sung s :: *               -- Sung s is the type-level version of k
  sung :: s x -> Sung s          -- sung is the demotion function

instance Sing Aey where
  type Sung Aey = A
  sung Aey = A

instance Sing Bey where
  type Sung Bey = B
  sung Bey = B

instance Sing Cey where
  type Sung Cey = C
  sung Cey = C

instance Sing Dey where
  type Sung Dey = D
  sung (DAey aey) = DA (sung aey)
  sung (DBey bey) = DB (sung bey)
  sung (DCey cey) = DC (sung cey)

第 6 步。执行。

get :: forall x. forall d. Dey d -> (Convert (DInner d) x) => x
get (DAey x) = convert (sung x)
get (DBey x) = convert (sung x)
get (DCey x) = convert (sung x)

请放心,当我们拥有适当的 pi 时,那些 DAey 将是实际的 DA 而那些 x 将不再需要 sung。我的 get 手波类型是 Haskell,你的 get 代码就可以了。但与此同时

main = do
  print (get (DCey Cey) :: B)
  print (get (DBey Bey) :: A)

类型检查很好。也就是说,您的程序(加上 DInnerget 的正确类型)似乎是有效的 Dependent Haskell,我们快到了。