この記事では「FixLangで遊ぼう」の第4回として、loop関数、共用体、構造体について説明したいと思います。
本記事はFixLangの公式ドキュメントをもとに、初学者向けに翻訳して適宜書き改めたものです。詳しく知りたい方は公式ドキュメントをご覧ください。
過去の記事は、こちらをご覧ください。
loop関数
FixLang には、多くの言語で見られるような for 文や while 文は存在しません。代わりに、組み込みの loop 関数を利用してループ処理を実行します。(なお、ループ処理は再帰関数や fix 関数を使っても実現可能です)
loop 関数は、以下のような構文で記述します。
loop(
初期状態, |状態変数|
ループ処理本体
)
loop 関数は、s -> (s -> LoopState s r) -> r という型を持ちます。ここで、s はループ状態を表す型パラメーターで、「初期状態」や「状態変数」の型を示します。r はループ処理の結果を表す型パラメーターで、loop 関数の返り値となります。
「ループ処理本体」では、LoopState s r 型の値を返す必要があります。この値はループを継続するか中断するかを表すものです。LoopState 型の値を構築するには、以下の関数のいずれかを使います。
- ループを継続する場合:
continue : s -> LoopState s r - ループを中断する場合:
break : r -> LoopState s r
loop 関数は以下のように実行されます。
- 「初期状態」を「状態変数」に格納し、「ループ処理本体」を実行します。
- 「ループ処理本体」が
continue(次の状態)を返した場合、「次の状態」を「状態変数」に格納し、「ループ処理本体」を再度実行します。 - 「ループ処理本体」が
break(処理結果)を返した場合、loop関数を中断し、「処理結果」を返します。
loop関数の例
calc_fib : I64 -> Array I64;
calc_fib = |n| (
let arr = Array::fill(n, 0);
let arr = arr.set(0, 1);
let arr = arr.set(1, 1);
let arr = loop((2, arr), |(idx, arr)|
if idx == arr.get_size {
break $ arr
} else {
let x = arr.@(idx-1);
let y = arr.@(idx-2);
let arr = arr.set(idx, x+y);
continue $ (idx+1, arr)
}
);
arr
);
上記の例では、loop 関数を利用してフィボナッチ数列を計算しています。
ループの初期状態は (2, arr) です。ループ処理本体は状態変数として (idx, arr)というタプルを取ります。idxは配列の添字、arrはフィボナッチ数列を格納する配列です。このように、タプルを状態変数にすれば、複数の情報をループで扱うことができます。
idxが配列サイズ未満の場合、フィボナッチ数列の値を計算してarrに格納し、continue $ (idx+1, arr)を返してループ処理を続行します。
idxが配列サイズに達した場合、break $ arr を返してループ処理を抜けます。loop関数の返り値はフィボナッチ数列を格納した配列になります。
共用体(union)
共用体を使うと、さまざまなバリアント(種類)のどれかであるようなデータを表現できます。
例えば、果物の種類(りんご、みかん、いちご)や処理結果(正常終了、エラー終了)などは共用体で表現できます。
共用体は、C言語のenumに似ていますが、バリアント(種類)ごとに異なる型の付属情報を持つことができます。
共用体を定義するには、下記の構文を使用します。
type 共用体名 型パラメータ... = union {
バリアント名_1: バリアント型_1, // 種類その1
バリアント名_2: バリアント型_2, // 種類その2
...
バリアント名_n: バリアント型_n // 種類そのn
};
共用体の定義では次のことを注意してください。
- 共用体名の最初の文字は大文字にする必要がある
- 型パラメータは0個以上で空白で区切って記述する
- 型パラメータの最初の文字は小文字にする必要がある
- バリアント定義とバリアント定義の間には
,が必要 - 最後のバリアント定義の後にある
,は無視される - 最後にセミコロンが必要
共用体の例 (LoopState)
例えば、loop関数の説明に現れた LoopState s r は2つの型パラメータ s と r を備えた共用体として定義されています。
type LoopState s r = union {
continue : s, // ループ継続 (`s`型の値を持つ)
break : r // ループ中断 (`r`型の値を持つ)
};
LoopState s r 型の値は、continueまたは breakのどちらかです。continueは付属情報としてs 型の値を持ち、breakは付属情報としてr 型の値を持ちます。型を集合と見なすと、LoopState s r型はs型とr型の直和になります。
共用型を定義すると、いくつかの基本的なメソッド関数が自動的に定義されます。
-
{バリアント名}: {バリアント型} -> {共用体型}: バリアントの値から共用体を構築する。 -
is_{バリアント名}: {共用体型} -> Bool: 共用体が指定されたバリアントかどうか判定する。 -
as_{バリアント名}: {共用体型} -> {バリアント型}: 共用体が指定されたバリアントの場合、バリアントの値を取得する。それ以外の場合はパニックする(すなわち、エラーメッセージを印字してプログラムの実行を停止する)。
例えば、先ほどの例のように LoopState s r を定義した場合、以下の関数がLoopState名前空間に自動的に定義されます。
-
continue : s -> LoopState s r:s型の値からLoopState値を構築する。 -
break : r -> LoopState s r:r型の値からLoopState値を構築する。 -
is_continue : LoopState s r -> Bool:LoopState値がcontinueで構築されたものかどうかを判定する。 -
is_break : LoopState s r -> Bool:LoopState値がbreakで構築されたものかどうかを判定する。 -
as_continue : LoopState s r -> s:LoopState値がcontinueで構築された場合、s型の値を抽出する。continueでない場合はパニックする。 -
as_break : LoopState s r -> r:LoopState値がbreakで構築された場合、r型の値を抽出する。breakでない場合はパニックする。
共用体の例その2 (Option)
共用体の別の例として、Option型があります。Option型は、存在しない可能性がある値を表すために使用されるものです。
(他の言語では、オブジェクトが存在しないときに null を使うことがありますが、FixLangではOption型を使います)
Option型は次のように定義できます。
type Option a = union {
none : (), // 値が存在しない
some : a // 値が存在する (`a`型の値を持つ)
};
上記の定義によって、以下の関数が Option名前空間に自動的に定義されます。
-
none : () -> Option a:()型の値からOption値を構築する。 -
some : a -> Option a:a型の値からOption値を構築する。 -
is_none : Option a -> Bool:Option値がnoneで構築されたものかどうかを判定する。 -
is_some : Option a -> Bool:Option値がsomeで構築されたものかどうかを判定する。 -
as_none : Option a -> ():Option値がnoneで構築された場合、()型の値を抽出する。noneでない場合はパニックする。 -
as_some : Option a -> a:Option値がsomeで構築された場合、a型の値を抽出する。someでない場合はパニックする。
なお、noneを使ってOption値を構築するには、none()と記述する必要があります。
(f() == f(())という糖衣構文を思い出してください)
構造体
FixLangで複数の情報をひとまとめにするには、構造体を使用します。
構造体を定義する
構造体を定義するには、下記の構文を使用します。
type 構造体名 型パラメータ... = struct {
フィールド名_1: 型_1,
フィールド名_2: 型_2,
...,
フィールド名_n: 型_n
};
例:
type User = struct {
name: String, // 名前 (String型)
age: I64 // 年齢 (I64型)
};
構造体の定義では次のことを注意してください。
- 構造体名の最初の文字は大文字にする必要がある
- 型パラメータは0個以上で空白で区切って記述する
- 型パラメータの最初の文字は小文字にする必要がある
- フィールド定義とフィールド定義の間には
,が必要 - 最後のフィールド定義の後にある
,は無視される - 最後にセミコロンが必要
構造体の値を構築する
構造体の値を構築するには、下記の構文を使用します。
構造体名 {
フィールド名_1: 値_1,
フィールド名_2: 値_2,
...,
フィールド名_n: 値_n
}
例:
let user = User {
name: "taro", // 名前
age: 20 // 年齢
};
構造体のフィールドの値を取得・設定・更新する
構造体のフィールドの値を取得するには、@フィールド名 という関数を使用します。@フィールド名の型は、構造体型 -> フィールド型 です。
eval *println(user.@name); // "taro" と出力される
eval *println(user.@age.to_string); // "20" と出力される
構造体のフィールドの値を設定するには、set_フィールド名 という関数を使用します。set_フィールド名の型は、フィールド型 -> 構造体型 -> 構造体型 です。
let user = user.set_name("jiro"); // name を "jiro" に変更した新しい構造体が返る
let user = user.set_age(23); // age を 23 に変更した新しい構造体が返る
構造体のフィールドの値を更新するには、mod_フィールド名 という関数を使用します。mod_フィールド名の型は、(フィールド型 -> フィールド型) -> 構造体型 -> 構造体型 です。
let user = User { name: "taro", age: 20 };
let user = user.mod_name(|name| "SUPER " + name); // name を "SUPER taro" に変更した新しい構造体が返る
let user = user.mod_age(add(1)); // age を 21 に変更した新しい構造体が返る
終わりに
今回の記事では、ループ関数、共用体、構造体について説明しました。
次回の記事では、イテレータと参照カウンタについて説明する予定ですが、もしかすると別の内容にするかもしれません。