Haskell函数组合 - (a -> b) -> (a -> c) -> (b -> c -> d) -> (a -> d)

7

我希望学习如何使用point-free方式完成以下操作:

withinBounds :: [Int] -> Bool
withinBounds xs = (all (>= 0) xs) && (all (<= 8) xs)

我知道为了易读性和代码的清晰度,这种写法更好,但我想学习如何组合函数。我一直在思考如何做到这一点。 整个(扩展?)类型签名是:
[Int] -> ([Int] -> Bool) -> ([Int] -> Bool) -> (Bool -> Bool -> Bool) -> Bool

我正在尝试实现的组合函数的类型签名是:
(a -> b) -> (a -> c) -> (b -> c -> d) -> (a -> d)

我用混合的lambda形式写了下面的笔记。如果有一种方法可以通过lambda演算来简化问题,那么请解释一下:

\L@[] ->  \f1@([] -> Bool) -> \f2@([] -> Bool) -> \f3@(Bool -> Bool -> Bool) -> f3.(f1.L).(f2.L) 

在上面的代码中,.表示应用程序,@表示捕获(因此f3是(Bool -> Bool -> Bool)的另一个名称)。非常感谢。
编辑:我知道这不是最优或可重用的代码,我知道将其转换为点自由会使其在可读性等方面变得更糟。澄清一下,我想知道如何将其转换为点自由,因为我想学习更多关于haskell和组合的知识
编辑2:一个关于点自由的很好的SO答案

3
你的 withinBounds 函数无法组合使用,最好写一个单个元素的检查,然后在调用 all。实际上,我可能会直接将只能针对单个元素的 withinBounds 函数嵌入到 all withinBounds 中。 - Benjamin Gruenbaum
1
@MIJOTHY:通常,如果类型更通用,则更容易重复使用,例如 withinBounds :: Ord e => (e, e) -> e -> Bool; withinBounds (a,b) x = a <= x && x <= b。现在您的原始函数只是 all (withinBounds (0,8))。此外,我可以将其用作过滤器的谓词:filter (withinBounds (4,100)) [1..103] - Zeta
是的,我可以理解通用性=更可重用,但我想知道是否有一种特定的方法来判断一个函数是否可以使用组合运算符以点无形式编写。也许我的“可组合性”用法是错误的。因此,为了用你写的更一般的版本重新表述问题,我能否以点无形式编写 withinBounds (a,b) x = a <= x && x <= b - MIJOTHY
@MIJOTHY:是的,但你不想这样做。uncurry ((. flip (<=)) . ap . ((&&) .) . (<=))。最终,代码不仅要被计算机读取,还要被人阅读。无点版本不仅更长,而且比原始版本更难理解。 - Zeta
1
@MIJOTHY:哦,抱歉,我忘了告诉你。在Hackage上有pointfree包,在#haskell的lambdabot上也可以找到,还有http://pointfree.io。话虽如此,我通常会移动参数直到最终得到可约化函数,例如:http://stackoverflow.com/a/36542287/1139697。 - Zeta
显示剩余3条评论
4个回答

9
您可以利用函数是应用类型的事实,并以以下方式编写withinBounds

您可以利用函数是应用类型的事实,并以以下方式编写withinBounds

withinBounds = pure (&&) <*> all (>= 0) <*> all (<= 8)

或者这样做:
withinBounds = (&&) <$> all (>= 0) <*> all (<= 8)

您可以在这里这里了解有关Applicatives的内容。


为什么我的评论被删除了/为什么它消失了? - MIJOTHY
1
不知道呢 :/ 昨天它有两个赞,其中一个是我给的。 - Safareli

9

有一个类专门用于多通道的无点组合,名为Arrow。如果你决定将所有东西都做成无点形式,则我认为这是最好的方法。不过,其中的棘手之处在于你需要不断地对函数进行非柯里化:

import Control.Arrow

withinBounds = all (>=0) &&& all (<=8) >>> uncurry (&&)

这个过程最好通过下面的图示来理解:
      all (>=0) ────
       ╱                ╲
──── &&&            >>>  uncurry (&&) ───
       ╲                ╱
      all (<=8) ──── 

Arrow 可以在广义情况下使用;不仅适用于 Hask 函数,而且适用于任何合适的范畴。但它足够有用,可以将其应用于函数。


太好了,这个图确实很有帮助。我会读一些关于箭头的资料。谢谢! - MIJOTHY

2

绕开整个问题,我认为我可能会这样写:

import Data.Ix
withinBounds = all (inRange (0, 8))

当然,这有点投机取巧,因为接下来人们自然会问如何以无点方式实现inRange。如果绝对不能使用inRange,那么我会内联实现它,代码如下:

withinBounds = all (liftA2 (&&) (>=0) (<=8))

这里使用了reader applicative将一个参数提供给两个函数。你所请求的组合函数是liftA2,不过参数次序被翻转了:

requested :: (a -> b) -> (a -> c) -> (b -> c -> d) -> (a -> d)
liftA2    :: (b -> c -> d) -> (a -> b) -> (a -> c) -> (a -> d)

哦,我明白了!所以由于 liftA2 f a b = fmap f a <*> bf <$> x = fmap f xliftA2 f a b = f <$> a <*> b,这与 @Safareli 的答案很好地联系在了一起。 - MIJOTHY
由于对于 ((->) r) fmap = (.),它也可以写成 withinBounds = (&&) . (all (>= 0)) <*> (all (<= 8)) - MIJOTHY

-2

注意,当且仅当8-x>=0时,x<=8。因此,只使用prelude,我们可以写成:

withinBounds :: [Int] -> Bool
withinBounds = all $ (all (>=0)) . (zipWith (+) [0,8]) . (zipWith (*) [1,-1]) . (replicate 2)

基本上,我只是将x映射到[x,x],然后映射到[x,8-x],然后要求这两个同时大于等于0。

当然,正如评论中指出的那样,您也可以使a,b成为参数,以便稍后重用它们。

希望这有所帮助。


down-voter是否愿意解释一下投票反对的原因,这样我就可以改进了吗?实际上,这个技巧在其他类似的任务中也非常有用,并且不需要像其他答案那样高深的知识。 - awllower
好吧,问题主体中OP说可读性不是他关心的问题:他只想知道如何以无须点号的方式编写此内容。此外,我添加了一个解释来说明此函数的过程。也许我应该再解释一下?无论如何,感谢回复。 - awllower

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