HN 日本語サマリー

← 一覧へ戻る
プログラミング

関数引数は関数の色ではない

Function Arguments Are Not Function Colors (jerf.org)

26 pointsby ingve10 コメント

要約

関数引数が「色」として扱われることがあるという議論に対し、筆者は、引数が単に呼び出しスタック全体に影響を及ぼす「色」とは異なり、変更が直接の呼び出し元にのみ伝播する「通常の変更」とは区別されるべきだと論じている。Goのcontextのような例は、変更の伝播範囲が限定的であるため「色」ではないと分析し、関数の変更が呼び出しスタック全体に影響を与える場合にのみ「色」と定義できると提唱している。

全文翻訳

関数引数は関数の色ではない

長年にわたり、オンラインでの関数の色に関する様々な議論の中で、関数引数も色を構成しうるという、一見もっともらしい意見が出てきました。

例えば、Go言語にはcontext.Contextという値があり、タイムアウト、キャンセル、そして様々な関数にスレッドできる少量のデータを管理します。これは、受信者が直接使用しなくても、単に渡すだけでよいものを関数間で渡していくことを意図して設計されています。一般的に、ある関数がコンテキストの使用を開始したら、それを必要とする可能性のあるすべての呼び出し関数に渡すべきです。

したがって、表面的にはこれは色のように見えます。この特定の引数を手に入れたら、将来その型のすべての関数をその引数で呼び出さなければならない、ということです。

これを拡張して、一部の人が主張するように、すべての関数パラメータが「色」であると論じることもできます。

迅速ながらも重要な反論として、もしこれが事実なら、私たちは今これを議論していないだろうと観察できます。もし非同期処理が、数千もの他の色のきらびやかな混沌とした虹の中の一つの「色」に過ぎないなら、この「色」という概念は、プログラミング言語の関数が発明されてから50年以上経っても発明されなかったでしょう。一部の言語における非同期処理の動作には、捉えて理解する価値のある、依然として何か独特なものがあります。

しかし、一つの例から状況を深く理解することは困難です。ここでは、それを単一の例ではなく、基準にしようと試みます。

色を変化依存グラフの形状として捉える

問題をこのように表現しましょう:

呼び出しスタックの4段下にある関数の属性を変更しなければならないと仮定します。それは、その関数が取るパラメータであるか、それが「非同期」であるか否か、あるいは「ASMが含まれているか否か」のような他の属性であるか、あるいは関数が持つ他のどのような属性であっても構いません。ローカル言語が関数に持たせることを許可する特性に応じて、関数へのどのような変更でも発生し得ます。

この変更により、呼び出し元を変更する必要が生じる可能性があります。

現れる可能性のある基本的なケースは3つあります:

  1. 呼び出し元のいずれにも変更は必要ない。
  2. 変更は直接の呼び出し元の変更を必要とする可能性がある。
  3. 変更はスタック内のすべての関数の変更を必要とする可能性がある。

図示すると、以下のようになります。色がない、関数パラメータの場合、「変更の必要性」の関係は次のようになります:

[画像: スタック内の5つの関数を示す図。それらの連鎖リスト表現があり、各関数は前の関数を変更する必要があることを示している]

色の場合は、次のようになります:

[画像: スタック内の5つの関数を示す図。しかし、この場合は一番下の関数がすべての呼び出し元に直接リンクされている]

「色」と「通常の」変更の違いは、色のケースでは、スタックの一番下の関数への変更はその親に影響するだけでなく、その上のすべての関数に即座に、そして避けがたく影響するため、それらすべてが変更に対応しなければならないということです。

「通常の」変更の場合、それは呼び出し元にのみ伝播します。その後、その呼び出し元に連鎖する可能性があります。図に示されているように、それがトップレベル関数まで伝播した場合、それは違いのない区別のように見えるかもしれません。

しかし、重要なのは、図が一般的なケースを示しているのではなく、最悪のケースを示しているということです。実際には、関数はチェーンがスタックのトップに到達する前に、ほぼ常にチェーンを断ち切ります。どの関数でも、伝播する変更を「キャッチ」してカプセル化できるため、それより上の関数はそれを認識しなくなります。

これを、実際にスタックを上に伝播させる変更をどれくらいの頻度で行うかで測定できます…まあ…手作業でプログラミングしていた昔はそうでした。それが、あなたがすることであることは知っていますが、それは決して常にすることではなく、トップのメイン関数までずっとすることもしばしばありませんでした。

変更の分離

私たち人間は、ほとんどの関数変更に対して、上位の関数が下位の関数のすべての詳細を知る必要性をなくす能力があるため、構造化プログラミングを広く利用しています。

プログラム全体のすべての関数パラメータの変更が、プログラム全体の呼び出しスタック全体にわたって変更を強制するようになったら、どうなるでしょうか?

明らかにこれは起こりません。それはあまりにも起こらないので、私が何を話しているのか想像することさえ難しいかもしれません。

例えば、私が常に持ち出しているGoのコンテキストの場合、Goが私の主要なプログラミング言語の一つであるだけでなく、この例が繰り返し出てくるためですが、どこからでも関数がcontext.Background()を呼び出して、退化したものの完全に機能するコンテキスト値を取得できます。

したがって、コンテキストを受け取るように変更された関数は、context.Background()の結果を渡すだけで、他の通常の関数から呼び出すことができます。さらに、これは仮説的な状況ではなく、私が以前は持っていなかった関数にcontext.Contextパラメータを追加するときに、テストコードやコンテキストを気にしない他の呼び出し元を新しいパラメータに適応させるために、実際のコードでまさにそれを行うことは非常に一般的です。

したがって、この「連鎖リスト」の関係は、実際にはチェーンをメインに到達する前に断ち切るだけでなく、非常に、非常に早期にチェーンを断ち切る傾向があります。変更を最後までスレッドしなければならないのは苦痛なので、私たちはそれをしないための多くの方法を開発し、関数が提供するカプセル化を改善してきました。時々そうしなければならないかもしれませんが、それは非常に認知的に利用可能で、覚えやすく、気づきやすい稀なイベントの種類であり、その結果、記憶の中で完全に過大評価されています。実際には、それは一般的なケースではなく、非常に稀なケースです。

通常の変更はカプセル化できます。非常に特定の変更は、何らかの理由でメインまで上がる必要があるかもしれませんが、これは明らかに例外であり、それでもプログラムの残りの部分はカプセル化するこの能力を他の方法で使用する可能性があります。

A color change escapes this encapsulation. All functions above it in the stack must match its requirements. (色の変更はこのカプセル化から逃れます。スタックの上にあるすべての関数は、その要件に一致しなければなりません。)

色の定義

依存関係の「色」の形状を持つ関数へのどのような変更も色です。連鎖リストの形状を持つ関数へのどのような変更も色ではありません。

元の定義エッセイでは非同期処理をその標準的な例として使用しましたが、それはすべての言語のすべての非同期処理が常に色であるという意味ではありません。言語の非同期処理サポートの具体性を調べて、それが一致するかどうかを確認する必要があります。多くの言語ではそれは色ではありません。私は、Zigの現在のいくつかの変更により、IOを行う関数は、どのIo値を渡すかによって色であるか否か、あるいはむしろ、色が関数自体ではなくIo値の属性であるかによって異なると考えています。非同期処理が色であるかどうかは、定義に合致するかどうかによります。定義は、非同期処理が常に色であるということではありません。

また、標準的な「JavaScriptの非同期処理」のケースでさえ、私たちが望むほど明るく輝かしい線ではないという事実によって、これはさらに複雑になります。JavaScriptでは、同期コンテキストで非同期アクションを実行することは依然として可能です。それは、同時に実行されるべき他の非同期アクションのコストを犠牲にするだけです。私はこれを「実際には非同期処理を実行していない」と見なすでしょう。そのコストの驚異的な性質と、それが機能の本質的な機能を破壊し、かろうじて機能するだけであることを考えると。しかし、あなたの経験は異なるかもしれません。すべての場合と同様に、詳しく見れば見るほど、それは二項関係というよりもスペクトラムになり、スペクトラムはより複雑になるように思えます。

しかし、物事がスペクトラムであり、線を引くことが当初思っていたよりも複雑になるからといって、色の変更と通常の変更の間に実際の違いがないわけではありません。

高度なトピック:部分的な色

高度な理解のために、この観点から、部分的な色を想像することもできます。Haskellは部分的な色の王様であり、それらを使用して、介入する関数がどのように制約されるかについて強力な保証を行います。

例えば、ソフトウェアトランザクションメモリ(STM)モナドSTMは、トランザクション値の作成を可能にすることによってソフトウェアトランザクションメモリを実装します。これらの値は、STMモナドコンテキスト内からreadTVarのような関数を通じてのみアクセスできます。

STM関数は、純粋関数を呼び出すことができ、その純粋関数は別の純粋関数を呼び出すことができます。しかし、その純粋関数がSTM内の何かを使用する必要がある場合、それはその呼び出しとSTMの以前の使用との間のスタック全体に伝播し、介入するすべての関数をSTM「色」にリフトすることを強制します。この色は、STMモナド内のコード、結果のトランザクション性を保証するSTMメカニズムを通じて実行されるものだけが、トランザクション変数にアクセスできるという保証を強制するために使用されます。

しかし、それは呼び出しスタックの上にある関数の一部に対する非局所的で色のような相互作用ですが、呼び出しスタックのトップまでは伝播しません。トップレベルのHaskellプログラムの型はIO ()です。STMはその型シグネチャに現れません。IO以外のものはすべて、main関数自体の下のある場所にカプセル化されていることが保証されます。

これは確かに色です。なぜなら、これはHaskellのIO色に乗っているからです。STMトランザクションはIOでのみ実行できます。そのため、型チェッカーはTVar aを純粋関数に渡すことを許可しますが、純粋関数はunsafePerformIOなしではそれを何もできません。Haskellは、コードベースの一部に適用できるIOの「色合い」を追加することを許可します。

当初、私はこれをより単純なStateの観点から書こうとしましたが、作業を進めるうちにStateは色ではないことに気づきました。関数が状態全体を渡され、その状態で他の純粋関数を呼び出し、その純粋関数がそれを使って独自のStateベースの計算を構築することを妨げるものは何もありません。もし望むなら、その新しい状態を元のコンテキストまでスレッドバックすることができます。それは不格好で慣用的ではないでしょうが、合法であり、道徳的でもあります。

したがって、Haskell内のすべてのモナドが色であるわけではなく、何らかの方法でIO内でしか解決できないものだけが色です。しかし、Haskellは、そのメカニズムで追加の「色」を作成することを許可します。

ほとんどの言語では、このように独自の「色」を追加することは許可されていないと考えるのが妥当だと思います。しかし、これもまた、各言語の専門家にそれが事実かどうかを議論してもらうことにします。

最後に、色は、介入する呼び出しを飛び越え、関連する色の島内の他のすべての関数に変更を強制するという特性を持たなければならないことを繰り返します。上記のSTMの場合、トランザクション変数にアクセスしたいものはすべてSTMモナド内になければなりません。型チェッカーは、呼び出しの中間部分への変更をカプセル化しようとする試みを、unsafeによる例外を除いて、厳格に防ぎます。中間にある純粋関数のいずれかが、下のすべてに対して要件を満たし、それをカプセル化する方法はありません(unsafeによる例外を除く)。だからこそ、それが単に連鎖する要件として伝播しているのではなく、実際にあらゆる介入するスタックフレームに直接課されていると言っても妥当なのです。もし中間にある何かが原則として要件を満たし、それより上の関数からそれをカプセル化できるなら、たとえ特定のコード変更に対して、特定のインスタンスでそれが実用的でなくても、それは色ではありません。