33

我目前正在做一个函数式编程课程,我很高兴作为一等公民的高阶函数和函数的概念。但是,我还不能想到许多实际上有用的,概念上的惊人的,或者仅仅是简单有趣的高阶函数。 (除了典型的而非沉闷的map,filter等功能)。什么是高阶函数的一些有趣用途?

你知道这些有趣功能的例子吗?

也许函数返回函数,函数返回的功能列表(?)等

我很感激在Haskell,这是我目前正在学习:)

回答

45

那么,你注意到Haskell没有语法循环?否whiledofor。因为这些都只是高阶函数:

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] 

高阶函数替换需要在控制结构的语言烤语法,这意味着几乎每一个哈斯克尔程序使用这些功能 - 这使他们非常有用!

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

尤其是,monads只有可能,因为我们可以链接在一起,并操纵功能,创建程序。

事实是,当生命是一阶时,生活是相当无聊的。一旦你有更高的订单,编程只会变得有趣。

+0

...'forM'呢? :) – hvr 2011-04-26 19:46:11

+3

那么,单子会跟随,然后单子的高阶函数就会跟随。它一直都是海龟! – 2011-04-26 19:53:25

+4

非常好的答案。人们提出的一些“控制结构”非常不合常规,但非常有用。我想到的一个很好的例子是'quickCheck' - 一个控制操作符,它用任意输入多次运行提供的函数,试图使其失败。这是一个非常简单的概念,而高阶函数(和类型类)使它同样易于使用。只需'快速检查$ \ n xs - >长度(取n xs)<= n'并观察(对于负值'n',这是一个错误,顺便说一句,这是一个简单的疏漏,但quickCheck很容易捕捉到它)。 – mokus 2011-04-27 13:49:36

7

对于柯里尔也需要高阶函数,Haskell随处使用。实质上,一个带两个参数的函数相当于一个函数带一个参数并返回另一个带一个参数的函数。当你看到在Haskell这样的类型签名:

f :: A -> B -> C 

...的(->)可以解读为右结合,可见这实际上是一个高阶函数返回B -> C类型的函数:

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

的两个参数,一个非咖喱功能将改为有型是这样的:

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

因此,任何时候你用在Haskell部分应用程序,你正在使用更高阶的函数。

4

你可以做的一件有趣而有点疯狂的事是模拟一个面向对象的系统使用一个函数并将数据存储在函数的作用域(即闭包中)。它是高阶的,因为对象生成器函数是返回对象(另一个函数)的函数。

我Haskell是相当生疏,所以我不能轻易给你一个Haskell的例子,但这里有一个简单的Clojure例子希望传达的理念:

(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))))) 

用法:

(def a (make-object 10)) 

(a :get) 
=> 10 

(a :set 40) 

(a :get) 
=> 40 

同原则可以在Haskell中工作(除了您可能需要更改设置操作以返回一个新函数,因为Haskell是纯功能的)

+0

我不认为你可以说*模拟*面向对象的编程。这取决于你的OOP的定义:是继承(开放递归)还是不需要?如果是,那么你展示的不是一个对象,而只是一种常见的隐藏状态。如果不是,那么你显示的*是一个对象(而不是它的模拟),只能跟随一个没有被该语言标准化的对象协议。 – gasche 2011-04-26 17:12:18

+0

@gasche - 当然,这只是一个简单的例子。但是如果你想要增加继承,这很容易 - 例如你可以使用存储在哈希表中的方法实现来完成基于原型的继承(这也将是存储在OP的数据结构中的一阶函数的一个很好的例子!) – mikera 2011-04-26 17:23:04

+0

Haskell不是很容易,因为它非常严格类型安全,并没有分型的内在普遍概念。当语言没有提供内置的方法来判断一种类型是否可以(安全地和自动地)替换另一种类型时,使用继承是很困难的。否则,Haskell中的函数记录会构成一个非常有用的基于原型的对象系统。这对于使用动态类型或内置子类型的语言来说当然没有任何障碍。 – 2011-04-26 19:53:51

15

当我学会了一个函数可以成为数据结构的一部分的时候,我真的开始感受到这种力量。这是一个“消费者monad”(技术可能性:免费monad (i ->))。

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

所以一个Coro可以即刻产生一个值,或者是根据一些输入另一个科罗。例如,这是一个Coro Int Int

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

这会消耗三个整输入,并返回它们的总和。你也可以把它根据输入不同的表现:

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,然后消耗了产生它们的和以前更多的INTS。这可以被认为是一个函数,它可以采用任意多个参数,而不需要任何语言魔法,而只需要更高阶的函数。

数据结构中的函数是一个非常强大的工具,在我开始做Haskell之前,它不属于我的词汇表。

+1

其在第一个分支中的“返回累积”。另外,我觉得第一个被消费的元素作为一个与其他元素不同的语义有点肮脏。我宁愿将长度作为'subStream'的参数。 – gasche 2011-04-26 20:03:52

+0

@ gasche,谢谢修复。另外我同意它很脏 - 关键是这个“函数”的“签名”可以取决于它的参数,显示数据结构中函数结构的功能。 – luqui 2011-04-27 00:02:53

3

这里有一个小意译code片段:

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 

它使用几个 “高阶” 功能:

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 -notation“断行运营商”。

在Haskell中编程时,只需使用它们。如果你没有更高级的功能,那么当你意识到它们有多么非常有用时。

36

面向对象编程中使用的许多技术是缺少高阶函数的变通方法。

这包括在函数式编程中无处不在的design patterns的数字。例如,访客模式是实现fold的一种相当复杂的方式。解决方法是使用方法创建一个类,并将该类的元素作为参数传入,作为传递函数的替代方法。

strategy模式是一个方案的另一个例子,该方案经常将对象作为参数传递,以替代实际打算使用的函数。

类似地dependency injection通常会涉及一些笨拙的方案来传递函数的代理函数,因为通常直接将函数直接作为参数传递会更好。

所以我的答案是高阶函数通常用于执行面向对象编程人员执行的相同类型的任务,但直接而且使用更少的样板。

+7

就像旁注一样,OO没有任何关于阻止函数作为一流对象的功能。 SmallTalk和Ruby都具有一流的功能。 – 2011-05-03 05:46:06

+0

@约翰绝对。我作为第一类对象的第一次介绍是通过多年前学习Smalltalk。 – sigfpe 2011-05-17 17:22:22

9

Joel Spolsky写了一个famous essay演示Map-Reduce如何使用Javascript的高阶函数工作。任何提出这个问题的人都必须阅读。

+0

谢谢!我会检查它:) – 2011-04-27 21:40:34

2

有趣的是,如果不是特别实用,有一件事是Church Numerals。这是一种只用函数表示整数的方法。疯狂,我知道。 < shamelessPlug>这是我制作的implementation in JavaScript。它可能比Lisp/Haskell实现更容易理解。 (但可能不是,说实话,JavaScript并不是真正意义上的这种东西。)</shamelessPlug>

3

这里有一种模式,我还没有看到其他人提到,但第一次真的让我感到吃惊我了解了它。考虑一个统计软件包,其中有一个样本列表作为输入,并且您想计算一系列不同的统计数据(还有很多其他方法可以用来激发这一点)。底线是你有一个你想运行的功能列表。你如何运行它们?

statFuncs :: [ [Double] -> Double ] 
statFuncs = [minimum, maximum, mean, median, mode, stddev] 

runWith funcs samples = map ($samples) funcs 

有各种各样的高阶善良在这里进行,其中一些已在其他答案中提到。但我想指出'$'功能。当我第一次看到'$'的使用时,我被吹走了。在此之前,我从来没有认为这是不是作为一个方便的替代括号中是非常有用的其他...但是这几乎是不可思议......

+0

好的。我应该先阅读有关$函数的信息。让我们看... – 2011-04-27 21:44:04

+0

'($)::(a - > b) - > a - > b'它只是一个普通函数应用程序的运算符。 “f $ a”与“f a”相同。这是正确的联想,并有一个优先权0. – mightybyte 2011-04-28 00:46:47

+0

欲了解更多详细信息检查此问题http://stackoverflow.com/questions/940382/haskell-difference-between-dot-and-dollar-sign – mightybyte 2011-04-28 16:00:26

4

我高阶记忆化的特定风扇:

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

(考虑任何函数,返回功能的事实,该函数的参数必须能够被编码到特里有限公司的memoized版本)。

这是http://hackage.haskell.org/package/MemoTrie

+0

喜欢这一个!谢谢! – 2011-04-27 21:42:08

2

有人提到,JavaScript支持某些高阶功能,包括an essay from Joel Spolsky。马克杰森多米诺斯写了一本名为Higher–Order Perl的全书;本书的源代码可以免费下载到各种精细格式,包括PDF

自从至少Perl 3以来,Perl支持的功能更多地让人联想到Lisp而不是C,但直到Perl 5完全支持闭包以及所有后续的功能才可用。第一个Perl 6实现的ne是用Haskell编写的,它对这个语言的设计进展有很大的影响。的在Perl功能的编程方法

实例在日常的编程显示出来,特别是mapgrep

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

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

由于sort也承认的闭合,所述map/sort/map图案是超级常见:

@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/*"); 

@sorted_lines = map { $_->[0] } 
       sort { 
        $a->[4] <=> $b->[4] 
          || 
        $a->[-1] cmp $b->[-1] 
          || 
        $a->[3] <=> $b->[3] 
          || 
        ... 
       } 
       map { [$_ => reverse split /:/] } @lines; 

reduce功能使得列表两轮牛车轻松无循环:

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

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

还有比这多很多,但是这仅仅是一个味道。闭包可以很容易地创建函数发生器,编写自己的高阶函数,而不仅仅是使用内置函数。事实上,比较常见的异常机型之一,

try { 
    something(); 
} catch { 
    oh_drat(); 
}; 

一个内置。然而,它几乎被简单定义为try是一个函数,它接受两个参数:第一个arg中的闭包和第二个中的闭包。

Perl 5没有currying内置,虽然有一个模块。不过,Perl 6具有内置的柯里化和一流的延续,还有更多。

6

马丁Escardó提供an interesting example of a higher-order function

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

给定两个函f, g :: (Integer -> Bool) -> Int,然后equal f g决定是否fg是(外延上)等于或不,即使fg没有一个有限区域。事实上,共域,Int,可以用任何可判定的等式来替换。

Escardó给出的代码是用Haskell编写的,但是相同的算法应该可以在任何函数式语言中使用。

您可以使用Escardó描述的相同技术来计算任意连续函数对任意精度的定积分。