例如:
data Foo = Bar Int | Baz String
我想使用像:
这样的函数qux :: [Foo] -> [Baz]
qux fs = filter f fs
where
f (Baz _) = True
f _ = False
通常情况下,我需要:
data Bar = Bar Int
data Baz = Baz String
data Foo = FBar Bar | FBaz Baz
qux = ...
但我对此解决方案不满意,因为它需要大量冗余代码。
有没有一种优雅的方法来解决这个问题?
答案 0 :(得分:3)
一种可能性是使用GADTs。它的要点是,这允许我们对数据类型定义有一个所谓的"phantom" type argument。我们可以做的一件事是限制使用这种数据类型的函数的类型(例如),以便它只适用于某个构造函数可以创建的值。
以下是我将如何编写您在此样式中提供的示例(注意:您需要启用GADTs扩展名):
{-# LANGUAGE GADTs #-}
data BarType -- Note: These could be called Bar and Baz, but I named them this for clarity
data BazType
data Foo a where
Bar :: Int -> Foo BarType
Baz :: Int -> Foo BazType
有了这个,您可以编写如下函数:
onlyAcceptsBars :: Foo BarType -> Int
onlyAcceptsBars (Bar bar) = -- ...
-- There would be a compile-time error if you had a case like this:
-- onlyAcceptsBars (Baz baz) = ...
onlyAcceptsBazs :: Foo BazType -> Int
onlyAcceptsBazs (Baz baz) = -- ...
acceptsAnyFoo :: Foo a -> Int
acceptsAnyFoo (Bar bar) = -- ...
acceptsAnyFoo (Baz baz) = -- ...
答案 1 :(得分:1)
不,没有办法完全按照自己的意愿行事。我们来看下面的例子
data Foo = Bar Int String | Baz Bool Int
isBaz (Baz _ _) = True
isBaz _ = False
要实现您的目标,您基本上有三种选择。首先,过滤而无需改变类型
qux :: [Foo] -> [Foo]
qux = filter isBaz
或者,您可以将Foo
转换为元组
qux :: [Foo] -> [(Bool, Int)]
qux = map f . filter isBaz
where
f (Baz b i) = (b,i)
或者您可以定义新类型
data Baz' = Baz' Bool Int
qux :: [Foo] -> [Baz']
qux = map f . filter isBaz
where
f (Baz b i) = Baz' b i
或者您可以按照问题中的建议重写Foo
data Bar = Bar Int String
data Baz = Baz Bool Int
data Foo = FooBar Bar | FooBaz Baz
isBaz (FooBaz _) = True
isBaz _ = False
qux :: [Foo] -> [Baz]
qux = map f . filter isBaz
where
f (FooBaz baz) = baz
希望你能找到其中一种可以接受的。
答案 2 :(得分:1)
听起来好像你在这两件事之间做了类比:
Foo
及其变体构造函数Bar
和Baz
; Foo
及其子类型Bar
和Baz
。 Haskell根本不会那样工作。任何类型都不是任何其他类型的子类型或超类型;类型构造函数(如Foo
)和值构造函数(如Bar
/ Baz
)是完全独立的实体。
你可以尝试在这里做几件事。第一个也是最简单的一个是jwodder在评论中建议的:写一个[Foo] -> [String]
。这具有简单和直接的优点,缺点是你可能想要一种不是String
的类型。
这样做的一个变体是定义与Bar
分开的Baz
和Foo
类型,并根据这些定义Foo
:
newtype Bar = Bar Int
newtype Baz = Baz String
data Foo = ABar Bar | ABaz Baz
现在,您无法使用预期String
的普通Baz
;缺点是它显然更加冗长,但你可以编写辅助函数来简化事情:
-- Virtual constructors for `Foo`
bar :: Int -> Foo
bar i = ABar (Bar i)
baz :: String -> Foo
baz s = ABaz (Baz s)
-- A helper function to do the pattern matching for you. You give it a pair of
-- functions, one telling it how to process the `Int` in a `Bar`, the other how
-- to process the `String` in a `Baz`.
analyzeFoo :: (Int -> r) -> (String -> r) -> Foo -> r
analyzeFoo barCase bazCase (ABar (Bar i) = barCase i
analyzeFoo barCase bazCase (ABaz (Baz s) = bazCase s
然后有更高级的解决方案,也许你不应该担心。 David Young使用GADT和空类型标记Foo
类型以指示其携带的元素类型,因此Foo BarType
只能使用Bar
和Foo BazType
来构建与Baz
。
另一种高级解决方案是这样的:首先,您将Foo
类型参数化,以便代替Int
和String
使用类型变量:
data Foo a b = Bar a | Baz b
现在,一种专门化这种类型以便它只能是Baz
的方法是将类型变量a
实例化为不包含值的类型;如果a
没有值,那么除非程序出错或永远运行,否则无法点击Bar
。
void
库附带了一种名为Void
的类型。使用它:
import Data.Void
data Foo' a b = Bar a | Baz b
type Foo = Foo' Int String
type Bar = Foo' Int Void
type Baz = Foo Void String
qux :: [Foo] -> [Baz]
qux = concatMap f
where f (Bar i) = []
f (Baz s) = [Baz s]
example :: [Baz] -> [Baz]
example = map go
where go (Bar v) = absurd v -- can't happen, no value exists for `v`
go (Baz s) = Baz (s ++ "!")
这里的诀窍是类型Foo' Void String
基本上意味着“包含不存在的内容的Bar
(Void
)或包含Baz
的{{1}}一个String
。“这意味着必须是后者。
答案 3 :(得分:1)
使用列表推导,
qux fs = [Baz x | Baz x <- fs]
尽管类型保持不变,但您需要自动“子类型向下转换”。或者,定义
foo bar baz v | Bar x <- v = bar x | Baz y <- v = baz y
qux fs = filter (foo (const False) (const True)) fs
但我怀疑你想要更复杂的子案例选择,比如有两个以上的变体,只接受一个。 类型分析器函数foo
也可以帮助您。路易斯·卡西利亚斯也提到了这一点,但是在更复杂的环境中。它可以与原始类型一起使用,不会改变。
但是,同样,结果类型保持不变,与您想要的不同。如果您必须对qux
的结果进行类型级别区分,则只需标记他们,然后保留Foo
:
newtype Baz a = Baz' a
newtype Bar a = Bar' a
qux :: [Foo] -> Baz [Foo]
qux = Baz' . filter (foo (const False) (const True))
或者您可以使用Baz'
在列表中推送qux :: [Foo] -> [Baz Foo]; qux = map Baz' . filter ...
。在任何情况下,类型分析器功能方法都很有用。