Option モナド
Option は「値の有無」を型で表現するモナドです。Rust の Option<T> に着想を得ています。
概要
Option には 2 つの状態があります。
Some<T>: 値Tを保持している状態None: 値がない状態
use WizDevelop\PhpMonad\Option;
$some = Option\some(42); // Some<int> - 値あり
$none = Option\none(); // None - 値なしOption を作成する
some / none
最も基本的な作成方法です。
use WizDevelop\PhpMonad\Option;
$value = Option\some('hello'); // Some<string>
$empty = Option\none(); // NonefromValue
既存の値を Option に変換します。第 2 引数で「None とみなす値」を指定できます。
use WizDevelop\PhpMonad\Option;
// null は None になる(デフォルト)
$opt1 = Option\fromValue($user); // $user が null なら None、そうでなければ Some
// 0 を None とみなす
$opt2 = Option\fromValue($count, 0); // $count が 0 なら None
// 空文字を None とみなす
$opt3 = Option\fromValue($name, ''); // $name が '' なら Noneof
関数の戻り値を Option に変換します。
use WizDevelop\PhpMonad\Option;
$opt = Option\of(fn() => getUser($id));
// 戻り値が null なら None、そうでなければ SometryOf
関数を実行し、例外が発生した場合は None を返します。
use WizDevelop\PhpMonad\Option;
$date = Option\tryOf(
fn() => new DateTimeImmutable($dateString),
null,
\DateMalformedStringException::class
);
// 日付のパースに失敗したら None状態を判定する
isSome / isNone
$opt = Option\some(42);
if ($opt->isSome()) {
// 値がある場合の処理
}
if ($opt->isNone()) {
// 値がない場合の処理
}isSomeAnd
値があり、かつ述語を満たす場合に true を返します。
$opt = Option\some(10);
$opt->isSomeAnd(fn($x) => $x > 5); // true
$opt->isSomeAnd(fn($x) => $x > 15); // false
Option\none()->isSomeAnd(fn($x) => true); // false値を取得する
unwrap
値を取得します。None の場合は例外をスローします。
$opt = Option\some(42);
$value = $opt->unwrap(); // 42
$none = Option\none();
$none->unwrap(); // RuntimeException をスローexpect
カスタムエラーメッセージで値を取得します。
$opt = Option\some(42);
$value = $opt->expect('値が必要です'); // 42
$none = Option\none();
$none->expect('値が必要です'); // RuntimeException: 値が必要ですunwrapOr
デフォルト値を指定して取得します。
$opt = Option\some(42);
$opt->unwrapOr(0); // 42
$none = Option\none();
$none->unwrapOr(0); // 0unwrapOrElse
デフォルト値を遅延評価で取得します。
$none = Option\none();
$none->unwrapOrElse(fn() => expensiveCalculation());
// None の場合のみ expensiveCalculation() が実行されるunwrapOrThrow
None の場合に指定した例外をスローします。
$none = Option\none();
$none->unwrapOrThrow(new NotFoundException('ユーザーが見つかりません'));値を変換する
map
値を変換します。None の場合はスキップされます。
$opt = Option\some(5);
$result = $opt
->map(fn($x) => $x * 2) // Some(10)
->map(fn($x) => "値: $x"); // Some("値: 10")
$none = Option\none();
$none->map(fn($x) => $x * 2); // None(スキップ)mapOr
値を変換するか、デフォルト値を返します。
$opt = Option\some(5);
$opt->mapOr(fn($x) => $x * 2, 0); // 10
$none = Option\none();
$none->mapOr(fn($x) => $x * 2, 0); // 0mapOrElse
値を変換するか、デフォルト値を遅延評価で取得します。
$none = Option\none();
$result = $none->mapOrElse(
fn($x) => $x * 2,
fn() => calculateDefault()
);filter
述語を満たさない場合は None になります。
$opt = Option\some(10);
$opt->filter(fn($x) => $x > 5); // Some(10)
$opt->filter(fn($x) => $x > 15); // Noneinspect
値を検査(副作用を実行)し、自身を返します。デバッグに便利です。
$result = Option\some(42)
->inspect(fn($x) => var_dump($x)) // int(42) を出力
->map(fn($x) => $x * 2)
->unwrap();論理演算
and
両方が Some の場合、右側の Option を返します。
$a = Option\some(1);
$b = Option\some(2);
$a->and($b); // Some(2)
$a->and(Option\none()); // None
Option\none()->and($b); // NoneandThen
値を受け取り Option を返す関数を適用します(flatMap)。
function parsePositive(int $x): Option {
return $x > 0 ? Option\some($x) : Option\none();
}
$opt = Option\some(5);
$opt->andThen(fn($x) => parsePositive($x)); // Some(5)
$opt2 = Option\some(-1);
$opt2->andThen(fn($x) => parsePositive($x)); // Noneor
左が None なら右を返します。
$a = Option\some(1);
$b = Option\some(2);
$a->or($b); // Some(1)
Option\none()->or($b); // Some(2)orElse
左が None なら右を遅延評価します。
$none = Option\none();
$none->orElse(fn() => Option\some(fetchDefault()));orThrow
None の場合に例外をスローし、Some の場合はそのまま返します。
$opt = Option\some(42);
$opt->orThrow(new Exception('エラー')); // Some(42)
$none = Option\none();
$none->orThrow(new Exception('エラー')); // Exception をスローxor
どちらか一方だけが Some の場合、その値を返します。
$a = Option\some(1);
$b = Option\some(2);
$a->xor($b); // None(両方 Some なので)
$a->xor(Option\none()); // Some(1)
Option\none()->xor($b); // Some(2)
Option\none()->xor(Option\none()); // NoneResult への変換
okOr
Some を Ok に、None を指定したエラーの Err に変換します。
use WizDevelop\PhpMonad\Option;
$opt = Option\some(42);
$result = $opt->okOr('エラー'); // Ok(42)
$none = Option\none();
$result = $none->okOr('エラー'); // Err('エラー')okOrElse
エラー値を遅延評価します。
$none = Option\none();
$result = $none->okOrElse(fn() => createError());ユーティリティ関数
flatten
Option<Option<T>> を Option<T> に平坦化します。
use WizDevelop\PhpMonad\Option;
$nested = Option\some(Option\some(42));
Option\flatten($nested); // Some(42)
$nested2 = Option\some(Option\none());
Option\flatten($nested2); // Nonetranspose
Option<Result<T, E>> を Result<Option<T>, E> に変換します。
use WizDevelop\PhpMonad\Option;
use WizDevelop\PhpMonad\Result;
Option\transpose(Option\some(Result\ok(42))); // Ok(Some(42))
Option\transpose(Option\some(Result\err('e'))); // Err('e')
Option\transpose(Option\none()); // Ok(None)イテレーション
Option は IteratorAggregate を実装しているため、foreach で使用できます。
$opt = Option\some(42);
foreach ($opt as $value) {
echo $value; // 42
}
$none = Option\none();
foreach ($none as $value) {
// 実行されない
}典型的なパターン
null チェックの置き換え
// Before
$name = null;
if ($user !== null) {
$profile = $user->getProfile();
if ($profile !== null) {
$name = $profile->getName();
}
}
$displayName = $name ?? 'Anonymous';
// After
$displayName = Option\fromValue($user)
->map(fn($u) => $u->getProfile())
->map(fn($p) => $p->getName())
->unwrapOr('Anonymous');配列からの安全な取得
function getArrayValue(array $arr, string $key): Option {
return Option\fromValue($arr[$key] ?? null);
}
$value = getArrayValue($config, 'timeout')
->filter(fn($t) => $t > 0)
->unwrapOr(30);