pointfree 中的翻转太多 Haskell?

Too many flips in pointfree Haskell?

我正在使用 Learn You A Haskell For Great Good! 学习 Haskell,我遇到了这个例子:

ghci> sum (takeWhile (<10000) (filter odd (map (^2) [1..])))
166650   

我心想“好吧,10000 快要被参数化了”。使用我目前所掌握的知识,我想出了一个严重依赖 (.)flip:

的解决方案

flip (takeWhile . flip (<)) (filter odd (map (^ 2) [1..]))

好吧,我们可以用 > 代替 *:

flip (takeWhile . (>)) (filter odd (map (^ 2) [1..]))

基本上,参数化变量的方法很明显。无论该表达式出现在哪里,我们都可以轻松地将其参数化:

odd_squares n = takeWhile (<n) (filter odd (map (^2) [1..]))

但是我提出的仅参数化单个变量的无点方法似乎有点疯狂,尤其是 flip 的使用两次。那时你陷入了诸如“(<) 再次应用哪个顺序?”之类的杂草中。而且您正在失去无点抽象的全部好处。

相加得到:

sum $ flip (takeWhile . flip (<)) (filter odd (map (^ 2) [1..]))

许多分隔符,一个 $(.)flip

所以,我猜一定有一种更简单的方法可以像这样在函数表达式中参数化变量。任何提示指向正确的方向吗?我查看了 Whosebug 上的其他帖子,这些帖子将我带到了 https://wiki.haskell.org/Pointfree,但这对我没有太大帮助,因为他们选择了纯粹偶然不需要 flip.

Whosebug 上出现的其他一些帖子更像是“嘿,对我来说这个点免费”的请求,而答案通常只是继续并使用一堆翻转,以不可读的函数结束。我的问题更多的是“pointfree 很好,你可以抽象掉数据争论的细节,但是一旦你使用 flip a bunch 你又回到了数据争论的杂草中,那么还有什么其他工具可以帮助你很好地使用 pointfree ?

*虽然这不是重点:如果 (<) 没有另一个名字的翻转版本,我们就不得不翻转它,所以总的来说我认为这条线以上更能代表我在这里使用的过程,以及你将如何得到一堆 flip

oddsquares = filter odd (map (^2) [1..];这不会改变,它只会降低其余部分的可读性。

sum (takeWhile (<10000) (filter odd (map (^2) [1..])))
  == sum (takeWhile (<10000) oddsquares)

首先将 (< 10000) 移动到更接近表达式的末尾。

  == sum (flip takeWhile oddsquares (<10000))  -- f x y == flip f y x

然后你可以引入几个组合运算符和一个 flip(使 (<) 部分正确):

  == sum . flip takeWhile oddsquares . flip (<) $ 10000  -- f (g x) == f . g $ x

你当然可以用(>)替换flip (<)(因为\x -> x < 10000等同于\x -> 10000 > x

  == sum . flip takeWhile oddsquares . (>) $ 10000

Jon Purdy 提出了一个关于使用部分来消除 flip 的好观点。我不介意自己明智地使用 flip,但为了完整起见,

sum . (`takeWhile` oddsquares) . (>) $ 10000

flip 是一个相当低级的用于编写 pointfree 代码的工具;一般可读性不是很好,特别是当翻转的表达式很长或者有多次翻转时。

好的经验法则:

  • 如果可用,使用运算符的翻转版本代替 flip

    • (>) ↔︎ (<)
    • (>>=) ↔︎ (=<<)
  • 如果仍然可读,请使用函数的一部分而不是 flip

    • flip map xs = (`map` xs)
  • 使用Applicative组合器将参数作为reader“环境”传递:

    • (f <$> g) x = f (g x)
    • (f <*> g) x = f x (g x)
    • liftA2 f x y = f <$> x <*> y
  • 为了操作多个参数,将它们包裹在一个元组中并在外面使用 curry 来呈现一个普通的柯里化类型:

    • multiplyAdd :: Num a => a -> a -> a -> a
      multiplyAdd = fmap curry $ curry
        $ (*) <$> fst <*> ((+) <$> fst . snd <*> snd . snd)
      
  • 使用 Arrow 和元组组合器来处理元组值:

    • (f *** g) (x, y) = (f x, g y)
    • (f &&& g) x = (f x, g x)
    • swap ~(x, y) = (y, x)
    • both f ~(x, y) = (f x, f y)
  • 将定义分解成许多小部分

sumOddSquaresLessThan = sum <$> oddSquaresLessThan
  where

    -- Alternatives:
    --
    -- * ‘(>)’ instead of ‘flip (<)’
    --
    -- * ‘flip takeWhile oddSquares <$> …’
    --   instead of ‘… <*> pure oddSquares’
    oddSquaresLessThan = takeWhile <$> flip (<) <*> pure oddSquares
    oddSquares = filter odd squares
    squares = map (^ 2) [1 ..]

这里 (f <$> g) x = (f . g) x = f (g x),映射“在”一个参数(B 组合器)下; (f <*> g) x = f x (g x) 正在应用一个参数(S 组合子);并且 pure x y = const x y = x,忽略参数(K 组合器)。这本质上是用于将 lambda 演算表达式转换为组合子演算(SKI、BCKW)的标准“抽象算法”:用 S 替换应用程序,在 K 中包装常量,使用带有 I(标识)的参数,并在带有B.