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)
静かなる記号たち
関数と式を基本としている言語において、コード中に大量の括弧が現れるのは自然と予想されることです。かの LISP が「括弧だらけの言語」として揶揄されるのは偶然ではないのです。
しかしながら Frege では、記号はまったく目立たず、括弧をほとんど使わないコーディングスタイルに対応しています。
それでは、関数適用から始めましょう。
関数適用
Frege では関数適用を表すのに空白文字を、すなわち文字セットの中でもっとも目立たない文字を使用します。関数適用は、すべての演算子の中でもっとも高い優先度を持つ、つまり他のどんな演算子よりも強く結合する性質があります。また関数適用は左結合的でもあります。f x y
は実際にはふたつの関数適用からできていて、まず f x
が関数を返し、y
がその関数の引数となります。
ドル記号
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 の間の括弧は必須です。しかしすでに学んだ通り、ドル記号の優先度は最低です。ということは今回もドル記号を使えばうまく動くはず、ですよね? これはその通りで、同じように動作します。
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 を削って f が inc の別名であるかのように書くこともできます。以下はこれが妥当な変形であることの証明です。
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 つ得られたことになります。
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
と書くこともできましたが、コードがどう動作するかではなくコードが何を主張するかについて、両者の間には微妙な差があります。
-
f
は関数reverse
である -
f
はリストをreverse
させる
最初はポイントフリースタイルを見て混乱し、次に面白くなってきて、さらには乱用するようになり、それから適量に落ち着く、というのが初心者が辿るプロトタイピングと修正のサイクルです。
参考文献
Frege Language Reference | |
Declaration of the $ function |
https://github.com/Frege/frege/blob/master/frege/prelude/PreludeBase.fr#L109-109 (where line numbers might change) |