広く使用されている Lispで自動メモ化がサポートされているかどうかは定かではありませんが、メモ化が関数型言語でより一般的である理由が 2 つあり、Lisp ファミリーの言語ではもう 1 つの理由があると思います。
まず第一に、人々は関数型言語で関数を書きます。つまり、結果が引数のみに依存し、環境に悪影響を与えない計算です。その要件を満たさないものは、メモ化の対象にはなりません。そして、まあ、命令型言語は、これらの要件が満たされていない、または満たされていない可能性がある言語にすぎません。
もちろん、(ほとんどの) Lisp のような単に関数型に適した言語であっても、注意が必要です。たとえば、次のようなものはメモ化しないでください。
(defvar *p* 1)
(defun foo (n)
(if (<= n 0)
*p*
(+ (foo (1- n)) (foo (- n *p*)))))
第二に、関数型言語は一般的に不変のデータ構造について話したいということです。これは、次の 2 つのことを意味します。
- 大きなデータ構造を返す関数をメモ化することは実際には安全です
- 非常に大きなデータ構造を構築する関数は、中間構造を変更できないため、膨大な量のガベージを作成する必要があることがよくあります。
(2) は少し物議を醸しています: 受け入れられた知恵は、GC は今では問題にならないほど優れている、コピーは非常に安価である、コンパイラーは魔法を行うことができる、などです。まあ、そのような関数を書いたことがある人は、これが部分的にしか真実ではないことを知っているでしょう: GCは優れており、コピーは安価です (しかし、それらをコピーするためにポインターを追跡する大きな構造は、しばしばキャッシュに対して非常に敵対的です)。彼らが行うと主張されている魔法を行うことはほとんどありません)。したがって、機能しないコードにむやみに頼ってごまかすか、メモを取るかのどちらかです。関数をメモ化すると、すべての中間構造を一度だけ構築するだけで、すべてが安価になります (メモリ以外では、メモ化の適切な弱点はそれを処理できます)。
第三に、言語が簡単なメタ言語的抽象化をサポートしていない場合、メモ化を実装するのは非常に困難です。別の言い方をすれば、Lisp スタイルのマクロが必要です。
関数をメモ化するには、少なくとも 2 つのことを行う必要があります。
- メモ化のキーとなる引数を制御する必要があります。すべての関数が引数を 1 つだけ持つわけではなく、複数の引数を持つすべての関数を最初にメモ化する必要があるわけではありません。
- 自己末尾呼び出しの最適化を無効にするには、関数内に介入する必要があります。これにより、メモ化が完全に破壊されます。
簡単すぎてちょっと残酷ですが、Python をからかうことでこれを実証します。
デコレーターは、Python で関数をメモ化するために必要なものだと思うかもしれません。実際、デコレーターを使用してメモ化ツールを作成することもできます (私はそれらのツールをたくさん作成しました)。そして、これらはほとんど偶然に行われますが、ある種の仕事ですらあります。
まず、デコレーターは、デコレートしている関数について簡単に知ることができません。したがって、関数へのすべての引数のタプルに基づいてメモ化しようとするか、メモ化する引数をデコレータで指定する必要があるか、または同様に厄介なものになります。
第 2 に、デコレータは、デコレートしている関数を引数として取得します。関数内をいじることはできません。もちろん、Pythonは「1956年以降に発明された概念はありません」というポリシーの一部として、f
の定義内の(そして介在するバインディングなしの)レキシカルf
への呼び出しが実際には自己呼び出しであると想定していないため、それは実際には問題ありません。しかし、おそらくいつの日かそれが起こり、すべてのメモ化が壊れるでしょう。
要約すると、関数を確実にメモ化するには、Lisp スタイルのマクロが必要です。おそらく、それらを持つ命令型言語は Lisp だけです。