有没有办法使用一种类型的构造函数作为类型?

时间:2013-12-11 21:59:37

标签: haskell

例如:

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 = ...

但我对此解决方案不满意,因为它需要大量冗余代码。

有没有一种优雅的方法来解决这个问题?

4 个答案:

答案 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)

听起来好像你在这两件事之间做了类比:

  1. 类型构造函数Foo及其变体构造函数BarBaz;
  2. 面向对象的类型Foo及其子类型BarBaz
  3. Haskell根本不会那样工作。任何类型都不是任何其他类型的子类型或超类型;类型构造函数(如Foo)和值构造函数(如Bar / Baz)是完全独立的实体。

    你可以尝试在这里做几件事。第一个也是最简单的一个是jwodder在评论中建议的:写一个[Foo] -> [String]。这具有简单和直接的优点,缺点是你可能想要一种不是String的类型。

    这样做的一个变体是定义与Bar分开的BazFoo类型,并根据这些定义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只能使用BarFoo BazType来构建与Baz

    另一种高级解决方案是这样的:首先,您将Foo类型参数化,以便代替IntString使用类型变量:

    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基本上意味着“包含不存在的内容的BarVoid)或包含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 ...。在任何情况下,类型分析器功能方法都很有用。