📑

fpdartの型たち②(IO/Task/Reader)

2024/04/21に公開

はじめに

この記事では、前回の続きにfpdart[1]の基本的な概念について紹介します。

前回の記事

https://zenn.dev/minma/articles/2d9f977809a246

主要な型

  • IO: 副作用を持つ計算を表現するための型
  • Task: 非同期計算を表現するための型
  • Reader: 依存性注入を実現するための型

副作用

本題に入る前に、まずは副作用という言葉を簡単に説明します。

副作用(Side Effect)は、プログラムの実行中に関数や式の評価によって、外部の状態が変化することを指します。Dartにおいても、副作用はプログラムの挙動を予測しにくくし、デバッグを難しくする要因となります。

例えば、以下のようなコードが副作用を持ちます:

  • スコープ外にある変数の値を操作する

    int a = 10;
    
    int multiplyByTwo(int n) {
      a += 10;
      return n * 2;
    }
    
  • ログやファイルを残す

    void main() {
      print('Hello, world!');
      File('example.txt').writeAsStringSync('Data');
    }
    

fpdartにおいては、下記2つの主な目的を持ちます。

  • 型シグネチャを使用して副作用を明示する (Eitherでエラーを明示するように)
  • 副作用の実行をより詳細に制御し、コードの保守とテストをしやすくする

IO(同期)

Dartの実行環境では、ファイルの読み書きやネットワークリクエストなどの副作用を持つ操作が一般的です。IOを使用することで、これらの副作用をカプセル化し、コードの安全性と保守性を高めることができます。

void main() {
  final io = IO(() => File('data.txt').readAsStringSync()); // 副作用をカプセル化する
  final result = io.run(); // ファイルの読み込みを実行する
  print(result);
}

それ以外に、IOで副作用を扱う例は以下の通りです。

  • ロギング
  • 現在の日付の取得
  • 乱数の取得

Task(非同期)

Dartの非同期プログラミングでは、Futureが一般的に使用されますが、Taskはより洗練された方法で非同期計算を扱うことができます。Taskは遅延評価され、複数の非同期処理を組み合わせて効率的に実行することができます。

void main() {
  // 関数が非同期の場合は、`IO`ではなく`Task`型を使用する
  final task = Task(() => http.get(Uri.parse('https://example.com')));
  task.run().then((response) => print(response.body));
}

IOEither/TaskEither

IOEitherTaskEitherは、EitherIOTaskを組み合わせたものです。
前回で紹介したように、Eitherは、成功と失敗のいずれかの結果を表現するための型であり、エラーハンドリングに役立ちます。副作用を持つ計算とエラーハンドリングを組み合わせることにより、より安全で効果的なプログラミングを実現します。

Reader

関数型プログラミングでは、関数の入力を明示的に指定することで、外部の状態に依存しない純粋な関数を作成することが重要です。Readerを使用して関数をラップし、依存性を明示的に管理することができます。

class User {
  const User(this.id, this.name);

  final int id;
  final String name;

}

// ユーザーIDを受け取り、そのユーザーの名前を取得する関数
String getUserName(Database db, int userId) {
  // データベースからユーザー情報を取得する処理などを想定
  final userMap = db.get({1: 'Alice', 2: 'Bob', 3: 'Charlie'});
  return userMap[userId] ?? 'Unknown';
}

// データベースを注入する`Reader`を作成
Reader<Database, String> getUserNameReader(int userId) => Reader(
  (db) => getUserName(db, userId);
);

void main() {
  // `Reader`の計算を実行し、ユーザーIDが1のユーザーの名前を取得
  final result = getUserNameReader(1)
  print('User name: $result'); // 出力: User name: Alice

  // 別のユーザーIDを指定して計算を実行
  final result2 = getUserNameReader(2);
  print('User name: $result2'); // 出力: User name: Bob
}

上記の例ではgetUserName()が外部のデータベースに依存していますが、Readerでラップすることでその依存性をパラメータで定義する必要がなくなます。
getUserName()は外部の状態に依存せず、純粋な関数として扱うことができます。

また、依存性を注入することで、異なる状況や環境で再利用することができ、関数の振る舞いを変更する際にも、関数の本体を変更することなく、依存性の注入を変更するだけで済みます。
そのため、テスト時には、モックやスタブなどを使用して依存性を制御し、テストケースごとに異なる振る舞いをするように設定することができます。関数の再利用性と保守性も向上します。

さいごに

普段で慣れた命令型からいきなり関数型の世界に飛び込むと、代数的データ型や、純粋関数、高階関数、カリー化などの概念を理解すればするほど、関数型プログラミングの世界は複雑であることがわかって、脳死になってしまうでしょう。

関数型プログラミングを勉強するには、過剰な執着を抱えず頭を空っぽにしてから始めるのは大事かと思います。

脚注
  1. Dartで関数型プログラミングの概念を実践するためのライブラリです。 ↩︎

GitHubで編集を提案

Discussion