在Haskell中执行布尔函数的“and”和“or”操作

45

我刚刚写了以下两个函数:

fand :: (a -> Bool) -> (a -> Bool) -> a -> Bool
fand f1 f2 x = (f1 x) && (f2 x)

f_or :: (a -> Bool) -> (a -> Bool) -> a -> Bool
f_or f1 f2 x = (f1 x) || (f2 x)

它们可以用来组合两个布尔函数的值,例如:

import Text.ParserCombinators.Parsec
import Data.Char

nameChar = satisfy (isLetter `f_or` isDigit)

看完这两个函数后,我意识到它们非常有用。以至于我现在怀疑它们是否已包含在标准库中,或者更可能的是,存在一种使用现有函数实现这一功能的简洁方式。

那么,应该使用什么方法才是“正确”的呢?


4
Haskell是一种语言,在标准库中几乎所有东西都已经在一个类型类中。Typeclassopedia可以帮助解决这个问题。 - Jared Updike
7个回答

53

一个简化,

f_and = liftM2 (&&)
f_or  = liftM2 (||)
或者
      = liftA2 (&&)         
      = liftA2 (||)

((->) r)函子中使用Applicative。


应用版本

为什么呢?因为我们有:

instance Applicative ((->) a) where
    (<*>) f g x = f x (g x)

liftA2 f a b = f <$> a <*> b

(<$>) = fmap

instance Functor ((->) r) where
    fmap = (.)
所以:
  \f g -> liftA2 (&&) f g
= \f g -> (&&) <$> f <*> g          -- defn of liftA2
= \f g -> ((&&) . f) <*> g          -- defn of <$>
= \f g x -> (((&&) . f) x) (g x)    -- defn of <*> - (.) f g = \x -> f (g x)
= \f g x -> ((&&) (f x)) (g x)      -- defn of (.)
= \f g x -> (f x) && (g x)          -- infix (&&)

Monad版本

或者使用liftM2,我们可以得到:

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

所以:

  \f g -> liftM2 (&&) f g
= \f g -> do { x1 <- f; x2 <- g; return ((&&) x1 x2) }               -- defn of liftM2
= \f g -> f >>= \x1 -> g >>= \x2 -> return ((&&) x1 x2)              -- by do notation
= \f g -> (\r -> (\x1 -> g >>= \x2 -> return ((&&) x1 x2)) (f r) r)  -- defn of (>>=)
= \f g -> (\r -> (\x1 -> g >>= \x2 -> const ((&&) x1 x2)) (f r) r)   -- defn of return
= \f g -> (\r -> (\x1 ->
               (\r -> (\x2 -> const ((&&) x1 x2)) (g r) r)) (f r) r) -- defn of (>>=)
= \f g x -> (\r -> (\x2 -> const ((&&) (f x) x2)) (g r) r) x         -- beta reduce
= \f g x -> (\x2 -> const ((&&) (f x) x2)) (g x) x                   -- beta reduce
= \f g x -> const ((&&) (f x) (g x)) x                               -- beta reduce
= \f g x -> ((&&) (f x) (g x))                                       -- defn of const
= \f g x -> (f x) && (g x)                                           -- inline (&&)

如果使用liftA2而不是liftM2,那么推导会更容易。 - Conal

11

完全抄袭TomMD的代码,我看到了and . mapor . map,忍不住想要进行微调:

fAnd fs x = all ($x) fs
fOr fs x = any ($x) fs

我认为这些表述得很好。 fAnd:当应用于它们时,列表中的所有函数是否都为TruefOr:当应用于它们时,列表中是否有任何函数为True

ghci> fAnd [even, odd] 3
False
ghci> fOr [even, odd] 3
True

fOr是一个奇怪的名字选择,不过这对那些惯用命令式编程方式的程序员来说肯定是一个很好的“陷阱” =)


啊,非常好的实现。我怎么会忽略 allany 呢?! - Thomas M. DuBuisson
2
fAnd = fmap and . sequence fOr = fmap or . sequence - Tony Morris
我不确定 fAnd [even, odd] 3all ($3) [even, odd] 好多少。 - mb14
而且,点无版本:filter (\any` [even, prime]) . flip ($)) xs` - Clay Thomas

7

如果你总是想要两个功能,那么它看起来会更丑陋,但我认为我可以将其概括:

mapAp fs x = map ($x) fs

fAnd fs = and . mapAp fs
fOr fs = or . mapAp fs

> fOr [(>2), (<0), (== 1.1)] 1.1
True
> fOr [(>2), (<0), (== 1.1)] 1.2
False
> fOr [(>2), (<0), (== 1.1)] 4
True

这是我第一次在Haskell中看到($x)的使用。有一瞬间我感觉自己回到了PHP领域,不禁打了个寒战。 - Kendall Hopkins
是的,这总是一个奇怪的问题,但它比 (\f -> f x) 更短。 - Thomas M. DuBuisson

3
除了Don说的以外,liftA2/liftM2版本可能不够懒惰:
> let a .&&. b = liftA2 (&&) a b in pure False .&&. undefined *** Exception: Prelude.undefined 哎呀!
因此,您可能需要一个略微不同的函数。请注意,这个新函数需要一个Monad约束--Applicative是不够的。
> let a *&&* b = a >>= \a' -> if a' then b else return a' in pure False *&&* undefined False 这样更好。
至于那个建议使用on函数的答案,这适用于函数相同但参数不同的情况。在您给出的情况下,函数是不同的,但参数是相同的。这是您的示例,使得on成为一个合适的答案: (f x) && (f y) 可以写成: on (&&) f x y PS:括号是不必要的。

1
这个可以运行:let a .&&. b = liftA2 (&&) a b in (pure False .&&. undefined) () - Sjoerd Visscher
@SjoerdVisscher 为什么对于 ((->)e) 或者 Reader Monad 是 lazy 的而对于 IO Monad 不是? - Johannes Gerer
f <*> a 中,f 中的副作用必须先于 a 中的副作用发生。但 Reader Monad 没有副作用,因此它可以安全地忽略那些结果不需要的计算。 - Sjoerd Visscher

3

这个问题已经有所提及,但是表述比较复杂。你可以使用应用程序。

对于函数而言,基本上它会将相同的参数传递给多个函数,然后在最后进行组合。

因此,你可以这样实现:

(&&) <$> aCheckOnA <*> anotherCheckOnA $ a

对于链中的每个<*>,您会获得另一个适用于a的函数,然后您可以使用fmap(也可以替换为<$>)将所有输出混合在一起。之所以这适用于&&是因为它需要两个参数,而我们有两个星号函数相连。如果其中有一个额外的星号和另一个检查,则必须编写以下内容:

(\a b c -> a && b && c) <$> aCheckOnA <*> anotherCheckOnA <*> ohNoNotAnotherCheckOnA $ a

点击这里查看更多示例。


它不是 (&&) <$> aChekOnA <*> anotherCheckOnA $ a 吗? - mb14
@mb14 是的,我的错,我现在已经更新了答案。谢谢。 - JonnyRaa

1

这也可以使用箭头来完成:

import Control.Arrow ((&&&), (>>>), Arrow(..))

split_combine :: Arrow cat => cat (b, c) d -> cat a b -> cat a c -> cat a d
split_combine h f g = (f &&& g) >>> h

letter_or_digit = split_combine (uncurry (||)) isLetter isDigit

&&&(与&&无关)用于分割输入;>>>表示箭头/类别组合。

以下是一个例子:

> map letter_or_digit "aQ_%8"
[True,True,False,False,True]

这是因为函数 -- -> -- 是类别和箭头的实例。将类型签名与Don的liftA2liftM2示例进行比较,可以看出它们的相似之处:
> :t split_combine 
split_combine :: Arrow cat => cat (b, c) d  -> cat a b -> cat a c -> cat a d

> :t liftA2
liftA2    :: Applicative f => (b -> c -> d) ->     f b ->     f c ->     f d

除了柯里化之外,注意你几乎可以通过替换cat a ---> fArrow ---> Applicative将第一种类型转换为第二种类型(另一个区别是split_combine不仅限于在其第一个参数中使用纯函数;但可能不重要)。

-1
如果 f1 和 f2 相同,那么你可以使用 'on':
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

在 Data.Function 基础模块中。
fand1 f = (&&) `on` f
for1 f = (||) `on` f

典型用法:

Data.List.sortBy (compare `on` fst)

(来自Hoogle


3
这与问题所寻求的相反 - 它测试一个函数对多个值的情况(这已经由allany处理得很好了),而不是多个函数针对一个值的情况。 - Chuck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接