为多种类型签名重用相同的函数体

时间:2014-11-19 02:08:58

标签: haskell

我有这样的功能:

test :: [Block] -> [Block]
test ((Para foobar):rest) = [Para [(foobar !! 0)]] ++ rest
test ((Plain foobar):rest) = [Plain [(foobar !! 0)]] ++ rest

Block是包含ParaPlain和其他内容的数据类型。 函数的作用并不是特别重要,但我注意到[Para [(foobar !! 0)]] ++ restPara函数的主体(Plain)是相同的,除了使用的构造函数是类型foobar

问题:有两种情况合并写一下这个函数的简洁吗? 像

这样的东西
test :: [Block] -> [Block]
test ((ParaOrPlain foobar):rest) = [ParaOrPlain [(foobar !! 0)]] ++ rest

第一个ParaOrPlainPara foobarPlain foobar匹配,第二个ParaOrPlain分别为ParaPlain

请注意,Block也可以是(例如)BulletListOrderedList,我不想对其进行操作。 (编辑:test x = x以获取其他类型。)

关键是我不想复制函数体两次,因为它们是相同的(除非调用ParaPlain)。

我觉得我可以使用Either,或者我自己的数据类型,但我不知道该怎么做。


修改 为了澄清,我知道我的函数体很麻烦(我是Haskell的新手),我感谢各种回答者的简化。

然而,问题的核心是,我希望避免复制ParaPlain行。像(用我的制作语言......)

# assume we have `test (x:rest)` where `x` is an arbirtrary type
if (class(x) == 'Para' or class(x) == 'Plain') {
    conFun = Plain
    if (class(x) == 'Para')
        conFun = Para
    return [conFun ...]
}
# else do nothing (ID function, test x = x)
return x:rest

即,我想知道在Haskell中是否有可能以这种方式根据输入参数的类型分配构造函数。希望澄清这个问题。

6 个答案:

答案 0 :(得分:3)

一种方法是使用(多态)辅助函数,例如:

helper ctor xs rest = ctor [ head xs ] : rest

test :: [Block] -> [Block]
test ((Para xs):rest)  = helper Para xs rest
test ((Plain xs):rest) = helper Plain xs rest
test bs                = bs

请注意,ParaPlain只是[whatever] -> Block类型的函数。

答案 1 :(得分:3)

假设表格中的数据类型声明:

data Block
    = Para { field :: [Something] }
    | Plain { field :: [Something] }
    ...

您可以简单地使用通用记录语法:

test :: [Block] -> [Block]
test (x:rest) = x { field = [(field x) !! 0] } : rest

Live demo

答案 2 :(得分:1)

首先请注意,正如user5402已经写过的那样,您应该将[Para [(foobar !! 0)]] ++ rest替换为Para [foobar !! 0] : rest,这看起来已经不错了。接下来请注意,基本上,你所做的只是修改列表的头部,所以作为一个有意义的帮助我会使用

modifyHead :: (a->a) -> [a] -> [a]
modifyHead f (x:xs) = f x : xs
modifyHead _ [] = []

test = modifyHead m
 where m (Para foobar) = Para [head foobar]  -- `head` is bad, but still better than `(!!0)`!
       m (Plain foobar) = Plain [head foobar]

答案 3 :(得分:1)

这与我在不使用Template Haskell或Data.Data时可以得到的一样好: - )

首先,我们需要启用一些扩展并修复Block数据类型以获得具体性(如果我的简化假设是错误的,请告诉我并且我将看到可以挽救的内容!)

{-# LANGUAGE GADTs, LambdaCase #-}
data Block = Para [Int]
           | Plain [String]
           | Other Bool Block
           | YetAnother deriving (Show,Eq)

这里重要的一点是ParaPlain是一元构造函数,但数据类型本身可以包含具有不同数量参数的构造函数。

正如@leftaroundabout和@ user5402解释的那样,我们可以将修改单个Block并将该修改仅应用于列表的第一个元素的问题分开,因此我将专注于重写

modifyBaseline :: Block -> Block
modifyBaseline (Para xs) = Para [xs !! 0]
modifyBaseline (Plain xs) = Plain [xs !! 0]
modifyBaseline rest = rest

接下来,我们需要能够将一元构造函数作为值进行讨论。这里有三件重要的事情:

  • 使值与给定的构造函数匹配,
  • 若有,那么' inside',
  • 我们如何(重新)构建这样的价值。

我们很好地将其打包成自定义类型(t是构造函数所属的类型,而a就是其中的内容):

data UnaryConstructor t a = UnaryConstructor {
    destruct:: t -> Maybe a
    construct :: a -> t
}

现在我们可以定义

para :: UnaryConstructor Block [Int]
para = UnaryConstructor (\case { Para ns -> Just ns ; _ -> Nothing }) Para
plain :: UnaryConstructor Block [Int]
plain = UnaryConstructor (\case { Plain ss -> Just ss ; _ -> Nothing }) Plain

(如果你写LambdaCase,你可以摆脱(\xs -> case xs of { Para ns -> Just ns; _ -> Nothing})扩展名,但这种方式很好而且紧凑!)

接下来,我们需要打包'一元构造函数和应用于其中包含的值的函数:

data UnaryConstructorModifier t where
    UnaryConstructorModifier :: UnaryConstructor t a -> (a -> a) -> UnaryConstructorModifier t

这样我们就可以写

modifyHelper :: [UnaryConstructorModifier t] -> t -> t
modifyHelper [] t = t
modifyHelper ((UnaryConstructorModifier uc f):ucms) t 
    | Just x <- destruct uc t = construct uc (f x)
    | otherwise = modifyHelper ucms t

最后(lambda可能是(\xs -> [xs !! 0])或(\xs -> [head xs])尝试):

modify :: Block -> Block
modify = modifyHelper [UnaryConstructorModifier para (\(x:_) -> [x]), 
                       UnaryConstructorModifier plain (\(x:_) -> [x])]

如果我们现在用

测试它
ghci> let blocks = [Para [1,2,3,4], Plain ["a","b"], Other True (Para [42,43]), YetAnother ] 
ghci> map modify blocks == map modifyBaseline blocks

我们得到True - 万岁!


重复位现在位于paraplain的定义中,您必须将构造函数的名称写入三次,但如果不使用Template Haskell,则无法解决这个问题。或Data.Data(我可以看到)。

进一步改进的选择是为不同arity的构造函数做一些事情,并在a -> Maybe a中放置UnaryConstructorModifier类型的函数来处理(\(x:_) -> [x]的偏倚但是我认为这很好地回答了你的问题。

我希望你能理解这一点,因为我已经掩盖了一些细节,包括UnaryConstructorModifier定义中的内容以及modifyHelper中使用的模式保护{1}} - 所以问你是否需要澄清!

答案 4 :(得分:1)

您最接近原始想法的可能是“通用Para - 或 - Plain - 修改”功能。

{-# LANGUAGE RankNTypes #-}
modifyBlock :: (forall a . [a]->[a]) -> Block -> Block
modifyBlock f (Plain foobar) = Plain $ f foobar
modifyBlock f (Para foobar)  = Plain $ f foobar

观察f每次使用不同的类型!

然后,你可以写

test (b:bs) = modifyBlock (\(h:_) -> [h]) b : bs

答案 5 :(得分:1)

这可以通过ViewPatterns

很好地完成

注意:这里实际上并不需要查看模式,只是让它看起来更漂亮imho

注意':这假设在两种情况下块内的列表都是相同的类型

{-# LANGUAGE ViewPatterns #-}
paraOrPlain :: Block a- > Maybe (a -> Block a, a)
paraOrPlain (Plain xs) = Just (Plain,xs)
paraOrPlain (Para xs) = Just (Para,xs)
paraOrPlain _ = Nothing

-- or even better

para (Para xs) = Just (Para,xs)
para _ = Nothing

plain (Plain xs) = Just (Plain,xs)
plain _ = Nothing

paraOrPlain' b = para b <|> plain b -- requires Control.Applicative

test ((paraOrPlain -> Just (con,xs)) : rest) = con (take 1 xs) : rest

当然,你仍然需要在某个地方进行模式匹配 - 如果没有TH或Generic,你就无法做到这一点 - 但是你可以用一种有助于重用的方式来做到这一点。