私は現在関数型プログラミングコースを受講していますが、高階関数と第一級市民としての機能の概念に非常に面白がっています。ただし、実際に役立つ、概念的に驚くべき、または単純に興味深い高階関数の多くはまだ考えられません。(典型的でかなり鈍い、、などの機能に加えてmap
)filter
。
そのような興味深い関数の例を知っていますか?
たぶん、関数を返す関数、関数のリスト(?)を返す関数などです。
私が現在学んでいる言語であるHaskellの例をいただければ幸いです:)
私は現在関数型プログラミングコースを受講していますが、高階関数と第一級市民としての機能の概念に非常に面白がっています。ただし、実際に役立つ、概念的に驚くべき、または単純に興味深い高階関数の多くはまだ考えられません。(典型的でかなり鈍い、、などの機能に加えてmap
)filter
。
そのような興味深い関数の例を知っていますか?
たぶん、関数を返す関数、関数のリスト(?)を返す関数などです。
私が現在学んでいる言語であるHaskellの例をいただければ幸いです:)
さて、Haskellにはループの構文がないことに気づきましたか?いいえwhile
または。do
_ for
これらはすべて高階関数であるため、次のようになります。
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プログラムがこれらの関数を使用するため、非常に便利です。
カスタム動作を汎用スケルトン関数にプラグインできるようになったため、これらは優れた抽象化への第一歩です。
特に、モナドは、プログラムを作成するために、一緒にチェーンし、関数を操作できるためにのみ可能です。
実は、一次の人生はかなり退屈です。プログラミングは、高次のものができて初めて面白くなります。
オブジェクト指向プログラミングで使用される多くの手法は、高階関数がない場合の回避策です。
これには、関数型プログラミングで広く使用されている多くのデザインパターンが含まれます。たとえば、ビジターパターンは、フォールドを実装するためのかなり複雑な方法です。回避策は、メソッドを使用してクラスを作成し、関数を渡す代わりに、クラスの要素を引数として渡すことです。
戦略パターンは、実際に意図されている関数の代わりにオブジェクトを引数として渡すことが多いスキームの別の例です。
同様に、依存性注入には、関数を引数として直接渡す方がよい場合が多い場合に、関数のプロキシを渡すための不格好なスキームが含まれることがよくあります。
したがって、私の答えは、高階関数は、OOプログラマーが実行するのと同じ種類のタスクを実行するためによく使用されますが、直接、ボイラープレートがはるかに少ないということです。
関数がデータ構造の一部になり得ることを知ったとき、私は本当に力を感じ始めました。これが「消費者モナド」(テクノバブル:無料のモナド(i ->)
)です。
data Coro i a
= Return a
| Consume (i -> Coro i a)
したがって、aCoro
は即座に値を生成するか、入力に応じて別のCoroになることができます。たとえば、これはCoro Int Int
:
Consume $ \x -> Consume $ \y -> Consume $ \z -> Return (x+y+z)
これは3つの整数入力を消費し、それらの合計を返します。また、入力に応じて異なる動作をさせることもできます。
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を始める前は語彙の一部ではありませんでした。
論文「解析のための高階関数でさえ、またはなぜ誰もが6次関数を使いたいと思うのか?」をチェックしてください。クリス・オカサキ。MLを使用して書かれていますが、アイデアはHaskellにも同様に当てはまります。
Joel Spolskyは、Javascriptの高階関数を使用してMap-Reduceがどのように機能するかを示す有名なエッセイを書きました。この質問をする人は必読です。
Haskellがどこでも使用しているカリー化には高階関数も必要です。基本的に、2つの引数を取る関数は、1つの引数を取り、1つの引数を取る別の関数を返す関数と同等です。Haskellでこのような型署名を見ると:
f :: A -> B -> C
...(->)
これは右結合法則として読み取ることができ、これが実際には次の型の関数を返す高階関数であることを示していますB -> C
。
f :: A -> (B -> C)
2つの引数のカリー化されていない関数は、代わりに次のような型になります。
f' :: (A, B) -> C
したがって、Haskellで部分適用を使用するときはいつでも、高階関数を使用しています。
MartínEscardóは、高階関数の興味深い例を提供します。
equal :: ((Integer -> Bool) -> Int) -> ((Integer -> Bool) -> Int) -> Bool
2つの汎関数が与えられた場合、とは有限の定義域を持っていなくても、とが(拡張的に)等しいかどうかf, g :: (Integer -> Bool) -> Int
をequal f g
決定します。実際、終域、は、決定可能な同等性を持つ任意のタイプに置き換えることができます。f
g
f
g
Int
Escardóが提供するコードはHaskellで書かれていますが、同じアルゴリズムがどの関数型言語でも機能するはずです。
Escardóが説明しているのと同じ手法を使用して、任意の精度で任意の連続関数の定積分を計算できます。
面白くて少しおかしなことの1つは、関数を使用してオブジェクト指向システムをシミュレートし、関数のスコープ(つまりクロージャ)にデータを格納することです。オブジェクトジェネレーター関数がオブジェクトを返す関数(別の関数)であるという意味で、これは高次です。
私の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は純粋関数型であるため、新しい関数を返すために集合演算を変更する必要があることを除いて)
私は高階メモ化の特別なファンです:
memo :: HasTrie t => (t -> a) -> (t -> a)
(任意の関数を指定して、その関数のメモ化されたバージョンを返します。関数の引数をトライにエンコードできる必要があるという事実によって制限されます。)
ここにいくつかの例があります:http://www.haskell.org/haskellwiki/Higher_order_function
私もこの本をお勧めします:http ://www.cs.nott.ac.uk/~gmh/book.htmlこれはHaskellのすべてへの素晴らしい入門書であり、高階関数をカバーしています。
高階関数はアキュムレータを使用することが多いため、より大きなリストから特定のルールに準拠する要素のリストを作成するときに使用できます。
これが小さな言い換えられたコードスニペットです:
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
「改行演算子」です。
Haskellでプログラミングするときは、それらを使用するだけです。高階関数がないのは、それらがどれほど信じられないほど便利であったかを理解したときです。
これは、他の誰も言及していないパターンで、初めて知ったときに本当に驚きました。入力としてサンプルのリストがあり、それらについてさまざまな統計を計算したい統計パッケージについて考えてみます(これを動機付ける方法は他にもたくさんあります)。肝心なのは、実行したい関数のリストがあるということです。それらすべてをどのように実行しますか?
statFuncs :: [ [Double] -> Double ]
statFuncs = [minimum, maximum, mean, median, mode, stddev]
runWith funcs samples = map ($samples) funcs
ここではあらゆる種類の高階関数が実行されており、その一部は他の回答で言及されています。しかし、私は「$」関数を指摘したいと思います。この「$」の使用を最初に見たとき、私はびっくりしました。それまでは、かっこの代わりに便利なもの以外はあまり役に立たないと思っていましたが、これはほとんど魔法のようでした...
特に実用的ではないにしても、一種の楽しいことの1つは、ChurchNumeralsです。これは、関数だけを使用して整数を表す方法です。クレイジー、私は知っています。<shamelessPlug>これが私が作成したJavaScriptの実装です。Lisp/Haskellの実装よりも理解しやすいかもしれません。(しかし、正直なところ、おそらくそうではありません。JavaScriptは、この種のことを実際に意図したものではありませんでした。)</ shamelessPlug>
Javascriptは、Joel Spolskyのエッセイなど、特定の高階関数をサポートしていると言われています。Mark Jason Dominusは、Higher– OrderPerlという本全体を書きました。この本のソースは、 PDFを含むさまざまな細かい形式で無料でダウンロードできます。
少なくともPerl3以来、PerlはCよりもLispを彷彿とさせる機能をサポートしてきましたが、クロージャの完全なサポートとそれに続くすべてが利用可能になったのはPerl5まででした。そして、最初のPerl 6実装のneは、Haskellで書かれました。これは、その言語の設計がどのように進歩したかに大きな影響を与えました。
Perlでの関数型プログラミングのアプローチの例は、特にmap
とgrep
:を使用した日常のプログラミングに現れます。
@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;
これ以上のものがたくさんありますが、これはただの味です。クロージャを使用すると、ビルトインを使用するだけでなく、関数発生器を簡単に作成して、独自の高階関数を作成できます。実際、より一般的な例外モデルの1つは、
try {
something();
} catch {
oh_drat();
};
組み込みではありません。try
ただし、これは、最初の引数でクロージャを取得する関数と、2番目の引数でクロージャを取得する関数の2つの引数を取る関数であるとほぼ簡単に定義されます。
Perl 5にはカリー化機能が組み込まれていませんが、そのためのモジュールがあります。ただし、Perl 6には、カリー化とファーストクラスの継続機能が組み込まれています。