关于haskell:高阶函数有哪些有趣的用途?

What are some interesting uses of higher-order functions?

我现在正在做一个函数式编程课程,我对高阶函数的概念很感兴趣,因为它是一等公民。然而,我还不能想到许多实际有用的、概念上令人惊奇的,或者仅仅是简单有趣的高阶函数。(除了典型的、相当沉闷的mapfilter等功能外)。

你知道这些有趣的函数的例子吗?

可能是返回函数的函数,返回函数列表的函数(?)等。

我很欣赏Haskell的例子,这是我目前正在学习的语言:)


你注意到haskell没有循环的语法吗?无whiledofor。因为这些都是高阶函数:

1
2
3
4
5
6
7
8
9
 map :: (a -> b) -> [a] -> [b]

 foldr :: (a -> b -> b) -> b -> [a] -> b

 filter :: (a -> Bool) -> [a] -> [a]

 unfoldr :: (b -> Maybe (a, b)) -> b -> [a]

 iterate :: (a -> a) -> a -> [a]

更高阶的函数取代了对控制结构语言中烘焙语法的需要,这意味着几乎每个haskell程序都使用这些函数——使它们非常有用!

它们是实现良好抽象的第一步,因为我们现在可以将自定义行为插入通用的框架函数中。

特别是,monad是唯一可能的,因为我们可以连接在一起,操作函数来创建程序。

事实上,当生活是第一秩序的时候,生活是很无聊的。只有当你有更高的阶时,编程才会变得有趣。


由于缺乏高阶函数,OO编程中使用的许多技术都是解决方法。

这包括一些在函数式编程中普遍存在的设计模式。例如,访问者模式是实现折叠的一种相当复杂的方法。解决方法是用方法创建一个类,并将类的元素作为参数传入,作为传入函数的替代方法。

策略模式是一个方案的另一个例子,该方案通常将对象作为参数传递,以替代实际需要的函数。

类似地,依赖注入常常涉及一些笨拙的方案来传递函数的代理,而直接作为参数传递函数通常会更好。

所以我的答案是,高阶函数通常被用来执行与OO程序员执行的相同类型的任务,但是直接执行,并且使用的样板文件要少得多。


当我了解到一个函数可以成为数据结构的一部分时,我真的开始感觉到它的强大。这里有一个"消费者单子"(technoballe:free monad over (i ->))。

1
2
3
data Coro i a
    = Return a
    | Consume (i -> Coro i a)

因此,Coro既可以立即产生一个值,也可以是另一个coro,这取决于某些输入。例如,这是一个Coro Int Int

1
Consume $ \x -> Consume $ \y -> Consume $ \z -> Return (x+y+z)

这将消耗三个整数输入并返回它们的和。您还可以根据输入使其行为不同:

1
2
3
4
5
sumStream :: Coro Int Int
sumStream = Consume (go 0)
    where
    go accum 0 = Return accum
    go accum n = Consume (\x -> go (accum+x) (n-1))

这将消耗一个int,然后在生成它们的和之前消耗更多的int。这可以被认为是一个函数,它接受任意多个参数,构造时没有任何语言魔力,只是高阶函数。

数据结构中的函数是一个非常强大的工具,在我开始使用haskell之前,它不是我词汇表的一部分。


请看这篇论文中的更高阶函数来解析,或者为什么有人想要使用六阶函数?'作者:克里斯·冈崎。它是用ML编写的,但这些想法同样适用于Haskell。


JoelSpolsky写了一篇著名的文章,演示了如何使用Javascript的高阶函数来实现map reduce。A对于任何提出此问题的人都必须阅读。


更高阶函数也需要电流,这是哈斯克尔使用的任何地方。本质上,一个接受两个参数的函数等价于一个接受一个参数并返回另一个接受一个参数的函数。当您在haskell中看到这样的类型签名时:

1
f :: A -> B -> C

(->)可以理解为右相关函数,表明这实际上是一个返回B -> C类型函数的高阶函数:

1
f :: A -> (B -> C)

由两个参数组成的非循环函数将具有如下类型:

1
f' :: (A, B) -> C

所以,只要在Haskell中使用部分应用程序,就可以使用高阶函数。


mart_n escard_提供了一个有趣的高阶函数示例:

1
equal :: ((Integer -> Bool) -> Int) -> ((Integer -> Bool) -> Int) -> Bool

给定两个函数f, g :: (Integer -> Bool) -> Int,那么equal f g决定fg是否(扩展)相等,即使fg没有有限域。事实上,密码子Int可以被任何类型的密码子所取代,并且具有可判定的平等性。

escard_给出的代码是用haskell编写的,但是相同的算法应该可以在任何函数语言中工作。

您可以使用与escard_描述的相同的技术,以任意精度计算任意连续函数的定积分。


我特别喜欢高阶记忆:

1
memo :: HasTrie t => (t -> a) -> (t -> a)

(给定任何函数,返回该函数的memoized版本。受函数参数必须能够编码到trie的事实限制。)

这是来自http://hackage.haskell.org/package/memotrie


你能做的一件有趣且有点疯狂的事情是使用一个函数来模拟一个面向对象的系统,并在函数的作用域(即在一个闭包中)存储数据。在这个意义上,对象生成器函数是返回对象的函数(另一个函数)。

我的haskell有点生疏,所以我不能很容易地给你一个haskell的例子,但这里有一个简化的clojure例子,希望能传达这个概念:

1
2
3
4
5
6
(defn make-object [initial-value]
  (let [data (atom {:value initial-value})]
      (fn [op & args]
        (case op
          :set (swap! data assoc :value (first args))
          :get (:value @data)))))

用途:

1
2
3
4
5
6
7
8
9
(def a (make-object 10))

(a :get)
=> 10

(a :set 40)

(a :get)
=> 40

同样的原理也适用于haskell(除了您可能需要更改set操作以返回一个新函数,因为haskell是纯函数的)


这是一个模式,我还没有看到其他人提到过,当我第一次了解它时,这真的让我吃惊。考虑一个统计数据包,其中您有一个样本列表作为输入,并且您希望计算一组不同的统计数据(还有很多其他方法可以激发这一点)。底线是您有一个要运行的函数列表。你怎么管理他们?

1
2
3
4
statFuncs :: [ [Double] -> Double ]
statFuncs = [minimum, maximum, mean, median, mode, stddev]

runWith funcs samples = map ($samples) funcs

这里有各种各样的高阶善,其中一些在其他答案中提到过。但我想指出"$"函数。当我第一次看到使用"$"时,我被炸飞了。在那之前,我认为它除了作为括号的一个方便的替代品之外没有什么用处……但这几乎是不可思议的……


下面是一个小的解释代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rays :: ChessPieceType -> [[(Int, Int)]]
rays Bishop = do
  dx <- [1, -1]
  dy <- [1, -1]
  return $ iterate (addPos (dx, dy)) (dx, dy)
...  -- Other piece types

-- takeUntilIncluding is an inclusive version of takeUntil
takeUntilIncluding :: (a -> Bool) -> [a] -> [a]

possibleMoves board piece = do
  relRay <- rays (pieceType piece)
  let ray = map (addPos src) relRay
  takeUntilIncluding (not . isNothing . pieceAt board)
    (takeWhile notBlocked ray)
  where
    notBlocked pos =
      inBoard pos &&
      all isOtherSide (pieceAt board pos)
    isOtherSide = (/= pieceSide piece) . pieceSide

这使用了几个"高阶"功能:

1
2
3
4
5
6
7
iterate :: (a -> a) -> a -> [a]
takeUntilIncluding  -- not a standard function
takeWhile :: (a -> Bool) -> [a] -> [a]
all :: (a -> Bool) -> [a] -> Bool
map :: (a -> b) -> [a] -> [b]
(.) :: (b -> c) -> (a -> b) -> a -> c
(>>=) :: Monad m => m a -> (a -> m b) -> m b

(.).操作符,(>>=)do符号"换行操作符"。

在Haskell中编程时,只需使用它们。没有高阶函数的地方是当你意识到它们是多么的有用。


这里有几个例子:http://www.haskell.org/haskellwiki/higher_order_function

我也推荐这本书:http://www.cs.nott.ac.uk/~gmh/book.html,这是对所有haskell的很好介绍,涵盖了更高阶函数。

高阶函数通常使用一个累加器,因此在从一个更大的列表中形成符合给定规则的元素列表时可以使用它。


有人提到javascript支持某些高阶函数,包括JoelSpolsky的一篇文章。MarkJasonDomius写了一本名叫高阶Perl的整本书;该书的来源可以免费下载,包括PDF格式。

至少从Perl3开始,Perl支持的功能比C更让人想起Lisp,但直到Perl5才完全支持闭包以及随后的所有功能。第一批Perl6实现中有一个是用Haskell编写的,它对该语言的设计进展有很大的影响。

Perl中的函数式编程方法示例出现在日常编程中,特别是在mapgrep中:

1
2
3
@ARGV    = map { /\.gz$/ ?"gzip -dc < $_ |" : $_ } @ARGV;

@unempty = grep { defined && length } @many;

由于sort也承认关闭,map/sort/map模式非常常见:

1
2
3
4
5
6
7
8
9
10
11
@txtfiles = map { $_->[1] }
            sort {
                    $b->[0]  <=>     $a->[0]
                              ||
                 lc $a->[1]  cmp  lc $b->[1]
                              ||
                    $b->[1]  cmp     $a->[1]
            }
            map  { -s => $_ }
            grep { -f && -T }
            glob("/etc/*");

1
2
3
4
5
6
7
8
9
10
11
@sorted_lines = map { $_->[0] }
                sort {
                     $a->[4] <=> $b->[4]
                             ||
                    $a->[-1] cmp $b->[-1]
                             ||
                     $a->[3] <=> $b->[3]
                             ||
                     ...
                }
                map { [$_ => reverse split /:/] } @lines;

reduce功能使列表黑客无需循环即可轻松进行:

1
2
3
$sum = reduce { $a + $b } @numbers;

$max = reduce { $a > $b ? $a : $b } $MININT, @numbers;

还有很多,但这只是一种品味。闭包使创建函数生成器变得容易,可以编写自己的高阶函数,而不仅仅是使用内置函数。实际上,更常见的异常模型之一,

1
2
3
4
5
try {
   something();
} catch {
   oh_drat();
};

不是内置的。然而,它几乎是琐碎的定义,其中try是一个接受两个参数的函数:第一个参数中的闭包和第二个参数中的闭包。

Perl5没有内置的currying,尽管它有一个模块。不过,Perl6内置了currying和一流的延续,还有更多。


有一件事很有趣,如果不是特别实用的话,那就是教会数字。它是一种只使用函数来表示整数的方法。疯了,我知道。这里是我用javascript实现的。它可能比Lisp/Haskell实现更容易理解。(老实说,可能不是。javascript并不是真的用于这类事情。)