この記事では「FixLangで遊ぼう」の第3回として、FixLangの基本的な文法(let式, if式など)について説明したいと思います。
FixLangのモジュールと型については、第2回を参照してください。
本記事はFixLangの公式ドキュメントをもとに、初学者向けに翻訳して適宜書き改めたものです。詳しく知りたい方は公式ドキュメントをご覧ください。
let式
ローカル変数を定義するには、let 式を使用します。
let 変数 = 式1 in 式2
上記の構文では、まず式1を評価して変数を初期化し、その変数を使って式2を評価します。変数のスコープは式2の内部です。
式2の評価結果がlet式の値になります。
また、以下のように in
の代わりに ;
を書いても同じ意味になります。ふだんはこちらを使うことが多いです。
let 変数 = 式1; 式2
let式が長い場合は、適当な場所で改行してインデントし、カッコをつけることをお勧めします。インデントやカッコは必須ではありませんが、可読性を高めるために役立ちます。
let sixty_four = (
let n = 3 + 5;
n * n
);
sixty_four + sixty_four
なお、変数のスコープは式2の内部だけなので、以下のように再帰関数を定義することはできません。
(式1ではまだfact
が初期化されていないため、fact
を使用できません)
let fact = (
|n| if n <= 1 { 1 } else { n * fact(n - 1) } // 式1
);
fact(5) // 式2
ローカルで再帰関数を定義するには、組み込み関数の fix
を利用します。
let fact = (
fix $ |loop, n| if n <= 1 { 1 } else { n * loop(n - 1) }
);
fact(5)
let式の説明からは逸脱するため、fix関数について詳しく知りたい方はここをクリックしてください。
fix
は、((a -> b) -> a -> b) -> a -> b
という型を持つ不動点関数です。
fix
に (a -> b) -> a -> b
型の関数f
を適用した結果 fix(f)
は、a -> b
型の関数です。
fix(f)
は、関数f
の不動点になります。つまり、fix(f) == f(fix(f))
が成り立ちます。
上記のサンプルコードでは、fact
は fix $ |loop, n| (以下略)
を評価した値になります。
ここで、f = |loop, n| (以下略)
とおくと、fact == fix(f)
が成り立ちます。
従って、fact == fix(f) == f(fix(f)) == f(fact)
が成り立ちます。
従って、fact
は |loop, n| (以下略)
の loop
に fact
そのものを代入した関数になります。
このように、組み込み関数のfix
を使うことで、ローカルで再帰関数を定義可能です。
if式
if式の構文は次の通りです。
if 条件式 { 式1 } else { 式2 }
上記の構文では、まず条件式を評価します。
条件式がtrueの場合、式1を評価します。条件式がfalseの場合、式2を評価します。
式1または式2の評価結果がif式の値になります。
注意点として、条件式の型はBoolにする必要があります。また、式1と式2は同じ型にする必要があります。
また、以下のように else { 式2 }
の代わりに ; 式2
と書いても同じ意味になります。
if 条件 { 式1 }; 式2
上記の構文では、式2の周りに波カッコをつける必要がないため、ネストを減らすことができます。
主に、エラー等の条件で処理途中で抜けるときに利用します。
可読性を高めるため、;
の後に改行を入れるのをおすすめします。
以下に例を示します。
sum_up_to: I64 -> Result ErrMsg I64;
sum_up_to = |n| (
if n < 0 { err $ "`n` should be a non-negative number" };
if n > 10000 { err $ "`n` is too large" };
ok $ Iterator::range(0, n).sum
);
関数適用
関数f
に値x
を適用するには、f(x)
と書きます。
前にも書きましたが、FixLang には「2変数関数」や「3変数関数」を表す専用の型はありません。
その代わり、a -> b -> c
という型を、「a
の値とb
の値を引数に取り、c
の値を返す2変数関数」のようなものと見なすことができます。
(->
演算子は右結合のため、上記の型は a -> (b -> c)
として解釈されます)
例えば、multiply : I64 -> I64 -> I64
という「2変数関数」を考えてみます。これに3
を部分適用した multiply(3) : I64 -> I64
は、整数を3倍する「1変数関数」になります。従って multiply(3)(5)
の結果は 15
になります。なお、f(x, y)
はf(x)(y)
と等価な糖衣構文であるため、最後の式は multiply(3, 5)
と書くことができます。
また、特別な構文として、f()
と書いた場合、f(())
として解釈されます。つまり、関数f
にユニット値()
を適用したものになります。
関数定義
「1変数関数」の値を作成するには、以下のように書きます。これは、他の言語ではラムダ式やクロージャと呼ばれるものです。仮引数のスコープは関数本体の式の内部です。
|仮引数| 関数本体の式
「2変数関数」の値を作成するには、以下のように書きます。
|仮引数1, 仮引数2| 関数本体の式
または
|仮引数1| |仮引数2| 関数本体の式
グローバル値の型と値を定義することで、グローバルな「関数」を定義できます。
関数本体の式が長い場合、可読性を高めるため、関数本体をカッコで囲み、適当な場所で改行するのをお勧めします。
fizzbuzz: I64 -> String;
fizzbuzz = |n| (
if n % 15 == 0 { "FizzBuzz" };
if n % 3 == 0 { "Fizz" };
if n % 5 == 0 { "Buzz" };
n.to_string
);
FixLang の関数は、関数定義の外で定義された値を「キャプチャ」できます。
test_message: String -> String;
test_message = |message| (
let show_mes = |n| (
n.to_string + ":" + message + "\n";
);
show_mes(1) + show_mes(2) + show_mes(3)
);
上記の例では、test_message
関数の内部でshow_mes
関数がローカル定義されています。show_mes
関数は関数外部で定義されたmessage
の値をキャプチャしています。
FixLangではすべてのオブジェクトは不変であるため、キャプチャした値も不変です。このため、関数の動作が変化することはありません。
例えば、以下のようにローカル関数の定義後に変数を再定義しても、キャプチャした値は変化しません。
test_message: String -> String;
test_message = |message| (
let show_mes = |n| (
n.to_string + ":" + message + "\n";
);
let message = message + "xxx"; // ここでmessageを再定義してもキャプチャした値は変化しない
show_mes(1) + show_mes(2) + show_mes(3)
);
.
演算子
関数に値を適用する別の方法として.
演算子があります。
.
演算子は x.f == f(x)
として定義されます。
.
演算子の優先度
.
演算子の優先度は、カッコによる関数適用の優先度よりも低いです。
従って、obj.method(arg)
は method(arg, obj)
として解釈されます。
// method関数が次のように型定義されていたとします。
method: Param -> Obj-> Result;
...
// このとき、以下の(1)〜(4)はすべて同じ結果になります。
// (通常は(1)または(4)の書き方が適切です)
let x = obj.method(arg); // (1)
let x = obj.(method(arg)); // (2)
let x = method(arg)(obj); // (3)
let x = method(arg, obj); // (4)
.
演算子の例
let arr = [1, 2, 3];
println(arr.get_size) // "3" と出力される
配列のサイズを取得するには、arr.get_size
と書きます。
get_size
の型は Array a -> I64
です。この関数に配列を適用すると、配列のサイズが返ってきます。
なお、arr.get_size()
と書くと、これは get_size((), arr)
として解釈されるため、エラーになります。間違いやすいためご注意ください。
let arr = ["Hello", "World"];
let arr = arr.set!(1, "FixLang");
println(arr.@(0) + arr.@(1)) // "HelloFixlang" と出力される
配列の要素を変更するには、arr.set!(idx, elem)
のように書きます。
set!
の型は I64 -> a -> Array a -> Array a
です。この関数にインデックスと要素と配列を適用すると、更新後の配列が返ってきます。
配列の要素を取得するには、arr.@(idx)
のように書きます。
@
の型は I64 -> Array a -> a
です。この関数にインデックスと配列を適用すると、要素が返ってきます。
$
演算子
関数に値を適用する別の方法として$
演算子があります。
$
演算子は f $ x == f(x)
として定義されます。
$
演算子は右結合です。つまり、f $ g $ x == f(g(x))
となります。
$
演算子の優先度
$
演算子の優先度は他のどの演算子よりも低いです。
このため、$
演算子はカッコを減らすために利用できます。
println $ (1, 2).to_string
// println((1, 2).to_string) と解釈される
println $ if condition { "Hello" } else { "World" }
// println(if condition { "Hello" } else { "World" }) と解釈される
パターン
let式、関数式はいずれもローカル名を導入します。
ローカル名の型がタプルまたは構造体の場合、渡された値を分解するためにパターンを利用できます。
以下にlet式でタプルを分解するパターンの例を示します。
let tuple = ("Hello", 123); // タプルを定義する
let (a, b) = tuple; // a は "Hello", b は 123 になる
以下に関数式でタプルを分解するパターンの例を示します。
myfunc: (String, I64) -> String;
myfunc = |(str, num)| str + num.to_string;
main: IO();
main = println $ myfunc $ ("Hello", 123); // "Hello123" と出力される
上記のmyfunc
は、(String, I64)
型の引数を受け取る「1引数関数」です。
一方、下記のmyfunc2
は、String
型の引数とI64
型の引数を受け取る「2引数関数」です。どちらの形もよく使われますが、混同しやすいのでご注意ください。
myfunc2: String -> I64 -> String;
myfunc2 = |str, num| str + num.to_string;
main: IO();
main = println $ myfunc2("Hello", 123); // "Hello123" と出力される
終わりに
本記事では、FixLangの基本的な文法について説明しました。
次回の記事では、ループ処理、共用体、構造体について説明する予定です。