1
1

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 型)

Last updated at Posted at 2025-05-14

はじめに

PHPでの開発では、「値が存在しない」状態や「処理が失敗する」状況に頻繁に遭遇します。従来、こうした状態の表現にはnull値や例外を使うのが一般的でしたが、これらには次のような課題があります。

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

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

php-monadライブラリについて

「php-monad」は、PHPで関数型プログラミングのモナド概念を実装したライブラリです。Rust言語のOption/Result型を参考に設計されており、次の2つの型を提供します。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?