LoginSignup
1
0

FixLangで遊ぼう (1): FixLangの紹介

Last updated at Posted at 2024-04-11

FixLangについて

最近、FixLang という関数型プログラミング言語に夢中になっています。FixLang は、Haskell と Rust の良い部分を組み合わせたような言語で、独特の特徴を持っています。

Haskell は関数型言語として有名ですが、学習曲線が急で、初心者には難しいと感じるかもしれません。一方、Rust はシステムプログラミング言語として人気ですが、複雑な文法や所有権の概念など、習得するのに時間がかかる可能性があります。

FixLang は、これらの言語の良い部分を取り入れつつ、より覚えやすく、使いやすい言語を目指して設計されています。

詳しくはFixLangの作者様の紹介記事をご覧ください。

FixLang の特徴と魅力

高速な実行速度

FixLang は LLVM でネイティブバイナリにコンパイルされるため、その実行速度は C 言語に匹敵します。さらに、オブジェクトのメモリ管理には参照カウント方式のガベージコレクションが採用されており、他の言語のガベージコレクションに比べて高速に動作します。

覚えやすい文法

FixLang の文法は独特ですが、Rust などの言語に比べると覚えやすいと言えます。一度慣れてしまえば、効率的にプログラミングを進めることができます。

充実した関数型言語の機能

FixLang では、モナド、型クラス(トレイト)、高階カインドなど、関数型プログラミングに必要な機能が一通り揃っています。Haskell は難しそうだと感じている方にとって、FixLang はおすすめの言語です。

不変性 (イミュータビリティ)

FixLang では、原則としてオブジェクトは一度作成すると変化しません。つまり、オブジェクトは不変 (イミュータブル) です。これにより、オブジェクトの状態が予期せず変更されてしまうことを防ぎます。

オブジェクトの部分的な変更

FixLang では、構造体のフィールドを変更したい場合、「set_xxx」という関数を使用します(「xxx」はフィールド名です)。ただし、オブジェクトは不変であるため、元の構造体は変更されず、変更された新しい構造体が作成されます。元の構造体への参照が残っている場合は、コピーを作成して変更を適用し、参照が残っていない場合は単に元の構造体を変更します。

type Foo = struct { a: I64, b: String };
let foo = Foo { a: 3, b: "hello" };
let foo2 = foo.set_a(4);
// foo2  foo をコピーしてフィールド `a` のみ変更した新しい構造体。
// foo が他から参照されていない場合は、コピーを省略して単にフィールドを変更する。

FixLang の文法

FixLang の文法には、他の言語とは異なる独特の特徴があります。ここでは、プログラミングをする際にハマりやすいポイントをいくつか紹介します。

ローカル変数の定義とスコープ

FixLang では、ローカル変数を定義する際に「let」式を使用します。

let x = 3; // x という変数を定義する
let y = x + 1; // y という新しい変数を定義する
println(y.to_string); // "4" と出力される

FixLang では、同じ名前で新しい変数を定義することもできます。この場合、以前の変数は隠されます。

let x = 3; // x という変数を定義する
let x = x + 1; // x という新しい変数を定義する
println(x.to_string); // "4" と出力される

「let 変数 = 式1; 式2」は、「let 変数 = 式1 in 式2」と等価です。

if 式と条件分岐

FixLang には if 文は存在せず、代わりに if 式が使用されます。

if 条件 { 1 } else { 2 }

式1 と式2 は同じ型にする必要があります。また、以下のように書くこともできます。

if 条件 { 1 }; 2

この書き方では、式2 の周りに {} が不要なため、ネストを減らすことができます。エラーなどで処理の途中で抜ける際に便利です。

例えば、FizzBuzz 問題は以下のように書くことができます。

module Main;

main: IO ();
main = (
    println(
        Iterator::range(0, 50).map(|i|
            let i = i + 1;
            if i % 15 == 0 { "FizzBuzz" };
            if i % 3 == 0 { "Fizz" };
            if i % 5 == 0 { "Buzz" };
            i.to_string
        ).join(", ")
    )
);

ループ処理

FixLang には while 文はありません。代わりに、「loop()」関数を使用します。

「loop()」関数の書式は次のとおりです。

loop(初期値, |パターン| ループ本体)

初期値とパターンには、タプルを使用することが多いです。
ループ本体は、「LoopResult s r」型の値を返す必要があります。
通常は、「break $ 返り値」または「continue $ 次の変数の値」を返します。

「loop()」関数を使ったFizzBuzzは以下のようになります。(モジュール宣言(module Main;)は省略しています)

main: IO ();
main = (
    let str = loop(
        ("", 1), |(str, i)|
        if i > 50 {
            break $ str
        };
        let str = if str == "" { str } else { str + ", " };
        let str = str + (
            if i % 15 == 0 { "FizzBuzz" };
            if i % 3 == 0 { "Fizz" };
            if i % 5 == 0 { "Buzz" };
            i.to_string
        );
        continue $ (str, i + 1)
    );
    println(str)
);

「loop()」関数を使用せずに、再帰関数で実装することもできます。

fizzbuzz: String -> I64 -> String;
fizzbuzz = |str, i| (
    if i > 50 {
        str
    };
    let str = if str == "" { str } else { str + ", " };
    let str = str + (
        if i % 15 == 0 { "FizzBuzz" };
        if i % 3 == 0 { "Fizz" };
        if i % 5 == 0 { "Buzz" };
        i.to_string
    );
    fizzbuzz(str, i + 1)
);

main: IO ();
main = (
    println(fizzbuzz("", 1))
);

モナドと値の取り出し

画面入出力などの副作用のある操作には、「IO」モナドを使用する必要があります。IO モナドの中から外には値を取り出すことができませんが、IO モナドの中では「*」演算子を使って値を取り出すことができます。

main: IO ();
main = (
    eval *println("Hello");
    eval *println("World");
    eval *println("Foo bar baz");
    pure()
);

printlnString -> IO () 型の関数なので、*println("Hello") の型は () になります。
ちなみに eval 式1; 式2 は、式1と式2を順番に評価し、式2の値を返す式です。式1の型は () である必要があります。
また、pure はモナドの値を返す関数です。モナドの値として()を返しているため、mainの型はIO ()になります。

同様に、標準入力から 1 行読み出すには、input_lineを使用します。

main: IO ();
main = (
    eval *println("What's your name?");
    let name = *input_line;
    eval *println("Hello " + name.strip_last_newlines + " san!");
    pure()
);

input_lineIO String 型のモナドです。読み出した行には改行文字が含まれるため、name.strip_last_newlinesで改行文字を除去しています。

ファイル入出力と IOFail モナド

ファイル入出力ではエラーが発生する可能性があるため、IOFail モナドを使用します。
IOFailモナドを do { ... }.try(eprintln) で囲むと、発生したエラーを捕捉して stderr にエラーメッセージを出力できます。
また、IOモナドをIOFailモナドの中で使用するには、liftする必要があります。

main: IO ();
main = (
    do {
        let file_path = Path::parse("hello.txt").as_some;
        let handle = *open_file(file_path, "w");
        eval *write_string(handle, "Hello\n");
        eval *write_string(handle, "World\n");
        eval *write_string(handle, "Foo Bar Baz\n");
        eval *close_file(handle).lift;   // close_file  IOモナドのため lift する
        pure()
    }.try(eprintln)
);

FixLangのミニライブラリについて

FixLang の標準ライブラリには、基本的な機能が揃っています。しかし、アプリケーション開発に利用する機能を追加したいと思い、ミニライブラリを作成しました。こちら で公開していますので、ぜひご覧ください。今後も機能を追加していく予定です。

終わりに

本記事では、FixLang の文法やハマりやすいポイント、ライブラリについて紹介しました。FixLang は、Haskell と Rust の良い部分を取り入れた魅力的な言語です。これをきっかけに、FixLang を使ってみたいと思う方が増えれば幸いです。今後も、FixLang の良いところやハマりポイントを紹介していく予定です。

1
0
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
0