1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPで関数型プログラミングを始めよう (Option/Result 型)

Posted at

はじめに

PHPでの開発において、「値が存在しないかもしれない」という状態や、「処理が失敗するかもしれない」という状況は日常的に発生します。これらの状態を表現するために、従来はnull値や例外を利用することが一般的でした。しかし、これらのアプローチには以下のような問題があります:

  • nullの使用は「値がない」ことを表現できますが、「なぜない」のかの理由が表現できません
  • 例外はプログラムフローを中断するため、想定内の「失敗」に対しては過剰な対応となることがあります
  • これらの状態チェックを忘れると、実行時エラーやバグの原因となります

関数型プログラミングの世界では、これらの問題に対する解決策として「Option型」と「Result型」が広く使われています。PHPでこれらの概念を簡単に利用できるようにするため弊社では「php-monad」というライブラリを作成しました。

php-monadとは?

「php-monad」は、PHPで関数型プログラミングにおけるモナドの概念を実装したライブラリです。特にRust言語のOption/Result型に着想を得ており、次の二つの型を提供します:

  1. Option<T> - 「値がある」状態(Some<T>)か「値がない」状態(None)かを表現する型
  2. Result<T, E> - 「成功」状態(Ok<T>)か「失敗」状態(Err<E>)かを表現する型

これらを利用することで、より型安全で明示的なエラーハンドリングを実現できます。

インストール方法

Composerを使用してインストールできます:

composer require wiz-develop/php-monad

Option型の使い方

Option型は「値があるかもしれないし、ないかもしれない」という状況を表現するのに適しています。以下に使用例を示します:

use WizDevelop\PhpMonad\Option;

// 値がある場合(Some)
$some = Option\some("Hello, World!");
echo $some->unwrap(); // "Hello, World!"

// 値がない場合(None)
$none = Option\none();
// $none->unwrap(); // 例外が発生するため、コメントアウト

// 値の存在チェック
if ($some->isSome()) {
    echo "値が存在します\n";
}

// デフォルト値の提供
echo $none->unwrapOr("デフォルト値"); // "デフォルト値"

// mapによる値の変換 (値がある場合のみ変換される)
$length = $some->map(fn($str) => strlen($str)); // Some(13)
echo $length->unwrap(); // 13

// フィルタリング(条件に合う場合のみSomeを保持)
$filtered = $some->filter(fn($str) => strlen($str) > 10); // Some("Hello, World!")
$filtered = $some->filter(fn($str) => strlen($str) > 20); // None

実際のユースケース例

データベースからのユーザー検索の例を考えてみましょう:

// 従来の方法
function findUser($id) {
    $user = DB::getUserById($id);
    return $user; // 見つからない場合はnull
}

// Option型を使った方法
function findUser($id): Option {
    $user = DB::getUserById($id);
    return $user ? Option\some($user) : Option\none();
}

// 使用例
$userId = 123;
$user = findUser($userId);

// 安全なアクセス
$username = $user
    ->map(fn($u) => $u->name)
    ->unwrapOr('ゲスト');

// 連鎖処理(ユーザーが見つかった場合のみ設定を取得)
$userPreference = $user
    ->flatMap(fn($u) => findUserPreference($u->id))
    ->unwrapOr(new DefaultPreference());

この例では、ユーザーが見つからない場合にも明示的に処理され、nullチェックの忘れによるエラーを防ぐことができます。

Result型の使い方

Result型は「処理が成功するか失敗するか」という状況を表現するのに適しています。例外を投げる代わりに使用することで、エラーハンドリングを型安全に行えます:

use WizDevelop\PhpMonad\Result;

// 成功の場合(Ok)
$ok = Result\ok(42);
echo $ok->unwrap(); // 42

// 失敗の場合(Err)
$err = Result\err("処理に失敗しました");
// $err->unwrap(); // 例外が発生するので、コメントアウト
echo $err->unwrapErr(); // "処理に失敗しました"

// 成功/失敗のチェック
if ($ok->isOk()) {
    echo "処理は成功しました\n";
}

if ($err->isErr()) {
    echo "エラーが発生しました: " . $err->unwrapErr() . "\n";
}

// デフォルト値の提供
echo $err->unwrapOr(0); // 0

// 成功値の変換
$doubled = $ok->map(fn($n) => $n * 2); // Result\ok(84)
echo $doubled->unwrap(); // 84

// エラー値の変換
$newErr = $err->mapErr(fn($e) => "エラー詳細: " . $e);
echo $newErr->unwrapErr(); // "エラー詳細: 処理に失敗しました"

実際のユースケース例

APIからのデータ取得処理を考えてみましょう:

// 従来の方法
function fetchData($url) {
    try {
        $response = HttpClient::get($url);
        if ($response->status === 200) {
            return json_decode($response->body);
        } else {
            throw new Exception("APIエラー: " . $response->status);
        }
    } catch (Exception $e) {
        // ここでエラーログを記録したり、デフォルト値を返すなど
        return null; // または再スロー
    }
}

// Result型を使った方法
function fetchData($url): Result {
    try {
        $response = HttpClient::get($url);
        if ($response->status === 200) {
            return Result\ok(json_decode($response->body));
        } else {
            return Result\err([
                'type' => 'http_error',
                'status' => $response->status,
                'message' => $response->statusText
            ]);
        }
    } catch (Exception $e) {
        return Result\err([
            'type' => 'exception',
            'message' => $e->getMessage()
        ]);
    }
}

// 使用例
$dataResult = fetchData('https://api.example.com/data');

// 成功時の処理
$processedData = $dataResult
    ->map(fn($data) => processData($data))
    ->unwrapOr([]);

// エラーハンドリング - 型安全かつ明示的
if ($dataResult->isErr()) {
    $error = $dataResult->unwrapErr();
    if ($error['type'] === 'http_error' && $error['status'] === 404) {
        echo "リソースが見つかりませんでした";
    } else {
        echo "APIエラー: " . $error['message'];
    }
}

// 成功した場合のみ次の処理に進む
$finalResult = $dataResult
    ->flatMap(fn($data) => processStep1($data))
    ->flatMap(fn($data) => processStep2($data))
    ->flatMap(fn($data) => processStep3($data));

この例では、エラーの種類や詳細を失うことなく、かつ例外に頼らずにエラーハンドリングができています。また、処理の連鎖(チェーン)も明示的で、途中で失敗した場合は以降の処理が自動的にスキップされます。

Option型とResult型の相互変換

php-monadではOption型とResult型の間での変換も簡単にできます:

// Option -> Result
$option = Option\some(42);
$result = $option->okOr("値がありません"); // Ok(42)

$none = Option\none();
$result = $none->okOr("値がありません"); // Err("値がありません")

// Result -> Option
$result = Result\ok(42);
$option = $result->toOption(); // Some(42)

$result = Result\err("エラー発生");
$option = $result->toOption(); // None

API リファレンス

Option<T>

静的メソッド

  • Option\some($value) - Some(T)インスタンスを生成
  • Option\none() - Noneインスタンスを生成
  • Option\from($value) - 値からOptionを生成(nullの場合はNone、それ以外はSome)

インスタンスメソッド

  • isSome(): bool - 値が存在するかチェック
  • isNone(): bool - 値が存在しないかチェック
  • unwrap(): T - 値を取り出す(Noneの場合は例外)
  • unwrapOr($default): T - 値を取り出すか、デフォルト値を返す
  • map(callable $f): Option<U> - 値を変換
  • filter(callable $predicate): Option<T> - 条件に合致する場合のみSomeを返す
  • expect(string $msg): T - 値を取り出す(Noneの場合はカスタムメッセージで例外)

Result<T, E>

静的メソッド

  • Result\ok($value) - Ok(T)インスタンスを生成
  • Result\err($error) - Err(E)インスタンスを生成

インスタンスメソッド

  • isOk(): bool - 処理が成功したかチェック
  • isErr(): bool - 処理が失敗したかチェック
  • unwrap(): T - 値を取り出す(Errの場合は例外)
  • unwrapOr($default): T - 値を取り出すか、デフォルト値を返す
  • unwrapErr(): E - エラー値を取り出す(Okの場合は例外)
  • map(callable $f): Result<U, E> - 値を変換
  • mapErr(callable $f): Result<T, F> - エラー値を変換
  • expect(string $msg): T - 値を取り出す(Errの場合はカスタムメッセージで例外)

関数型プログラミングのメリット

php-monadを使うことで、以下のような関数型プログラミングのメリットを享受できます:

  1. 明示的なエラーハンドリング - 例外に頼らずエラーを明示的に処理
  2. 型安全性の向上 - 「値がない」や「処理の失敗」を型として表現
  3. nullチェックの忘れ防止 - 値の存在に関する処理を強制
  4. 関数の合成を容易に - map, flatMapなどのメソッドを使って処理をスムーズに連鎖
  5. 副作用の分離 - 純粋な処理と副作用のある処理を明確に分離

ユースケース別の利用シーン

1. フォームバリデーション

function validateEmail($email): Result {
    if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return Result\ok($email);
    }
    return Result\err("無効なメールアドレスです");
}

function validatePassword($password): Result {
    if (strlen($password) >= 8) {
        return Result\ok($password);
    }
    return Result\err("パスワードは8文字以上である必要があります");
}

// 利用例
$emailResult = validateEmail($input['email']);
$passwordResult = validatePassword($input['password']);

// 両方のバリデーションが成功した場合のみユーザー登録
if ($emailResult->isOk() && $passwordResult->isOk()) {
    registerUser($emailResult->unwrap(), $passwordResult->unwrap());
} else {
    // エラーメッセージを収集
    $errors = [];
    if ($emailResult->isErr()) {
        $errors['email'] = $emailResult->unwrapErr();
    }
    if ($passwordResult->isErr()) {
        $errors['password'] = $passwordResult->unwrapErr();
    }
    
    displayErrors($errors);
}

2. データベースアクセス

function findUserById($id): Option {
    $user = DB::table('users')->find($id);
    return $user ? Option\some($user) : Option\none();
}

function getUserOrCreate($id, $defaultData): Result {
    try {
        return findUserById($id)
            ->map(fn($user) => Result\ok($user))
            ->unwrapOr(function() use ($defaultData) {
                try {
                    $userId = DB::table('users')->insert($defaultData);
                    $newUser = DB::table('users')->find($userId);
                    return Result\ok($newUser);
                } catch (Exception $e) {
                    return Result\err("ユーザー作成に失敗: " . $e->getMessage());
                }
            });
    } catch (Exception $e) {
        return Result\err("データベースエラー: " . $e->getMessage());
    }
}

3. 複雑な処理フロー

function processOrder($orderId): Result {
    return findOrder($orderId)
        ->flatMap(function($order) {
            // 在庫確認
            return checkInventory($order->items)
                ->flatMap(function($inventoryStatus) use ($order) {
                    // 支払い処理
                    return processPayment($order->payment)
                        ->flatMap(function($paymentResult) use ($order) {
                            // 配送設定
                            return setupShipping($order)
                                ->map(function($shippingInfo) use ($order, $paymentResult) {
                                    // すべて成功したら注文完了データを返す
                                    return [
                                        'order' => $order,
                                        'payment' => $paymentResult,
                                        'shipping' => $shippingInfo,
                                        'status' => 'completed'
                                    ];
                                });
                        });
                });
        });
}

// 使用例
$orderResult = processOrder($orderId);

if ($orderResult->isOk()) {
    $completedOrder = $orderResult->unwrap();
    sendOrderConfirmation($completedOrder);
    updateInventory($completedOrder['order']->items);
} else {
    $error = $orderResult->unwrapErr();
    handleOrderError($orderId, $error);
}

まとめ

php-monadライブラリを使用することで、PHPプログラミングにおける型安全性とエラーハンドリングを大幅に改善できます。特に、以下のようなケースでその効果が発揮されます:

  • nullチェックを忘れやすい場所での値の処理
  • 複雑なエラーハンドリングを伴う処理
  • 複数の処理ステップを連鎖させる必要がある場合
  • 副作用とビジネスロジックを分離したい場合

「値がない」という状態や「処理の失敗」という状態を明示的に型として表現することで、より安全でメンテナンス性の高いコードが書けるようになります。関数型プログラミングの概念を取り入れることで、PHPコードの品質と可読性を向上させましょう。

php-monadはフレームワークに依存しないため、Laravelなどの既存フレームワークと組み合わせて使用することも可能です。ぜひ導入してみてください。

参考リンク

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?