Amber

A Language for High-Level Programming with Self-Extension

チュートリアル5: 関数・クロージャ

関数

関数fを引数a,b,…に適用して呼び出すには

f(a, b, ...)

という構文を利用します。例えば以下のようになります。

amber:1> head(1)
=> Int
amber:2> append([1,2], [3,4,5])
=> [1, 2, 3, 4, 5]
amber:3> sin(1.0)
=> 0.8414709848078965

また

  • f(a)の代わりにa.f
  • f(a,b,c)の代わりにa.f(b,c)

という構文を利用する事も出来ます。

amber:1> 1.head
=> Int
amber:2> [1,2].append([3,4,5])
=> [1, 2, 3, 4, 5]
amber:3> 1.0.sin
=> 0.8414709848078965

この.を用いた構文は関数適用の構文糖衣ではありませんので注意して下さい。関数適用も含んだより一般的な処理を表します。この事については後の回に説明します。

関数は第一級オブジェクト

Amberの関数は第一級オブジェクトなので、他のオブジェクトと同様に変数に代入したりコンテナに格納したり関数の引数として渡す事が出来ます。以下の様に関数オブジェクトのヘッドはFunctionで、そのアーギュメントを直に読み書きする事はできません。

amber:1> head
=> <#Function:0xf7610a04>
amber:2> fullform(head)
=> "Function{...}"

関数を第一級オブジェクトとして扱う例として関数mapを紹介します。これはリストの各要素に渡された関数を適用した結果のリストを返します。

amber:1> map(head, [1, "foo", \foo, [1, 2, 3]])
=> [Int, String, Symbol, List]

関数の合成

関数に対する演算で重要なものの一つは関数の合成です。関数f,gをこの順番に合成するには

g * f

と記述します。g * fは引数にfを適用した結果にgを適用するという関数となります。

例えば以下の様にしてsin(cos(x))を計算する関数を作成する事が出来ます。

amber:1> sincos: sin*cos
=> <#Function:0xf6483128>
amber:2> sincos(1)
=> 0.5143952585235492

無名関数

関数オブジェクトを生成する為には以下の構文を用います。

(引数1, 引数2, ...) -> 式

例えば以下のように記述します。

amber:1> (x) -> x + 1
=> <#Function:0xf64895e8>
amber:2> %(3)
=> 4
amber:3> (x, y) -> x + y
=> <#Function:0xf64900d4>
amber:4> %(2, 3)
=> 5

また、頻繁に利用する一変数関数の場合に限っては()を省略して

amber:1> x -> x + 1
=> <#Function:0xf64881e8>

と記述する事が可能です。

関数の定義

Amberにおける関数(headなど)は通常の変数と同じ扱いです。(関数と変数の名前空間が同一です。) 従って、変数に無名関数を束縛すれば関数f(x)の定義となります。

amber:1> f: x -> x^2
=> <#Function:0xf64880a4>
amber:2> f(3)
=> 9

また、関数定義専用の構文として記号:を用いた以下の構文もあります。変数を定義する際にも:を用いたのを思い出して下さい。 今後は主にこの構文を用います。

amber:1> f(x): x^2
=> <#Function:0xf6484ca4>
amber:2> f(3)
=> 9

定義する関数の中身が複数行に渡る場合には、以前説明したブロックを用いる事が出来ます。 ブロックの評価結果は最後の文の評価結果となります。例えば、以下の様に記述します。

amber:1> f(x): {
amber:1~        a: 1
amber:1~        b: 2
amber:1~        c: 3
amber:1~        a*x^2 + b*x + c
amber:1~ }
=> <#Function:0xf640e168>
amber:2> f(3)
=> 18

また、関数の中身は新たなスコープとなります。従って、今の例のように関数内部でのみ用いる変数(ローカル変数)を定義することができます。 また、関数は変数と同じ名前空間に属しますので変数を定義出来る箇所ならばどこでも関数を定義する事が出来ます。 例えば、以下のようにして関数内で関数を定義するといった事が可能です。

amber:1> f(x): {
amber:1~        g(x): x^2+x+1
amber:1~        g(x) * g(x+1)
amber:1~ }
=> <#Function:0xf64dd424>
amber:2> f(1)
=> 21

return文

関数の途中で値を返却したい場合にはreturn文を利用する事が出来ます。

amber:1> f(x): {
amber:1~        if (x <= 0) return 0
amber:1~        x-1
amber:1~ }
=> <#Function:0xf64d6dc4>
amber:2> f(2)
=> 1
amber:3> f(-1)
=> 0

クロージャ

Amberはレキシカルクロージャを作成する機能を備えています。 あるスコープで定義された関数がそのスコープの外に抜け出た場合にも、その関数はそれが定義されたスコープの変数を読み書き出来ます。この機能によって、例えば以下のような状態を持つ関数を実現する事が出来ます。

amber:1> make_counter(): {
amber:1~        n: 0
amber:1~        return () -> { n += 1 }
amber:1~ }
=> <#Function:0xf64c2b2c>
amber:2> counter1: make_counter()
=> <#Function:0xf64cd64c>
amber:3> counter2: make_counter()
=> <#Function:0xf64d7390>
amber:4> counter1()
=> 1
amber:5> counter1()
=> 2
amber:6> counter2()
=> 1
amber:7> counter1()
=> 3
amber:8> counter2()
=> 2
amber:9> counter2()
=> 3
amber:10> counter1()
=> 4

クロージャcounter1counter2はどちらも同じnを読み書きしているにも関わらず、それぞれが独立してnの実体を持っている事に注意して下さい。 このように関数のスコープは呼び出される度に新しく生成されます。

もう少し複雑なクロージャの使い方として以下の例を見て下さい。

amber:1> make_counter2(): {
amber:1~        n: 0
amber:1~        count_up: () -> { n += 1 }
amber:1~        count_down: () -> { n -= 1 }
amber:1~        return (count_up, count_down)
amber:1~ }
=> <#Function:0xf64589b8>
amber:2> (up, down): make_counter2()
=> (<#Function:0xf646b9d4>, <#Function:0xf646bb20>)
amber:3> up()
=> 1
amber:4> up()
=> 2
amber:5> up()
=> 3
amber:6> down()
=> 2
amber:7> down()
=> 1
amber:8> up()
=> 2
amber:9> down()
=> 1

関数make_counter2は同一のスコープ内で2つのクロージャcount_upcount_downを作成しタプルにして返すものです。amber:2>の行では、make_counter2の生成する2つのクロージャを(up, down): ...という構文で受け取っています(この構文については次回説明します)。その後の呼び出しをみれば判る様に、関数updownは同一の環境を共有しています。またupdownを呼び出す以外の手段でこれらが参照するnを読み書きする事が出来ないことにも注意して下さい。

以上の様にクロージャによって、カプセル化された固有の状態を持つオブジェクトを作成する事が出来ます。またクロージャは関数と全く同一のインターフェースを持つので関数に引数として渡したり、合成するなどといった事が同様に出来ます。これは非常に良い抽象化の手段となります。

クロージャと関数の合成の例としてmake_counterとオブジェクトの文字列化を行う関数to_sを合成してみましょう。すると整数を文字列として順番に列挙するカウンターが出来上がります。

amber:1> make_counter(): {
amber:1~        n: 0
amber:1~        return () -> { n += 1 }
amber:1~ }
=> <#Function:0xf64c172c>
amber:2> f: to_s * make_counter()
=> <#Function:0xf64ce7cc>
amber:3> f()
=> "1"
amber:4> f()
=> "2"
amber:5> f()
=> "3"
amber:6> f()
=> "4"

returnのセマンティクスに関する注意

以下の例を見て下さい。文return 0が実行後の制御はf内に移っている事が判ります。つまりクロージャからのreturnはそのクロージャのみを抜けるという挙動をします。後の回に述べますが、breakcontinue等の文も同様にクロージャの中に閉じた振る舞いをします。

amber:1> f(): {
amber:1~        g(): { return 0 }
amber:1~        g()
amber:1~        return 1
amber:1~ }
=> <#Function:0xf64d0104>
amber:2> f()
=> 1

しかし、関数をまたいだジャンプを行いたい場面も多いと思います。現状の実装では後に述べる例外機構のみがこれを可能としますが、より汎用的な継続による実装に置き換える事も検討しています。その場合はreturnなどの文の扱いも変わってしまう可能性がありますが、言語の仕様が安定するまでご容赦下さい。