LoginSignup
1
1

FixLangで遊ぼう (3) 基礎編: 基本的な文法

Posted at

この記事では「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)) が成り立ちます。

上記のサンプルコードでは、factfix $ |loop, n| (以下略) を評価した値になります。
ここで、f = |loop, n| (以下略) とおくと、fact == fix(f) が成り立ちます。
従って、fact == fix(f) == f(fix(f)) == f(fact) が成り立ちます。
従って、fact|loop, n| (以下略)loopfact そのものを代入した関数になります。
このように、組み込み関数の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の基本的な文法について説明しました。
次回の記事では、ループ処理、共用体、構造体について説明する予定です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1