University of the People という無料の大学に行ってます。そこで書いたdiscussionを自分用に保存しておきたいので、ここに記録します。もとは英語で書いてて deepL するだけなので、日本語が変でもすみません。どうしようもない単語だけ英語に戻してます。
lazy evaluation = 遅延評価
higher order functions = 高階関数?
currying = カリー化
partial applicaiton = 部分適用
だと思ってますが、何分学生でこれを初めて勉強してる状態で、学生が課題で書いた内容ですので、間違ってるところは多々あるという前提でお読みください
なお、記事全体は Tsoding さんのyoutubeそのまんま なので、そっち見たほうが早いです、が、動画が1時間あるので、要約として見てもらえれば的なアレです。
質問1:レイジー評価について - Lazy evaluation
まず、Lazy評価なしで問題に直面した場合について、Tsodingの例を使って説明します。ここでは、無限ループのために常にハングアップしてしまうjavascriptの関数があります。
function hang() {
hang()
}
また、最初の引数だけを返す関数があります。
function takeFirst(a, b) {
return a
}
関数takeFirst()に渡される前に第2引数が評価されるため、takeFirst(true, hang())
を呼び出すとJavascriptがハングアップしてしまうのです。
コンパイラは第2引数のhang()を実行する必要がないと思いませんか?第2引数はtakeFirst()の中でさえ使われていないのです。Haskellは同じコードを何のエラーもなく実行します。なぜHaskellはそれができるのか?
Haskellの秘密はLazy評価です。遅延評価は、値が必要になるまで式の評価を保留する戦略です(Tutorialspoint, n.d.)。Javascriptで関数を引数として使用する際にも、同じことができます。次の画像の3つの呼び出しは同じ意味で、第2引数としてまだ実行されていない関数を渡しているだけです。関数化は、言い換えれば実行を遅らせることができる。
次のイメージでjavascriptの実装を見ると、HaskellがlazuSum()
の内部で何をしているのか理解できるかもしれません。遅延評価は、引数を関数として受け取り、必要なときに関数の内部で実行することで実現されます。
Lazy評価の最大の利点は、無駄な実行を減らすことによるパフォーマンスの向上です。上の画像でsum()とlazySum()を使って次のお題を説明します。
問題2-1:高階関数(HOF) - Higher order functions(HOF)
"高次関数とは、関数を引数にとる関数、または関数を返す関数のことです。"(Eliott, 2017) 一般的なHOFは、map()、filter()、zipWidth()などです。したがって、私のsum()はHOFではなく、lazySum()は関数を引数に取るのでHOFとなります。次の段落の別の例 sumOneArg() は、関数を返すので HOF です。
質問2-2: Currying - カリー化
currying を理解するためには、Haskell の不思議な性質を受け入れなければなりません。つまり、Haskellにおける関数とは、実際には引数を1つだけ取る単項関数 - unary function なのです。えっ、Haskellでは複数の引数を取る関数を宣言できるんだ、と言われるかもしれませんね。つまり、複数の引数はコンパイラによって分解され、実行時に1つの引数しか取らない関数に変換されて実行されるんだ。次の例は、sum()を1つの引数だけで実装する方法です。実際には、2つの数値を足し算します。
一見魔法のように見えますが、unary function は再帰的に複数の数値を足すことができます。このテクニックは、引数の数を最大でも1つに制限するcurryingと呼ばれるものです。簡単に言うと、curryingは引数の関数化である。Curryingの反意語はUncurryingで、defunctionalization の形を意味する。
curryingの利点は、コード中の部分的な適用を可能にすることである。遅延評価は引数の使い方によって実行タイミングを遅らせますが、引数が関数の場合のみ実現可能です(質問1の最初の例参照)。つまり、遅延評価を実現するためには、curryingによる関数化が不可欠なのです。
部分的な適用 - Partial application
一般的な命令型言語において、ある関数にすべての引数を渡す必要がある。
a = getA() // 1
b = getB() // 2
sum(a, b) // 3
しかし、この方法では、すべての引数が揃うまでsum()を呼び出すことができないので不便である。一方、Haskellでは、引数を一つずつ渡していき、それが必要になるまで実行を遅らせることができる。
a = getA() // 1
// bは時間がかかるので、aを先に置く
tmpFuncion = sum(a) // 部分的に実行する。戻り値は関数である。
... // 長い処理
b = getB() // 最後にb = 2を得る
tmpFunction(b) // 残りの処理が実行される
まとめ
関数型言語には、命令型言語とは異なる性能を向上させる戦略がある。currying、高階関数、遅延評価の考え方は、Javascriptのコールバック関数など、他の言語でも活用されている。Haskellでは反復処理(loop)が使えないなどの制限があるが、そのようなハンディキャップはなく、むしろ可読性や性能を向上させる可能性がある。
Reference
Tutorialspoint. (n.d.). Functional Programming - Lazy Evaluation. Retrieved on May 23, 2022, from https://www.tutorialspoint.com/functional_programming/functional_programming_lazy_evaluation.htm
Tsoding. (2020). YouTube. Retrieved on May 23, 2022, from https://www.youtube.com/watch?v=E5yAoMaVCp0&feature=emb_title
Elliott, E. (2017). https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99. Retrieved on May 23, 2022, from https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99