Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@qnighy
Created February 22, 2022 13:18
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save qnighy/b94e5647cd7f4488e84bad17aba17c01 to your computer and use it in GitHub Desktop.
Save qnighy/b94e5647cd7f4488e84bad17aba17c01 to your computer and use it in GitHub Desktop.
並置による関数適用に関する所感

並置による関数適用

並置による関数適用の善し悪しについて盛り上っているので、自分の意見を表明しておく。以下の2本立て。

  • 純粋に構文論的な議論 (構文拡張の余地を残す)
  • 意味論との関係での議論 (副作用の表示)

先に結論だけ書くと、私はどちらかといえば括弧による関数適用のほうが好みです。

そもそも並置による関数適用とは

以下のように特定のデリミタによらず、2つの式を並べるだけで関数適用できることを本稿では並置による関数適用 (application by juxtaposing) と呼ぶことにします。

result = f x y

理論面ではλ計算の標準的な記法で、また実用例としてはHaskellやOCamlなど広義のML系でよく見られる形式です。

並置による関数適用以外に以下のような関数適用構文が存在します。

  • 括弧による関数適用 f(x, y)
  • S式 (f x y)
    • これも広い意味では「括弧による関数適用」と言えますが、本稿では ( が中央に来るもののみを「括弧による関数適用」と呼ぶことにします
  • 中置適用 f $ x

以下、S式と中置適用の話は忘れます (Lisperの皆さん、ごめんなさい)

純粋に構文論的な議論 (構文拡張の余地を残す)

並置による関数適用では、識別子の並び a b が構文的に正しくなります。括弧による関数適用の場合はこのようなことはなく、通常 a b のようなトークン列は式中に存在できません。

括弧による関数適用を採用している言語はしばしば、この特性を用いて既存の構文の拡張 (文脈依存キーワードの導入) を試みます。

たとえばRustでは union はキーワードではありませんが、 union Foo のような並びは式として登場し得ないため曖昧性なくパースすることができます。

fn main() {
  // OK (unionは識別子)
  struct union {}; union {};
  // OK (unionはキーワード)
  union Foo { foo: i32 }
}

JavaScriptでも同様に文脈依存キーワードが存在します。

// async + (function f() {}) になる余地はない
async function f() {}

もちろん互換性を保ったまま構文拡張する手段は他にも色々ありますが、個人的にはこのパターンで作られる構文拡張のほうが自分の美意識に合うことが多いので好みです。

意味論との関係での議論

「並置による関数適用」と関連した話題に「カリー化された関数定義」があります。複数の引数を取る関数を定義するときに、以下のどちらを選ぶかという議論です。

  • タプルを用いて1回の関数呼び出しで全ての引数を受け取る (言語組み込みの複数引数機能を使う場合も含む)
  • カリー化し、引数の個数と同じだけの関数呼び出しを発生させる。

次の表でわかるように、この2つは本来は独立した議論です。

並置による関数適用 括弧による関数適用
引数のタプル渡し f (x, y) f(x, y)
カリー化 f x y f(x)(y)

しかし、実際には並置による関数適用をする言語ではカリー化された関数定義をするのが普通で、括弧による関数適用をする言語では引数のタプル渡しをするのが普通です。

さて、カリー化の善し悪しについて、筆者は次のようなスタンスでいます。

  • 関数呼び出しが副作用をもちうるなら、引数のタプル渡しのほうが好ましい。
  • そうでなければ、カリー化のほうが扱いやすい。

多くの言語では関数呼び出しが副作用を持ちます (Haskellも厳密にはpartiality副作用 (⊥) があると言えますが、この議論においては無視しても実害は少ないのでカリー化してもいいんじゃないかと思います)

副作用があるのにカリー化している言語といえばOCamlなどがありますが、そのOCamlは部分適用の処理のために変なVM命令があったりします。 といってもパフォーマンスとかVM自体の話は別に本稿の関心ではないのですが、これは個人的には一種のコードスメルだと思っています。 つまり、設計が悪いのでは?

どういうことかというと、関数呼び出しは副作用を持つわけですが、手続き型言語の大抵の関数は副作用を2~3回に分けて別々に実行する必要はありません (逆に副作用を任意回実行したいとなればジェネレーターなどの選択肢が出てきますが)

たとえば、 client.request("get", "https://example.com") は副作用を1回だけ実行するのであって、第一引数 (client) が渡された直後や第二引数 ("get") が渡された直後に何かする必要はないわけです。

この事実は、必要な関数呼び出しの回数としてそのままインターフェースに表現したほうが正直だと筆者は考えます。それは、基本的に関数呼び出しの回数を減らす (必要なければカリー化しない) ということです。

そうしてカリー化しないほうが自然だと決まれば、あるときは f x と書きあるときは f (x, y) と書くのもなんだかばかばかしいのでいっそ複数引数をネイティブサポートして f(x, y) としたほうがいいんじゃないかなと思います。

半分くらいは美意識の問題な気もしますが……

まとめ

Rubyの構文は地獄 (唐突)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment