我正在尝试将基本函数转换为高阶函数(特别是map,filter或foldr)。我想知道是否有任何简单的概念可以应用到可以看到我使用防护编写的旧函数并将它们转换为更高阶的地方。
我正在更改一个名为filterFirst
的函数,该函数从列表中移除第一个元素(第二个参数),该元素不满足给定谓词功能(第一个参数)。
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst _ [] = []
filterFirst x (y:ys)
| x y = y : filterFirst x ys
| otherwise = ys
例如:
greaterOne :: Num a=>Ord a=>a->Bool
greaterOne x = x > 1
filterFirst greaterOne [5,-6,-7,9,10]
[5,-7,9,10]
基于基本递归,我想知道是否可能有一种方法将此(以及类似的函数)转换为高阶映射,过滤器或文件夹。我不是很高级,这些功能对我来说是新的。
答案 0 :(得分:4)
首先,让我们翻转函数的参数顺序。这将使一些步骤变得更加容易,我们可以在完成后将其翻转回去。 (我将其称为翻转版本filterFirst'
。)
filterFirst' :: [a] -> (a -> Bool) -> [a]
filterFirst' [] _ = []
filterFirst' (y:ys) x
| x y = y : filterFirst' ys x
| otherwise = ys
请注意,所有filterFirst' ys (const True) = ys
的{{1}}。让我们用它代替它:
ys
使用if-else代替警卫:
filterFirst' :: [a] -> (a -> Bool) -> [a]
filterFirst' [] _ = []
filterFirst' (y:ys) x
| x y = y : filterFirst' ys x
| otherwise = filterFirst' ys (const True)
将第二个参数移至lambda:
filterFirst' :: [a] -> (a -> Bool) -> [a]
filterFirst' [] _ = []
filterFirst' (y:ys) x = if x y then y : filterFirst' ys x else filterFirst' ys (const True)
现在这是我们可以变成filterFirst' :: [a] -> (a -> Bool) -> [a]
filterFirst' [] = \_ -> []
filterFirst' (y:ys) = \x -> if x y then y : filterFirst' ys x else filterFirst' ys (const True)
的东西。我们要使用的模式是foldr
可以用filterFirst' (y:ys)
表示,而无需使用filterFirst' ys
,我们现在就在那里。
ys
现在我们只需要稍微整理一下:
filterFirst' :: Foldable t => t a -> (a -> Bool) -> [a]
filterFirst' = foldr (\y f -> \x -> if x y then y : f x else f (const True)) (\_ -> [])
然后将参数反转:
filterFirst' :: Foldable t => t a -> (a -> Bool) -> [a]
filterFirst' = foldr go (const [])
where go y f x
| x y = y : f x
| otherwise = f (const True)
我们完成了。 filterFirst :: Foldable t => (a -> Bool) -> t a -> [a]
filterFirst = flip $ foldr go (const [])
where go y f x
| x y = y : f x
| otherwise = f (const True)
根据filterFirst
实施。
附录:尽管foldr
不够强大,但与State monad一起使用时filter
:
filterM
答案 1 :(得分:4)
这里有一个适合使用的高阶函数,但它不在基础库中。 foldr
有什么问题?如果您只是折叠列表,最终将重建整个内容,包括删除后的 部分。
更合适的功能是recursion-schemes
包中的para
(我已将类型变量之一重命名):
para :: Recursive t => (Base t (t, r) -> r) -> t -> r
在列表的情况下,这专门用于
para :: (ListF a ([a], r) -> r) -> [a] -> r
其中
data ListF a b = Nil | Cons a b
deriving (Functor, ....)
这与foldr
非常相似。与recursion-schemes
等效的foldr
是
cata :: Recursive t => (Base t r -> r) -> t -> r
专门研究
cata :: (ListF a r -> r) -> [a] -> r
在这里休息一下,弄清楚为什么cata
的类型基本上等同于foldr
的类型。
cata
和para
之间的区别在于para
通过了折叠功能,不仅折叠了列表尾部的结果,还传递了列表尾部本身。找到第一个不匹配元素后,这为我们提供了一种简单有效的方法来生成列表的其余部分:
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst f = para go
where
--go :: ListF a ([a], [a]) -> [a]
go (Cons a (tl, r))
| f a = a : r
| otherwise = tl
go Nil = []
para
对于列表有点尴尬,因为它旨在适应更一般的上下文。但是,正如cata
和foldr
基本上是等效的一样,我们可以为列表编写一个稍微不太尴尬的函数。
foldrWithTails
:: (a -> [a] -> b -> b)
-> b -> [a] -> b
foldrWithTails f n = go
where
go (a : as) = f a as (go as)
go [] = n
然后
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst f = foldrWithTails go []
where
go a tl r
| f a = a : r
| otherwise = tl
答案 2 :(得分:3)
如果确实需要,我们可以使用filterFirst
来编写foldr
,因为foldr
有点“通用”,它允许我们使用递归执行任何列表转换。主要缺点是生成的代码是违反直觉的。我认为,在这种情况下,显式递归要好得多。
无论如何,这是完成的过程。这依赖于我认为是反模式的东西,即“将四个四个参数传递给foldr
”。我之所以将其称为反模式,是因为foldr
通常仅使用三个参数来调用,而结果不是使用第四个参数的函数。
filterFirst :: (a->Bool)->[a]->[a]
filterFirst p xs = foldr go (\_ -> []) xs True
where
go y ys True
| p y = y : ys True
| otherwise = ys False
go y ys False = y : ys False
清除?不是很多。这里的窍门是利用foldr
来构建函数Bool -> [a]
,如果使用False
调用则返回原始列表,如果使用True
调用则返回过滤后的列表。如果我们使用
foldr go baseCase xs
结果显然是
foldr go baseCase xs True
现在,基本情况必须处理空列表,在这种情况下,无论布尔参数是什么,我们都必须返回一个返回空列表的函数。因此,我们到达
foldr go (\_ -> []) xs True
现在,我们需要定义go
。这作为参数:
y
ys
(列表其余部分的函数Bool->[a]
)的结果,并且必须为更大的列表返回函数Bool->[a]
。所以我们也考虑一下
,最后使go
返回一个列表。好吧,如果布尔值为False
,我们必须返回列表不变,所以
go y ys False = y : ys False
请注意,ys False
的意思是“尾巴未变”,因此我们实际上是在重建整个列表而未变。
如果布尔值是true,我们将像p y
中那样查询谓词。如果那是错误的,我们丢弃y
,并返回列表尾部不变
go y ys True
| p y = -- TODO
| otherwise = ys False
如果p y
为真,则保持y
并返回过滤后的列表尾部。
go y ys True
| p y = y : ys True
| otherwise = ys False
最后一点,我们冷使用了一对([a], [a])
而不是函数Bool -> [a]
,但是这种方法不能推广到更复杂的情况。
仅此而已。知道这项技术很高兴,但是我不建议在实际的代码中推荐它,以免被其他人理解。
答案 3 :(得分:3)
Joseph和chi的答案已经显示了如何派生foldr
实现,因此,我将尝试帮助直觉。
map
是保留长度的,而filterFirst
不是保留长度的,因此琐碎的map
必须不适合实现filterFirst
。
filter
(实际上是map
)是无记忆的-相同的谓词/函数将应用于列表的每个元素,而不管其他元素的结果如何。在filterFirst
中,一旦看到第一个不令人满意的元素并将其删除,行为就会改变,因此filter
(和map
)是不合适的。
foldr
用于将结构简化为摘要值。这是非常笼统的,如果没有经验,这可能不会立即显而易见。 filterFirst
实际上就是这样的操作。直觉是这样的:“我们是否可以通过结构一次构建它,随我们进行构建(并根据需要存储其他状态)?”。我担心约瑟夫的答案会有些模糊,因为foldr
具有4个参数,可能不会马上就知道发生了什么,所以让我们以不同的方式尝试一下。
filterFirst p xs = snd $ foldr (\a (deleted,acc) -> if not deleted && not (p a) then (True,acc) else (deleted,a:acc) ) (False,[]) xs
这是第一次尝试。这里的“额外状态”显然是布尔值,指示我们是否已删除元素,并且该列表累积在元组的第二个元素中。最后,我们调用snd
仅获得列表。但是,此实现存在一个问题,即我们删除不满足谓词的 rightmost 元素,因为foldr
首先将最右边的元素与中性元素组合在一起,然后再组合第二个最右边的元素,依此类推上。
filterFirst p xs = snd $ foldl (\(deleted,acc) a -> if not deleted && not (p a) then (True,acc) else (deleted,a:acc) ) (False,[]) xs
在这里,我们尝试使用foldl
。这确实删除了最左边的不令人满意的元素,但具有反转列表的副作用。我们可以将reverse
放在前面,这样可以解决问题,但是由于两次遍历而不能令人满意。
然后,如果您回到foldr
,并且已经意识到(基本上)如果要转换列表同时保留foldr
是正确变体的顺序,则可以使用它一段时间,然后最后写了约瑟夫的建议。但是,我确实同意chi的观点,直截了当的递归是最好的解决方案。
答案 4 :(得分:2)
您的功能还可以表示为展开,或者更具体地,表示为无性。请允许我在解决方案本身之前先做一个简短的解释性说明。
Apomorphism是对Aparamorphism双重的递归方案(有关后者的更多信息,请参见dfeuer's answer)。 Apomorphisms是展开的例子,它从种子产生结构。例如,Data.List
提供unfoldr
,这是一个列表。
unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
赋予unfoldr
的函数获取一个种子,然后生成一个列表元素和一个新的种子(如果maymay值为Just
)或终止列表的生成(如果它为{{ 1}})。展开通常由递归方案中的Nothing
函数表示(“ ana”是“ anamorphism”的缩写)。
ana
专门用于列表,它变为...
ana :: Corecursive t => (a -> Base t a) -> a -> t
... ana @[_] :: (b -> ListF a b) -> b -> [a]
,穿着不同的衣服。
无定形是一个发展过程,在该过程中,可以通过俯冲产生其余结构而不是新种子来缩短结构的生成。对于列表,我们有:
unfoldr
apo @[_] :: (b -> ListF a (Either [a] b)) -> b -> [a]
用于触发短路:出现Either
时,展开短路,而出现Left
时则正常进行。
关于Right
的解决方案非常直接:
apo
它比dfeuer的基于{-# LANGUAGE LambdaCase #-}
import Data.Functor.Foldable
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst p = apo go
where
go = \case
[] -> Nil
a : as
| p a -> Cons a (Right as)
| otherwise -> case as of
[] -> Nil
b : bs -> Cons b (Left bs)
的解决方案更尴尬,因为如果我们想在没有尾巴的空列表的情况下进行短路,则我们不得不发出一个额外的元素(para
短路情况),因此我们必须向前看一个位置。如果我们要而不是b
用展开的方式来实现普通的旧filterFirst
,那么这种尴尬将会增加几个数量级,如List filter using an anamorphism中所解释的那样。
答案 5 :(得分:2)
此答案的灵感来自a comment from luqui,涉及一个现已删除的问题。
filterFirst
可以按照span
的相当直接的方式实现:
filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst p = (\(yeas, rest) -> yeas ++ drop 1 rest) . span p
span :: (a -> Bool) -> [a] -> ([a], [a])
在条件不成立的第一个元素处将列表一分为二。在span
之后,我们删除列表第二部分的第一个元素(使用drop 1
而不是tail
,这样我们就不必为{{1}添加特殊情况}),然后使用[]
重新组合列表。
顺便说一句,此实现有一个近乎无点的拼写,我发现它实在是太漂亮了:
(++)
尽管filterFirst :: (a -> Bool) -> [a] -> [a]
filterFirst p = uncurry (++) . second (drop 1) . span p
是一个高阶函数,但是如果您在问题的上下文中发现此实现令人失望,则完全可以理解。毕竟,span
并不比span
本身更基础。我们是否应该尝试更深入一点,看看我们是否可以在将它表示为折叠或其他某种递归方案的同时抓住这种解决方案的精神?
我相信像filterFirst
这样的函数可以很好地证明同胚性。亚同态是一个展开过程(有关更多信息,请参见my other answer),它生成一个中间数据结构,然后是一个折叠,从而将该数据结构转换为其他形式。如果看起来同形正确实现(如在 recursion的filterFirst
函数中完成的话),看起来可能需要两次通过才能获得结果(一次通过输入结构,另一次通过中间结构)。 -schemes ),它可以一次完成,折叠过程会消耗掉中间结构的一部分,因为它们是由展开过程生成的(因此,我们不必为了将其拆解而实际上全部构建起来) )。
在开始之前,这是运行以下内容所需的样板:
hylo
这里的策略是为亚纯态选择一个中间数据结构,该结构表示我们想要实现的本质。在这种情况下,我们将使用以下可爱的东西:
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TemplateHaskell #-}
import Data.Functor.Foldable
import Data.Functor.Foldable.TH
data BrokenList a = Broken [a] | Unbroken a (BrokenList a)
-- I won't actually use those instances here,
-- but they are nice to have if you want to play with the type.
deriving (Eq, Show, Functor, Foldable, Traversable)
makeBaseFunctor ''BrokenList
非常类似于列表(BrokenList
和Broken
镜像Unbroken
和[]
,而(:)
咒语生成{ {1}}基本函数类似于makeBaseFunctor
,具有BrokenListF
和ListF
构造函数),不同之处在于它的末尾附加了另一个列表(BrokenF
构造函数)。它以一种非常真实的方式表达了将列表分为两部分的想法。
借助UnbrokenF
,我们可以编写同胚。 Broken
是用于展开的操作,BrokenList
是用于折叠的操作。
coalgSpan
algWeld
在击中filterFirst p = hylo algWeld coalgSpan
where
coalgSpan = \case
[] -> BrokenF []
x : xs
| p x -> UnbrokenF x xs
| otherwise -> BrokenF xs
algWeld = \case
UnbrokenF x yeas -> x : yeas
BrokenF rest -> rest
元素时中断列表,以使coalgSpan
不成立。不将该元素添加到列表的第二部分(x
而不是p x
)会处理过滤。对于BrokenF xs
,它用于连接两个部分(非常类似于我们将使用BrokenF (x : xs)
实现algWeld
的方式)。
(有关运行中的(++)
的类似示例,请参见this older answer of mine的注释5中的cata
实现。它建议使用此实现BrokenList
会采取什么措施策略。)
基于breakOn
的实现至少有两个好处。首先,它具有良好的性能(临时测试表明,如果进行优化编译,它的性能至少与此处其他答案中最有效的实现方案一样好,并且可能快一些)。其次,它非常紧密地反映了span
的原始,显式递归实现(或者,无论如何,它比仅可折叠和仅可折叠实现更紧密)。