在Haskell中合并多个过滤功能的优雅方式

taor4pac  于 2022-11-14  发布在  其他
关注(0)|答案(6)|浏览(151)

给定以下过滤函数作为一元 predicate ,

f1 :: Int -> Bool
f1 x = x > 30

f2 :: Int -> Bool
f2 x = x < 60

f3 :: Int -> Bool
f3 x = x `mod` 3 == 0

我想通过所有的整数来过滤一个整数列表。目前我正在做的事情大致如下:

filtered = filter f1 $ filter f2 $ filter f3 [1..90]
-- [33,36,39,42,45,48,51,54,57]

但这似乎不是最好的解决方案;尤其是我不喜欢filter的多次重复和缺乏可组合性。
是否有一种方法可以将所有这些 predicate 组合成一个 predicate ,让我们将其命名为<?>,这样可能的语法将类似于以下内容?

filtered = filter (f1 <?> f2 <?> f3) [1..90]
-- [33,36,39,42,45,48,51,54,57]

这个假设的<?>运算符的类型签名将是(a -> Bool) -> (a -> Bool) -> (a -> Bool),但在Hoogle上是I wasn't able to find any such thing

uqzxnwby

uqzxnwby1#

这个呢?

import Control.Applicative (liftA2)
-- given f1 etc.
filtered = filter (f1 <&&> f2 <&&> f3) [1..90]
  where
    (<&&>) = liftA2 (&&)

这里,将&&提升为Applicative得到了标记为<?>的结果,即,将两个一元 predicate 的结果“与”在一起的运算符。
我最初使用.&&.作为提升运算符,但amalloy建议,通过与其他Functor/Applicative提升运算符(如<$> *)进行类比,<&&>将是一个更好的名称 *。

kzmpq1sx

kzmpq1sx2#

> filter (and . sequence [f1, f2, f3]) [1..100]
[33,36,39,42,45,48,51,54,57]

本质上,上面的工作是因为sequence(在上面使用的(->) a单子上)接受一个函数列表,并返回一个返回列表的函数。

sequence [f, g, h] = \x -> [f x, g x, h x]

使用and :: [Bool] -> Bool进行后期合成会得到一个布尔结果,因此可以在filter中使用它。
而且,有观点也没什么好羞愧的:

> filter (\x -> f1 x && f2 x && f3 x) [1..100]

只是稍微长了一点,而且可以说读起来更简单。

h7appiyu

h7appiyu3#

Data.Monoid定义了一个Predicate类型,可用于表示您的函数:

import Data.Monoid

-- newtype Predicate t = Predicate { getPredicate :: t -> Bool }
p1 :: Predicate Int
p1 x = Predicate $ x > 30

p2 :: Predicate Int
p2 x = Predicate $ x < 60

p3 :: Predicate Int
p3 x = Predicate $ x `mod` 3 == 0

Predicate具有Semigroup示例,该示例将两个 predicate 组合成一个 predicate ,如果 * 两个 * 输入 predicate 都满足,则满足该 predicate 。

-- instance Semigroup (Predicate a) where
-- Predicate p <> Predicate q = Predicate $ \a -> p a && q a

filtered = filter (getPredicate (p1 <> p2 <> p3)) [1..90]

不幸的是,在将组合 predicate 与filter一起使用之前,您需要将它们展开。您可以定义自己的filterP函数,并使用它来代替filter

filterP :: Predicate t  -> [t] -> [t]
filterP = filter . getPredicate

filtered = filterP (p1 <> p2 <> p3) [1..90]

还有一个Monoid示例(标识是一个 predicate ,它总是返回True),您可以像这样使用它

filtered = filter (getPredicate (mconcat [p1, p2, p3]))

你可以把它重新分解成

filterByAll = filter . getPredicate . mconcat

filtered = filterByAll [p1, p2, p3] [1..90]
zzlelutf

zzlelutf4#

您可以使用extra package的**(&&^) :: Monad m => m Bool -> m Bool -> m Bool**:

import Control.Monad.Extra((&&^))

filtered = filter (f1 &&^ f2 &&^ f3) [1..90]

这给了我们:

Prelude Control.Monad.Extra> filter (f1 &&^ f2 &&^ f3) [1..90]
[33,36,39,42,45,48,51,54,57]

(&&^)函数实现为[src]:

ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM b t f = do b <- b; if b then t else f

-- …

(&&^) :: Monad m => m Bool -> m Bool -> m Bool
(&&^) a b = ifM a b (pure False)

这是因为函数类型是Monad

instance Monad ((->) r) where
    f >>= k = \ r -> k (f r) r

因此,这意味着ifM被实现为如下函数:

-- ifM for ((->) r)
ifM b t f x
    | b x = t x
    | otherwise = f x

(&&^)函数检查第一个条件b x是否为True,如果不是,则返回False(因为fconst False,因此f xFalse)。如果b xTrue,则检查链中的下一个元素。

niknxzdl

niknxzdl5#

我们需要一种方法来使用像and这样的函数来组合 predicate ,而不仅仅是布尔值。
一种懒惰的方法是向Hoogle请求Functor f => ([b]-> b) -> [f b] -> f b这样的类型签名,其中f大概是Int ->这样的东西。
看起来效果不错:

λ> 
 λ> f1 x = x > 30
 λ> f2 x = x < 60
 λ> f3 x = (mod x 3) == 0
 λ> 
 λ> import Data.Distributive (cotraverse)
 λ> :t cotraverse
 cotraverse
  :: (Distributive g, Functor f) => (f a -> b) -> f (g a) -> g b
 λ> 
 λ> filter  ( cotraverse and [f1,f2,f3] )  [1..90]
 [33,36,39,42,45,48,51,54,57]
 λ>

正在检查:

λ> 
 λ> filter  (\x -> and (map ($ x) [f1,f2,f3]))  [1..90]
 [33,36,39,42,45,48,51,54,57]
 λ>
nzk0hqpo

nzk0hqpo6#

其他的答案都很好,但是我会给予我喜欢的合并函数的方式,它非常简洁。我非常喜欢使用Control中的lift函数。Monad

filter $ liftM2 (&&) f1 f2

liftM2的工作方式是将(&&)函数提升为一个单子,并将f1和f2作为参数。
我知道有一个名为liftM3的函数,但我不确定它是否能在此上下文中工作。
https://hackage.haskell.org/package/base-4.14.0.0/docs/Control-Monad.html#v:liftM3

相关问题