我可以使用 DataKinds 编写一个 returns 由参数编码的类型值的函数吗?

Can I write a function using DataKinds that returns a value of type encoded by the parameter?

假设我有一个货币类型:

data Currency = USD | EUR | YEN

和存储 int 的 Money 类型,并由给定的 Currency 参数化(Currency 被提升为具有 DataKinds 扩展名的种类)。

data Money :: Currency -> * where
    Money :: Int -> Money c

是否可以编写一个函数,moneyOf,将货币值作为参数,returns货币值由货币值的相应类型参数化?比如moneyOf :: Currency -> Money c,但是我们得到一个编译时保证c是从Currency值生成的类型?

否,但有解决方法。如您所见,您需要编写的类型类似于 moneyOf :: (c :: Currency) -> Int -> Money c,其中 c 在函数实现本身中绑定在类型 中( moneyOf _ amt = Money amt)。这不是我们在 Haskell 中可以做的事情。那么我们可以做什么呢?有两种选择,取决于你真正想要多少。

选项 1:代理。定义多类型

data Proxy (t :: k) = Proxy

这种类型背后的想法是,您可以使用 Proxy :: Proxy t 作为一种传递类型 t 的具体化术语级表示的方式。因此,例如,我们可以定义:

moneyOf :: Proxy c -> Int -> Money c
moneyOf _ = Money

然后,我们可以像moneyOf (Proxy :: Proxy USD) 10一样调用它来得到Money 10 :: Money USD。您可以使用的一个技巧是将函数类型改为 proxy k -> Int -> Money c(注意小写 proxy!),这样 proxy 将与任意函数类型统一。¹这对将参数传递给函数以修复它们的 return 类型,但除此之外它并不能真正让你做任何事情。

正如您描述的问题,我认为代理可能最适合解决它。(假设普通类型签名,如 Money 10 :: Money USD,不'工作,也就是说 – 当您可以使用它们时,它们甚至更简单!)

选项 2:单例类型。 但是,如果您发现需要更多通用性(或者您只是好奇),那么另一种方法是创建一个 单例类型 如下所示:

data SingCurrency (c :: Currency) where
  SUSD :: SingCurrency USD
  SEUR :: SingCurrency EUR
  SYEN :: SingCurrency YEN

这称为"singleton type",因为每个SingCurrency c只有一个成员(例如SUSD是类型[的唯一值=28=]).现在,你可以写

moneyOf :: SingCurrency c -> Int -> Money c
moneyOf _ = Money

此处,moneyOf SUSD 10 的计算结果为 Money 10 :: Money USD。但是,仅此一项并不能为您带来除使用之外的任何东西(除了少打字)。当你想生产单身人士时,他们会变得特别有趣:

class SingCurrencyI (c :: Currency) where
  sing :: SingCurrency c

instance SingCurrencyI USD where scur = SUSD
instance SingCurrencyI EUR where scur = SEUR
instance SingCurrencyI YEN where scur = SYEN

现在,如果你有一个SingCurrencyI c约束,你可以自动产生相应的SingCurrency c值和sing,从而允许你从类型级别移动到术语级别。 (请注意,尽管所有 Currency 都是 SingCurrencyI 的实例,但如果需要,您需要明确指定约束。²)我想不出任何使用它的好例子我的头;我认为我的建议是 只有当你发现自己处于一种你无法完成你需要的情况并且意识到单例的额外类型值同步会帮助你时才使用单例(以及你无法重新设计自己的情况)。

如果您确实发现自己在使用单例,那么机器已经为您设置好了 in the singletons package, in more generality: there's a data family Sing :: k -> * that takes the place of SingCurrency; and there's have a type class SingI :: k -> Constraint that takes the place of SingCurrencyI, which has the single member sing :: SingI a => Sing a. There's also a function withSingI :: Sing n -> (SingI n => r) -> r which allows you to convert freely from Sing n into SingI n (the other direction is just sing). (These are all provided in Data.Singletons.) There's also some Template Haskell in Data.Singletons.TH 允许您编写

singletons [d|data Currency = USD | EUR | YEN|]

在程序的顶层,以便定义 Currency 类型以及适当的 SingSingI 实例。 (您还需要启用以下语言扩展:KindSignaturesDataKindsTypeFamiliesGADTsExistentialQuantificationScopedTypeVariablesTemplateHaskell.)

这真的很强大——它几乎就像依赖类型,如果你眯着眼睛——但使用起来可能会很痛苦。事实上,如果您想了解更多信息,有一篇论文正是在讨论这个问题:"Hasochism: The Pleasure and Pain of Dependently Typed Haskell Programming", by Sam Lindley and Conor McBride。任何已经在考虑这些想法的人都可以阅读它,尽管 material 本质上有点棘手;不过请注意,它们的表示法略有不同。不幸的是,我不知道有任何好的博文或教程式的介绍这方面的内容。


¹ 我不确定类型族的类型统一规则的状态,但是......

² 否则,包含 sing 的 运行 时间字典将不会被传入,因此该值在 运行 时间将不可用。

Antal S-Z 答案的选项 2 的替代方案如下。

您保留 Currency 个单身人士 SingCurrency

data SingCurrency (c :: Currency) where
    SEUR :: SingCurrency EUR
    SUSD :: SingCurrency USD
    SYEN :: SingCurrency YEN

但是您可以使用现有的 GADT,而不是使用单例实例 class (SingCurrencyI)。

data AnyCurrency where
    AnyCurrency :: SingCurrency sc -> AnyCurrency

具有替代 SingCurrencyI 个实例的辅助功能。

anyCurrency :: Currency -> AnyCurrency
anyCurrency EUR = AnyCurrency SEUR
anyCurrency USD = AnyCurrency SUSD
anyCurrency YEN = AnyCurrency SYEN

使用

money :: SingCurrency c -> Int -> Money c
money = const Money

和存在主义Money

data AnyMoney where
    AnyMoney :: Money c -> AnyMoney

你可以实现

moneyOf :: Currency -> Int -> AnyMoney
moneyOf c v = case anyCurrency c of
                AnyCurrency sc -> AnyMoney $ money sc v

AnyMoney 上的模式匹配将允许您使用带有类型 Money c 参数的函数,即

useMoney :: Money c -> IO ()
useMoney = undefined

最终会得到你

useUseMoney :: Currency -> Int -> IO ()
useUseMoney c v = case moneyOf c v of
                    AnyMoney m -> useMoney m