静かなる記号たち

関数と式を基本としている言語において、コード中に大量の括弧が現れるのは自然と予想されることです。かの LISP が「括弧だらけの言語」として揶揄されるのは偶然ではないのです。

しかしながら Frege では、記号はまったく目立たず、括弧をほとんど使わないコーディングスタイルに対応しています。

それでは、関数適用から始めましょう。

関数適用

f(x)    -- as known from many languages
f x     -- Frege style (like linear functions in mathematics)
f $ x   -- explicit function application with the $ operator
f(x,y)  -- taking a tuple as an argument
f x y   -- same as ((f x) y)

Frege では関数適用を表すのに空白文字を、すなわち文字セットの中でもっとも目立たない文字を使用します。関数適用は、すべての演算子の中でもっとも高い優先度を持つ、つまり他のどんな演算子よりも強く結合する性質があります。また関数適用は左結合的でもあります。f x y は実際にはふたつの関数適用からできていて、まず f x が関数を返し、y がその関数の引数となります。

最後に挙げた例は通常 カリー化部分適用 などと呼ばれます。しかしその内容は特に 部分的 であるというわけではなく、単に fx を引数に取る関数 (これはさらに y を引数として取る) であるというだけです。時は 19 世紀、関数を返す関数はゴッドロープ・フレーゲによって初めて導入されました。

ドル記号

g x のような関数適用が他の関数 f の引数として現れる場合、括弧付きで f (g x) と書かねばなりません。そうしないと、g のみが f の引数とみなされてしまい、ほしい結果が得られないためです。このままではネストが深くなった時扱いづらいため、ドル記号による代替記法が用意されています。

foo (bar (baz (buz x)))  -- deeply nested style
foo $ bar $ baz $ buz x  -- flat $ style

面白いことに、$ 演算子は言語組み込みの機能ではありません。中置演算子として定義された通常の関数です。その定義は f $ x = f x のようになっています。巧妙なのは $ の優先度が非常に低く設定されているところで、これによって $ より右側 (後で見るように左側も同様) にあるすべての演算子がまず適用され、その結果が f に渡るようになっています。

実用上、$ は開き括弧に相当するものと見て、対応する閉じ括弧が式の一番右側にあると考えることもできます。

関数合成

いくらお金を見るのが好きでも、コード中に現れる $ 記号はそこまで目立たないとはいえません。しかしドット演算子は違います。これは 関数合成 のための演算子で、数学の授業と同じく f (g x)(f • g) x と書くことができます。

(foo . bar . baz . buz) x  -- function composition with dot
(foo • bar • baz • buz) x  -- Frege also allows unicode 2022

関数合成を目にした際は、右から左へ と読むのが最良の方法です。まず buz x が評価され、次に bazbar ときて最後に foo が評価されます。これは括弧を入れ子にして書いた場合とまったく同じ順序です。

実のところ関数合成は、Unix 系のシステムに見られるパイプ処理と (ただし順番が逆ですが) 非常によく似ています。

すでに述べた通り、関数適用は最強の優先度を持ち、関数合成よりも優先度が上です。したがって上に挙げた例では、正しく関数合成を行うために buzx の間の括弧は必須です。しかしすでに学んだ通り、ドル記号の優先度は最低です。ということは今回もドル記号を使えばうまく動くはず、ですよね? これはその通りで、同じように動作します。

foo . bar . baz . buz $ x  -- this is not used so often

それではさらに x も削って、ドル記号も括弧も使わずに動作させることはできないでしょうか?

実はできます。しかしそのためには、少し背景について説明する必要があります。

ラムダ式としての関数

引数に 1 を加算するだけの関数 inc があったとしましょう。この関数を記述する方法として、以下の 2 種類は同じ意味になります。

inc x = x + 1           -- usual function definition
inc   = \x -> x + 1     -- equivalent lambda style

ポイントフリースタイル

さて、inc と同じように動作する関数 f がほしい時、f の定義中で inc を使用して引数を渡すことができます。

f x   = inc x           -- f should be like inc
f     = inc             -- point-free style, "cancel x"

しかしさらに、x を削って finc の別名であるかのように書くこともできます。以下はこれが妥当な変形であることの証明です。

f     = inc             -- beta reduction =>
f     = \x -> x + 1     -- replace formal lambda parameter with free variable =>
f x   = x + 1           -- same as definition of inc

これで、今回の関数を定義する方法が 4 つ得られたことになります。

最終的な 4 つの書き方
f x =  foo (bar (baz (buz x)))    -- deeply nested style
f x =  foo $ bar $ baz $ buz x    -- flat $ style
f x = (foo . bar . baz . buz) x   -- function composition
f   =  foo . bar . baz . buz      -- point-free style

この等価性を心に留めておくことで、自分のコードを書く時や、さらには他の開発者のコードを読む時にも、自信を持って今回説明した機能を扱えるようになるはずです。

それなりの大きさのコードであれば、おそらく 4 種類のスタイルすべてが登場するでしょう。たとえば以前の記事では f = reverse の形でポイントフリースタイルを使用しましたが、この形式では説明なしでもコードの意味がわかると思います。f x = reverse x と書くこともできましたが、コードがどう動作するかではなくコードが何を主張するかについて、両者の間には微妙な差があります。

  1. f は関数 reverse である

  2. f はリストを reverse させる

最初はポイントフリースタイルを見て混乱し、次に面白くなってきて、さらには乱用するようになり、それから適量に落ち着く、というのが初心者が辿るプロトタイピングと修正のサイクルです。

参考文献

Frege Language Reference

http://www.frege-lang.org/doc/Language.pdf

Declaration of the $ function

https://github.com/Frege/frege/blob/master/frege/prelude/PreludeBase.fr#L109-109 (where line numbers might change)

results matching ""

    No results matching ""