在某些语言中(例如,#racket / typed),程序员可以指定联合类型而又不歧视它,例如,该类型(U Integer String)
捕获整数和字符串,而无需(I Integer) (S String)
以某种data IntOrStringUnion = ...
形式或类似形式对其进行标记。有没有办法在Haskell中做同样的事情?
Either
是您想要的... ish。用Haskell术语,我将以匿名求和类型描述您要查找的内容。匿名,我的意思是它没有定义的名称(如带有data
声明的名称)。求和类型是指可以具有几种(可区分)类型之一的数据类型。带有标签的工会等。(如果您不熟悉此术语,请尝试使用Wikipedia作为入门。)
我们有一个众所周知的惯用匿名产品类型,它只是一个元组。如果要同时使用Int
a和a String
,只需用逗号将它们混在一起即可:(Int, String)
。元组(看似)可以永远持续下去(Int, String, Double, Word)
,并且您可以以相同的方式进行模式匹配。(有一个限制,但是没关系。)
众所周知的惯用匿名和类型为Either
,来自Data.Either
(和Prelude
):
data Either a b = Left a | Right b
deriving (Eq, Ord, Read, Show, Typeable)
它有一些缺点,最突出的是在这种情况下以一种奇怪的方式进行Functor
支持的实例Right
。问题在于,链接会带来很多尴尬:类型最终会像Either (Int (Either String (Either Double Word)))
。正如其他人指出的那样,模式匹配更加尴尬。
我只想指出,我们可以更接近(我理解是)Racket用例。通过我非常简短的Googling,在Racket中看起来像可以使用函数isNumber?
来确定联合类型的给定值中实际上是哪种类型。在Haskell中,我们通常使用案例分析(模式匹配)来进行此操作,但是,这样做很尴尬Either
,并且使用简单模式匹配的函数可能最终会硬连接到特定的并集类型。我们可以做得更好。
IsNumber?
我将编写一个我认为是Haskell惯用语言的函数isNumber?
。首先,我们不喜欢进行布尔测试,然后再运行假定其结果的函数;取而代之的是,我们倾向于只转换到Maybe
那里然后从那里去。因此,函数的类型将以结尾-> Maybe Int
。(Int
现在用作替身。)
但是箭头的左手边是什么?“可能是Int或String或我们放入联合中的任何其他类型的东西。” 嗯好吧 因此它将成为多种类型之一。这听起来像typeclass,所以我们将约束和类型变量放在箭头的左侧:MightBeInt a => a -> Maybe Int
。好的,让我们写出这个类:
class MightBeInt a where
isInt :: a -> Maybe Int
fromInt :: Int -> a
好的,现在我们如何编写实例?好吧,我们知道第一个参数是否Either
为Int
,我们是金色的,所以我们将其写出来。(顺便说一句,如果您想做一个很好的练习,请仅查看instance ... where
下面三个代码块的各个部分,并尝试自己实现该类成员。)
instance MightBeInt (Either Int b) where
isInt (Left i) = Just i
isInt _ = Nothing
fromInt = Left
美好的。同样,ifInt
是第二个参数:
instance MightBeInt (Either a Int) where
isInt (Right i) = Just i
isInt _ = Nothing
fromInt = Right
但是呢Either String (Either Bool Int)
?诀窍是递归右手类型:如果不是Int
,那它MightBeInt
本身就是实例吗?
instance MightBeInt b => MightBeInt (Either a b) where
isInt (Right xs) = isInt xs
isInt _ = Nothing
fromInt = Right . fromInt
(请注意,所有这些都需要FlexibleInstances
和OverlappingInstances
。)花了我很长时间才可以编写和阅读这些类实例。如果这种情况令人惊讶,请不要担心。最重要的是,我们现在可以执行以下操作:
anInt1 :: Either Int String
anInt1 = fromInt 1
anInt2 :: Either String (Either Int Double)
anInt2 = fromInt 2
anInt3 :: Either String Int
anInt3 = fromInt 3
notAnInt :: Either String Int
notAnInt = Left "notint"
ghci> isInt anInt3
Just 3
ghci> isInt notAnInt
Nothing
伟大的!
好的,但是现在我们需要为每个要查找的类型编写另一个类型类吗?没有!我们可以通过要查找的类型来对类进行参数化!这是一个很机械的翻译;唯一的问题是如何告诉编译器我们正在寻找什么类型,这就是要解决的地方Proxy
。(如果您不想安装tagged
或运行base
4.7,只需定义data Proxy a = Proxy
。这没什么特别的,但是您需要PolyKinds
。)
class MightBeA t a where
isA :: proxy t -> a -> Maybe t
fromA :: t -> a
instance MightBeA t t where
isA _ = Just
fromA = id
instance MightBeA t (Either t b) where
isA _ (Left i) = Just i
isA _ _ = Nothing
fromA = Left
instance MightBeA t b => MightBeA t (Either a b) where
isA p (Right xs) = isA p xs
isA _ _ = Nothing
fromA = Right . fromA
ghci> isA (Proxy :: Proxy Int) anInt3
Just 3
ghci> isA (Proxy :: Proxy String) notAnInt
Just "notint"
现在的可用性情况是……更好。顺便说一句,我们丢失的主要内容是穷举性检查器。
(U String Int Double)
为了好玩,我们可以在GHC 7.8中使用DataKinds
和TypeFamilies
消除中缀类型构造函数,而使用类型级别列表。(在Haskell中,不能有一个类型构造函数,例如-IO
或-Either
不能使用可变数量的参数,但是类型级别列表只是一个参数。)只有几行,我并不是很想解释:
type family OneOf (as :: [*]) :: * where
OneOf '[] = Void
OneOf '[a] = a
OneOf (a ': as) = Either a (OneOf as)
请注意,您需要导入Data.Void
。现在我们可以这样做:
anInt4 :: OneOf '[Int, Double, Float, String]
anInt4 = fromInt 4
ghci> :kind! OneOf '[Int, Double, Float, String]
OneOf '[Int, Double, Float, String] :: *
= Either Int (Either Double (Either Float [Char]))
换句话说,OneOf '[Int, Double, Float, String]
与相同Either Int (Either Double (Either Float [Char]))
。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句