假设我有一种货币类型:
data Currency = USD | EUR | YEN
和存储int的Money类型,并由给定的Currency参数化(Currency被提升为具有DataKinds扩展名的种类)。
data Money :: Currency -> * where
Money :: Int -> Money c
是否可以编写一个函数moneyOf
,该函数将其作为参数,一个货币值,并返回一个Money值,该值由相应类型的Currency值参数化?例如moneyOf :: Currency -> Money c
,但我们得到编译时保证c
是从Currency值生成的类型?
答案 0 :(得分:7)
不,但有一些解决方法。如您所见,您需要编写的类型类似于moneyOf :: (c :: Currency) -> Int -> Money c
,其中c
在函数实现本身的类型和中都绑定了{{1} }})。这不是我们在Haskell中可以做的事情。那么我们可以做些什么呢?有两种选择,取决于你真正想要多少。
选项1:代理。定义多边形类型
moneyOf _ amt = Money amt
这种类型背后的想法是,您可以使用data Proxy (t :: k) = Proxy
作为传递类型t的具体术语级表示的方法。因此,例如,我们可以定义:
Proxy :: Proxy 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
,不起作用,是 - 当你可以使用它们时更简单!)
选项2:单例类型。但是,如果您发现需要更多通用性(或者您只是好奇),那么另一种方法是创建单例类型喜欢以下内容:
Money 10 :: Money USD
这称为“单例类型”,因为每个data SingCurrency (c :: Currency) where
SUSD :: SingCurrency USD
SEUR :: SingCurrency EUR
SYEN :: SingCurrency YEN
只有一个成员(例如,SingCurrency c
是SUSD
类型的唯一值)。现在,你可以写
SingCurrency USD
此处,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
的实例,但是如果需要,则需要明确指定约束.²)我想不出任何使用它的好例子我的头;我认为我的建议是只有当你发现自己处于一种你无法完成所需要的情况时才会使用单身人士,并意识到单身人士的额外类型 - 价值同步会对你有帮助 (并且你不能在这种情况下重新设计自己)。
如果你确实发现自己使用单身人士,那么机器就会为你in the singletons
package设置,更为一般:a data family Sing :: k -> *
代替SingCurrencyI
;并且a type class SingI :: k -> Constraint
取代SingCurrency
,其中包含SingCurrencyI
个成员。还有sing :: SingI a => Sing a
允许您自由地从Sing n
转换为SingI n
(另一个方向只是sing
)。 (这些都在a function withSingI :: Sing n -> (SingI n => r) -> r
中提供。)Data.Singletons
中还有一些允许你编写的模板Haskell
singletons [d|data Currency = USD | EUR | YEN|]
位于程序的顶层,以便定义Currency
类型以及相应的Sing
和SingI
个实例。 (您还需要启用以下语言扩展程序:KindSignatures
,DataKinds
,TypeFamilies
,GADTs
或ExistentialQuantification
,ScopedTypeVariables
和{{ 1}}。)
这真的很强大 - 它几乎像依赖类型,如果你眯眼 - 但它可能是一个巨大的痛苦使用。事实上,如果你想了解更多信息,那么有一篇论文正是在谈论这个问题:Data.Singletons.TH
,Sam Lindley和"Hasochism: The Pleasure and Pain of Dependently Typed Haskell Programming"。对于那些已经考虑过这些想法的人来说,这绝对是可读的,尽管这些材料本身就很棘手;但请注意,他们的符号略有不同。不幸的是,我不知道任何好的博客文章或教程式的介绍。
¹我不确定类型系列的类型统一规则的状态,尽管......。
²否则,将不会传入包含TemplateHaskell
的运行时字典,因此该值在运行时将不可用。
答案 1 :(得分:1)
Antal S-Z选项2的替代方案如下:
您保留Currency
单身人士SingCurrency
。
data SingCurrency (c :: Currency) where
SEUR :: SingCurrency EUR
SUSD :: SingCurrency USD
SYEN :: SingCurrency YEN
但是,您可以使用存在的GADT,而不是使用单例实例类(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