Amber

A Language for High-Level Programming with Self-Extension

チュートリアル6: パターンマッチング・部分関数の融合

パターンマッチング

Amberでは関数の仮引数の代わりにパターンを記述する事により関数の定義域を限定する事が出来ます。つまり

関数名(パターン1, パターン2, ...): 関数本体

(パターン1, パターン2, ...) -> 関数本体

という構文で記述します。

例えばパターン0は整数0にのみマッチします。そこで以下のように整数0が渡された時のみ実行される関数を定義する事が出来ます。定義域外の引数が渡されるとundefが返ります。

amber:1> f(0): "zero"
=> <#Function:0xf6480eb8>
amber:2> f(0)
=> "zero"
amber:3> f(1)
=> undef

また、関数の引数の数も定義域の一種と見なされます。すなわち、以下の様に引数の数が異なる場合にはundefが返ります。

amber:1> f(x): 0
=> <#Function:0xf6480b0c>
amber:2> f(0, 1)
=> undef

パターンの種類

仮引数

これまで用いてきた仮引数(x等の通常のシンボル)は任意の値にマッチするパターンと見なす事が出来ます。

f(x, y): ...

ドントケア

記号_ドントケアというパターンを表します。仮引数と同様任意の値にマッチしますが、マッチした値は捨ててしまいます。

f(_): ...

リテラルパターン

  • 整数定数
  • 文字列定数
  • シンボル定数

リテラルパターンとなり、それ自身に一致する値のみを受け付けます。

f(0): ...
f("foo"): ...
f(\foo): ...

浮動小数点数によるリテラルパターンを記述する事はできません。浮動小数点数の値が一致するという事の考え方は場合によって異なるからです。浮動小数点数に対するマッチングは後述のガード節を用いて下さい。

ヘッドパターン

シンボル(仮引数) @ シンボル(ヘッド)

という構文でヘッドパターンを記述出来ます。これは引数のヘッドが一致する場合にのみマッチします。

amber:1> f(x @ Int): "integer"
=> <#Function:0xf6482334>
amber:2> f(0)
=> "integer"
amber:3> f(1)
=> "integer"
amber:4> f(1.0)
=> undef

複合パターン

引数部にオブジェクトをそのまま記述するとオブジェクトパターンとなります。リテラルパターンもオブジェクトパターンの一種とみなす事が出来ます。ヘッドに対するマッチングが行われ、その後各アーギュメントについて再帰的にマッチングが行われます。

以下の例の関数fの定義域は、ヘッドがFooであり、アーギュメントが3つあり、1つめのアーギュメントが定数0であるオブジェクト1つということになります。

amber:1> f(Foo{0, x, y}): [x, y]
=> <#Function:0xf6492bc4>
amber:2> f(\Foo{0, 1, 2})
=> [1, 2]
amber:3> f(\Hoge{0, 1, 2})
=> undef
amber:4> f(\Foo{1, 2, 3})
=> undef

また記号...を用いてアーギュメントの個数を指定しないパターンも記述出来ます。以下の例ではヘッドがFooで、アーギュメントが2個以上の値にマッチします。

amber:1> f(Foo{x, y, ...}): [x, y]
=> <#Function:0xf6492bb4>
amber:2> f(\Foo{0})
=> undef
amber:3> f(\Foo{0, 1})
=> [0, 1]
amber:4> f(\Foo{0, 1, 2})
=> [0, 1]

以下のように変数...という記述によって、...にマッチする部分をリストとして取り出すことが出来ます。

amber:1> f(Foo{x, y, z...}): [x, y, z]
=> <#Function:0xf64966cc>
amber:2> f(\Foo{0, 1, 2, 3, 4, 5})
=> [0, 1, [2, 3, 4, 5]]

この記法を利用した例として、リストの先頭を取り出すcar、先頭以外を取り出すcdrは以下の様に定義する事ができます。

amber:1> car([a, ...]): a
=> <#Function:0xf6487d1c>
amber:2> cdr([_, as...]): as
=> <#Function:0xf6496c40>
amber:3> car([1,2,3,4,5])
=> 1
amber:4> cdr([1,2,3,4,5])
=> [2, 3, 4, 5]

最後に

シンボル(仮引数) @ 複合パターン

と記述する事によって、パターンマッチを行うと同時にオブジェクト自身を仮引数に束縛させる事が出来ます。

amber:1> f(obj @ Foo{x, y}): [obj, x, y]
=> <#Function:0xf6493ea0>
amber:2> f(\Foo{1, 2})
=> [Foo{1, 2}, 1, 2]

ガード節

f(引数1, 引数2, ...) when ガード節 : 関数本体

(引数1, 引数2, ...) when ガード節 -> 関数本体

という構文によってガード節を記述出来ます。ガード節とは何らかの述語であって、パターンマッチを全て通過した後関数本体を実行する前にそれが実行されます。ガード節の評価結果がtrueの時のみ関数本体が実行されます。

例えば自然数のみを受け取る関数を以下の様に定義する事ができます。

amber:1> f(x) when x >= 0 : "natural number"
=> <#Function:0xf6488edc>
amber:2> f(0)
=> "natural number"
amber:3> f(1)
=> "natural number"
amber:4> f(2)
=> "natural number"
amber:5> f(-1)
=> undef

部分関数の融合

Amberの全ての関数は全オブジェクトの一部だけを受け取る部分関数です。 この部分関数を融合して一つの関数を作成する事が出来ます。 そのためには演算子|を用いて

f | g

の様に記述します。この関数に引数を渡すとf,gの順番にパターンマッチを行い、 最初にマッチした方が呼ばれます。

簡単な例として数値の絶対値を求める関数は

x when x >= 0 -> x

x when x < 0 -> -x

を融合すれば良いですから

amber:1> abs: x when x >= 0 -> x | x when x < 0 -> -x
=> <#Function:0xf6515590>
amber:2> abs(1)
=> 1
amber:3> abs(-1.0)
=> 1.0

の様にして作ることが出来ます。複数行では以下のように可読性のよい書き方が出来ます。 1

abs: x when x >= 0 -> x
   | x when x < 0  -> -x

また、以下のように複数の関数定義文により記述する事も出来ます。

abs(x) when x < 0  : -x
abs(x) when x >= 0 : x

ただしこの場合は後に書いた定義の方が優先されるので注意して下さい。例えばフィボナッチ数列の一般項を求める関数は演算子|を用いる場合には

fib: 0 -> 0
   | 1 -> 1
   | n -> fib(n-1) + fib(n-2)

と記述しますが、関数定義を並べて書く場合には

fib(n): fib(n-1) + fib(n-2)
fib(0): 0
fib(1): 1

と書かなければなりません。このような仕様になっている理由は定義文は一つ一つ独立していて、実行される毎にfibの定義が拡張されていくのだと考えるからです。 このようにAmberでは関数の融合を関数の拡張と呼ぶ事があります。

Amberでの考え方

Amberの部分関数は任意のタイミングで自由に融合させる事が出来ます。 例えば、

f(0): 1

という関数を作ったとします。0以外の引数に関してはundefが返ります。 これに標準ライブラリにある恒等関数idを融合させると、「0以外には何もしない」という関数が出来ます。 「0以外はエラーにしたい」という場面もあるかもしれません。そのような時は例外を送出する関数と融合すれば良いです。

amber:1> f(0): 1
=> <#Function:0xf6480eb8>
amber:2> map(f, [0,1,2,3,4])
=> [1, undef, undef, undef, undef]
amber:3> map(f | id, [0,1,2,3,4])
=> [1, 1, 2, 3, 4]
amber:4> map(f | n -> {throw "invalid number: " + n}, [0,1,2,3,4])
Error: "invalid number: 1"

仕様の定まったアプリケーションを作成する場合にはこのような機能は不要ですが、 ライブラリを作成する場合や、将来的な拡張性を考えた場合には部分関数は大変便利です。 後に説明するAmberの自己拡張機能はこの部分関数の融合のメカニズムによって実現しています。

私は、関数が定義域外の値をどう処理するかは本来その関数のあずかり知らぬ事だから未定義値を返すのが自然だと考えています。 例外を送出する、エラー番号を返却する等の処理を行う場合には関数の間に共通する決まり事が必要になり、関数の部品としての独立性が低下するからです。 定義域外の値についてはundefを返し、呼び出す側がそれを柔軟に処理できる仕組みを用意するというのがAmberでの考え方です。

部分関数とスコープ

あるスコープ内でスコープの外側にある関数を拡張しても外側の関数は影響を受けません。 演算子|を用いれば一時的な拡張、スコープを用いればスコープ内でのみの拡張、一番外側で融合すれば恒久的な関数の機能拡張が行えるという事になります。

f(0): 1
{
    f(0): 2
    puts(f(0))      # => 2
}
puts(f(0))          # => 1

パターンマッチングを用いた変数定義・代入

Amberではパターンマッチングを利用して変数定義・変数への代入を行う事が出来ます。 関数定義で利用出来る任意のパターンを使う事が出来ます。

amber:1> (1, (2, 3))
=> (1, (2, 3))
amber:2> (x, y): %
=> (1, (2, 3))
amber:3> x
=> 1
amber:4> y
=> (2, 3)
amber:5> (x, y) = (y, x)
=> ((2, 3), 1)
amber:6> x
=> (2, 3)
amber:7> y
=> 1

関数定義の場合との違いは、マッチングに失敗した場合にはエラーとなるという事です。

amber:1> (x, y): (1, 2, 3)
Error: MatchingFailed{[amber:1], pattern{(x, y)}}

  • [1] 現状の実装ではシェルでこの入力はできません。ファイルに記述する場合限定です。シェルでの複数行文の入力については次回説明します。