2
Help us understand the problem. What are the problem?

posted at

updated at

Lazy evaluation, Currying を JavaScript で解説してみた(遅延評価、カリー化)

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

image.png

また、最初の引数だけを返す関数があります。

function takeFirst(a, b) {
    return a
}

関数takeFirst()に渡される前に第2引数が評価されるため、takeFirst(true, hang()) を呼び出すとJavascriptがハングアップしてしまうのです。

image.png

コンパイラは第2引数のhang()を実行する必要がないと思いませんか?第2引数はtakeFirst()の中でさえ使われていないのです。Haskellは同じコードを何のエラーもなく実行します。なぜHaskellはそれができるのか?
image.png

Haskellの秘密はLazy評価です。遅延評価は、値が必要になるまで式の評価を保留する戦略です(Tutorialspoint, n.d.)。Javascriptで関数を引数として使用する際にも、同じことができます。次の画像の3つの呼び出しは同じ意味で、第2引数としてまだ実行されていない関数を渡しているだけです。関数化は、言い換えれば実行を遅らせることができる。

image.png

次のイメージでjavascriptの実装を見ると、HaskellがlazuSum()の内部で何をしているのか理解できるかもしれません。遅延評価は、引数を関数として受け取り、必要なときに関数の内部で実行することで実現されます。

image.png

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つの数値を足し算します。

image.png

一見魔法のように見えますが、unary function は再帰的に複数の数値を足すことができます。このテクニックは、引数の数を最大でも1つに制限するcurryingと呼ばれるものです。簡単に言うと、curryingは引数の関数化である。Curryingの反意語はUncurryingで、defunctionalization の形を意味する。

image.png

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
2
Help us understand the problem. What are the problem?