PHPer vs モナド
こんにちは! モナド書いてますか? 私は書いてません!
関数プログラミングにはモナドがつきものということで、近年はScalaやF#のような言語でもモナドや類似の概念が愛用されています。
ところで筆者はRubyやLispのような動的言語でのダイナミックなプログラミングが大好きです。Lispも関数型言語と呼ばれることがありますが、基本的にはモナドのモの字も出てきません。そんなこんなで長期間にわたって関心を払いつつ、完全マスターしようと本を読んでも10年以上経ってもしっくりこないままです。
今回の記事の目的は、そのよくわからない概念をPHPに再現することで理解を試みることです。
モナドの背景
世の中には、いくつものプログラミング言語がありますが、今回の対象となる「モナド」はプログラムを抽象化する概念です。本来は圏論という数学の一ジャンルの用語ですが、もっぱら純粋静的型付き関数型と呼ばれるジャンルのプログラミング言語に豊富な表現能力を提供します。
では、その便利なモナドとは何なのでしょうか。ある人はプログラマブルなコンテナーに過ぎないと言い、またあるひとは制御可能なセミコロンだと言い、またある人は 「モナドは単なる自己関手の圏におけるモノイド対象だよ。何か問題でも?」 などと言います。 日本語でおk。
実際のところ、モナドの説明はたくさんあります。日本語で出版されたもの、オンラインのブログ記事、スライドなど大量に。そしてPHPでモナドを実装したという記事もあります。
……うむ、よくわからん。なるほど便利そうだという気持ちと、これを動的言語でやって意味あるのかという気持ちが共存しています。しかし現在は2022年、PHPも立派な静的型付き言語になりました。実装して目にものを見せてやりましょう。
純粋関数と副作用
用語を整理しましょう。ある関数が「純粋(pure)」であるということについて、しばしば曖昧に説明されることがあります。たとえば以下のような条件です。
- A: 同じ引数を渡せば必ず同じ結果が返ること
- B: 関数呼び出しが外部に影響を及ぼさないこと
- C: 外部状態によって関数呼び出しの戻り値が変わらないこと
たとえば$add = fn($a, $b) => $a + $b;
という関数を見たとき、小学校の算数で習ったように1+2
や10+20
のように+
の左右が同じならば、結果も必ず同じになります。つまりAの条件を満たします。
Bは深く考えると、とても難しい概念です。PHPのような種類のプログラミング言語においては、むしろ 外部に影響を及ぼすことが本質 であるとも考えられるからです。たとえばfwrite()
です。これはファイルポインタを受け取って、ポインタが指すリソースに文字列を書き込みます。このような関数は「不純(impure)」、つまり純粋ではないものだと見なされえます。このとき「リソースにデータを書き込む」のような、戻り値に現れない影響を「副作用(side effect)」と呼びます。
Cも似ていますが、すこし別の例を考えてみましょう。たとえばmicrotime()
やrandom_int()
は呼びだすたびに別の値を返します。どちらの関数も引数に渡された値とは何の関係もなく、前者は現在時刻のマイクロ秒を、後者はランダムな数字を返します。
PHPではB, Cに違反している例のどちらも間違いなく純粋関数ではないと言えそうですが、「外部」という表現がやや曖昧に感じられます。ここでは参照透明という概念を持ち出してみましょう。これは関数呼び出しで言うと、式の呼び出しと呼び出しの結果を置き換えても意味が変わらないようなことです。たとえば以下のような関数呼び出しの置き換えは成り立ちます。
$add = fn($a, $b) => $a + $b;
$result = $add(2, 3); // => 5
// ↓ 書き換え
$result = 5;
同じように、$result = pi()
を$result = 3.141592653589793
と置き換えても間違いなく成り立つ一方、$result = microtime(true);
を$result = 1646550888.952778;
と置き換えても意味はありません。microtime(true)
は現在時刻の取得こそが機能の主目的であり、1646550888.952778
はある時点を表す数字に過ぎないからです。
さて、純粋関数型言語というジャンルの言語は、参照透明であることをプログラミングの基本的なスタイルとしています。このようなプログラミング言語にはいくつかありますが、代表的なものにHaskellがあります。
PHPにおけるpure
本記事の本題であるモナドに触れる前に、PHPにおける純粋関数の扱いについて触れておきましょう。IDEであるPhpStormや、Psalm、PHPStanといった静的解析ツールはpure/impureの概念を取り扱えます。
/**
* 足し算をする関数
*/
function add(int $a, int $b): int
{
return $a + $b;
}
// 足し算をする…が結果を使っていない
add(1, 2);
// 結果をちゃんと使っているのでヨシ
echo add(1, 2);
静的解析ツールにある関数が純粋であるかどうかを検出させるには以下のような方法があります。
- 関数/メソッド定義のPHPDocに
@pure
と書く- 多くのツールで広くサポート
- ただしツールごとに振舞いが異なる
-
JetBrains\PhpStorm\Pure
アトリビュート- 原則PhpStormのみサポート
- 静的解析ツールの自動検出に期待する
- 現時点であまり多くは期待できない
この記事の主題ではないので詳しく触れませんが、本稿執筆時点(2022年2月)でPHP静的解析ツールの純粋性サポートには濃淡があります。たとえばPhpStormは純粋関数の自動検出ができる、Psalmは@pure
のほか@psalm-mutation-free
や@psalm-external-mutation-free
など細かいレベルでの副作用の有無を表明できます。ツールごとの機能の癖などを語るときりがないのですが、これを語るのは別の機会に譲りましょう。
純粋性のサポートとは別に、この記事においてはPHPStanをメインで活用します。
HaskellとPHPの違い
HaskellとPHPは「全然違う言語だ」と言い切ることは簡単なのですが、実際の大きな違いを見てみましょう。以下はHaskellのREPL(対話的実行環境)としてghci
コマンドで関数を定義してから呼び出して結果を表示するまでです。行頭が>
から始まる行がキーボードで入力したコードです。
> add a b c = a + b + c
> :t add
add :: Num a => a -> a -> a -> a
> add 10 20 30
60
このコードは型宣言をしていないのですが、:t add
で調べるとNum
という型がついています。これは型クラスというもので、+
などで数値として計算できるという共通した性質を持ったデータ型をまとめて取り扱うものです。
上記のコードをPHPに直訳すると、次の2種類の書きかたがあります。
$add = fn($a, $b, $c): int|float => $a + $b + $c;
var_dump($add(10, 20, 30));
// => int(60)
$add =
fn($a) =>
fn($b) =>
fn($c): int|float => $a + $b + $c;
var_dump($add(10)(20)(30));
// => int(60)
Haskellの関数は、必ず1つの値を受け取って1つの値を返すという構造をとります (ただし、タプルを使って複数の値の組をまとめてとることは可能)。
また、PHPは基本的にint|float
のように自分で明示的に型を書かないと基本的には静的な型がつきません。
モナドを導入する
Haskellのモナドは以下のような定義になっています。Haskellという言語は記号を使った関数(演算子)も定義できるので>>=
と>>
も関数です。
type Monad :: (* -> *) -> Constraint
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
Haskellによくある記号オペレータですが前者はbind
、後者はthen
のように呼ばれるようです。then
はなくても一旦どうにかなりそうです。
(>>=) :: m a -> (a -> m b) -> m b
は記号が入り組んでいますが、>>=(bind)
という関数に関する型定義です。Int -> Bool
のように書くと、「引数にInt
(整数)をとって、Bool
(真偽値)を返す関数」のように読みます。a
やb
は型パラメーターといって、Int
やBool
のような具体的な型名ではなく変数のような仮置きした型を扱うことができます。ここでは、モナドが内包している値の型がa
として置かれています。
まとめると「型A
を内包するモナドと『型A
を引数にとって型B
を内包するモナドを返す関数』を引数にとって、型B
を内包するモナドを返す関数」となります。これはとてもややこしい……。
さて、型パラメーターを使ったプログラミングの機能をジェネリクスまたはテンプレートと呼びます。
PHPでは以下のように定義してみます。
<?php
declare(strict_types=1);
namespace zonuexe\Mona;
use Closure;
/**
* @template T
*/
interface Monad
{
/**
* @template T2
* @param Closure(T):static<T2> $f
* @return static<T2>
*/
public function bind(Closure $f): Monad;
/**
* @template TValue
* @param TValue $value
* @return static<TValue>
*/
public static function unit(mixed $value): Monad;
}
最初の@template
はとても重要なタグです。interface
とメソッド定義の両方に存在します。諸々の実装を見ると>>=
はbind
やflatMap
だったり、return
はpure
やunit
だったり風習によるらしいのですが、今回はbind
とunit
にそろえます。
bind
の@param Closure(T):static<T2> $f
と@return static<T2>
という型定義に注目してみましょう。
これはPHPStanなど一部の静的解析ツールが解釈できる形式の記法で、T
という型の引数をひとつ受け取ってT2
という型を返すクロージャ(無名関数)ということです。T
はモナドがもともと内包している型、T2
はその関数が返す型です。static<T2>
はモナドを実装したクラスでその関数が返した値を返すということです。
……わかったようなわからないような感じですが、「bind
はflatMap
と相当する」のだというのが肝なようです。flatMap
はPHPにないので耳なじみのない関数ですが、map
したあとでflat
、つまり1段平らにする関数です。
「1段平らにする」とは以下のような操作です。
$a = [[1, 2],[3, 4], [5, 6]];
$result = array_merge(...$a);
// [1, 2, 3, 4, 5, 6]
なぜこんな演算が必要なのでしょうか。「flatしないmap」とは以下のようなイメージです。
$x = [1, 2, 3, 4, 5];
$f = fn(int $n) => $n * 2;
$result = array_map($f, $x);
// [2, 4, 6, 8, 10]
このarray_map()
を使ったPHPの式は、算数で習った以下のような表と同じです。
一方でflatMap
はどんなときに嬉しいのでしょうか。あまりない例ですが、たとえば「1から10までの数のうち、3の倍数だけが2つ並んだ配列を作れ」という例を考えます。
$a = range(1, 10);
$f = fn(int $n) => ($n % 3) === 0 ? [$n, $n] : [];
$result = array_merge(...array_map($f, $a));
// [3, 3, 6, 6, 9, 9]
元の$a
が1〜10の長さ10の配列だったのに対して、結果は長さ6になっていることがおわかりいただけるでしょうか。配列に対してmap
すると同じ長さの配列しか得られませんが、flatMap
として考えることで配列は伸縮自在になるのです。これはさりげにうれしい。
モナドは何をする人ぞ
ここまでモナドモナドと抽象的なことばかりを連ねてきました。これまで書いてきたコードは、モナドとは直接の関連がない数行のサンプルコードを除けば型宣言だったり、インターフェイス定義だけです。実際に使われるモナドというものはもっとずっと具体的なものです。
モナドは種類に応じて「○○モナド」のように呼ばれます。よく言われるのは「IOモナド」「Maybeモナド」「Listモナド」などでしょうか。ほかにもいろんなモナドを聞いたような気がしますが一旦おいておきましょう。今は…… 因子が足りない……。
IOモナドは仄聞するにデータを読み込んだり書き出したりするような何かを取り扱えるようです。Maybeモナドは結果があるかもしれないし、ないかもしれないというようなものに使えるようです。Listモナドに至っては、なんとリストはモナドだというのです。うーむ、共通性が読めない。何を言っているのだ。仕方ない、実装するぞ。
Maybeモナド
筆者は英語が苦手なのですが、“maybe” は英語の副詞として「たぶん」「もしかして」、名詞として「不確か」「不確実性」のような意味もあるようです。
HaskellのMaybe
は以下のような定義になっています。
data Maybe a = Nothing | Just a
Nothing
とJust
の2種類があればいいようです。よーし、PHPでは以下のようにabstract class
と継承で定義すればいいな。
/**
* @template T
* @implements Monad<T>
*/
abstract class MaybeMonad implements Monad
{
/**
* @template TValue
* @param TValue $value
* @return MaybeMonad<TValue>
*/
public static function unit(mixed $value): self
{
return new Just($value);
}
}
/**
* @template T
* @extends MaybeMonad<T>
*/
final class Just extends MaybeMonad
{
/**
* @param T $value
*/
public function __construct(
protected mixed $value
) {
}
/**
* @param Closure(T):static<T> $f
* @return static<T>
*/
public function bind(Closure $f): self
{
return $f($this->value);
}
}
/**
* @template T
* @extends MaybeMonad<T>
*/
final class Nothing extends MaybeMonad
{
/**
* @phpstan-param T $value
*/
public function __construct(mixed $value) {} // @phpstan-ignore-line
/**
* @template T2
* @param Closure(T):static<T2> $f
* @return static<T2>
*/
public function bind(Closure $f): self
{
/** @phpstan-var T2 */
$v = null;
return self::unit($v);
}
/**
* @template TValue
* @param TValue $value
* @return Nothing<TValue>
*/
public static function unit(mixed $value): self
{
return new Nothing($value);
}
}
さて、このモナドはどう使えばよいのでしょうか。ここでは文字列が整数として解釈可能ならパースして値を返し、解釈できない文字列だったら失敗した状態を返すという関数を考えます。
/**
* @return MaybeMonad<int>
*/
function parseIntM(string $value): MaybeMonad
{
return ctype_digit($value)
? Just::unit((int)$value)
: Nothing::unit(0);
}
$result1 = parseIntM("123"); // => Just(123)
$result2 = parseIntM("abc"); // => Nothing
// 数を2倍にしてモナドにくるんで返す関数
$f = fn(int $n) => MaybeMonad::unit($n * 2);
$result1a = $result1->bind($f); // => Just(246)
$result2a = $result2->bind($f); // => Nothing
ただ… PHPにモナドって必要なんでしょうか。これって、PHPだったら常識的にこうしませんか?
function parseInt(string $value): int|false
{
return ctype_digit($value) ? (int)$value : false;
}
$result1 = parseInt("123"); // => 123
$result2 = parseInt("abc"); // => false
うむ、すっきり! やっぱりPHPにモナドなんて要らなかったんや。
これは妥当な結論ですね。ただ… うっかりした人間が実装するとどうなるのでしょう。
// 数がfalseじゃなかったら2倍にして返す
$good = fn(int|false $n) =>
($n !== false) ? $n * 2 : false;
// 数を2倍にして返す関数
$bad = fn(int|false $n) => $n * 2;
私のようなウカツな人間がこの$bad
関数のようなものを作ってしまうと、このようなリスクがありえます。PHPではnull
やfalse
のような値に数値として計算しようとするとエラーもなく0
にキャストされてしまうのです。そうなったが最後、意図的に計算された0と偶然入り込んだエラー値を選り分けることはできません。なんたること…!
ところで、PHPでも似た考えを無意識に活用しているとは考えられないでしょうか。
// 本を検索し、結果がなければ空配列を返す関数
$books = searchBooks($keyword);
$author_ids = array_column($books, 'author_id');
$authors = array_column(searchAuthor($author_ids), null, 'id');
foreach ($books as $book) {
$book->author = $authors[$book->author_id)] ?? null;
}
ここでarray_column()
とforeach
を使っていますが、ここで$books
が空配列かどうかの条件分岐を書かなくてもうまく動作するということはおわかりいただけますでしょうか。どちらの機能も内部的には配列の内容を走査します。「配列を0回以上走査する」と「配列要素が1個以上なら走査する」は、多くの場合において同じだということです。
この考えを活用して、モナドの代用として配列を使ってしまいましょう。
/**
* @return list<int>
*/
function parseIntA(string $value): array
{
return ctype_digit($value)
? [(int)$value]
: [];
}
$result1 = parseIntA("123"); // => [123]
$result2 = parseIntA("abc"); // => []
// 2倍にして返す関数
$f = fn(int $n) => $n * 2;
$result1a = array_map($f, $result1);
// => [123]
$result2a = array_map($f, $result1);
// => []
これは実装が最低限で綺麗かもしれないですね。このコードにおいて明示的な条件分岐はparseIntA()
の中にしかありません。$f
もただ2倍にするだけでいいので私のようなウカツな人間にも安心です。
綺麗だからといって「あらゆる結果を配列で返す」という習慣がPHPに馴染むかどうかというのは置いておいて、ある程度安全なコードにはなりました。
ただしPHPのような言語でユニオン型とつきあうというのは、このようなリスクと隣り合わせであるということは覚悟しておいてください。
Listモナド
そもそもの話なのですが、コンピュータで「何かが並んだもの」を表現するときには、ざっくり言うと以下の2種類の方法があるらしいです。
- データを並べて格納するのに十分な連続した空間を確保して詰め込む
- 要素のデータと次のデータのありかの組を個数分作って数珠つなぎにする
一般的には前者を配列(array)、後者を連結リスト(linked list)と呼ぶみたいです。単に「リスト」といった場合は連結リストを指すことがあり、今回触れるListモナドも、どうやらそうです。筆者はリスパーという属性の人間なので辛うじて作りかたがわかります。やるぞー。
/**
* @template T
* @implements Monad<T>
* @implements IteratorAggregate<T>
*/
class ListMonad implements Monad
{
/** @var T */
private $car;
/** @var ListMonad<T> */
private $cdr;
/**
* @param T $car
* @param ListMonad<T> $cdr
*/
public function __construct($car, ListMonad $cdr = null)
{
if ($car !== null) {
$this->car = $car;
$this->cdr = $cdr ?? $this->nil($car);
}
}
/**
* @param T $v
* @param ListMonad<T> $list
* @return ListMonad<T>
*/
public static function cons($v, ListMonad $list): ListMonad
{
return new ListMonad($v, $list);
}
/**
* @param T $v
* @return ListMonad<T>
*/
public static function nil($v = null): ListMonad
{
/** @var ListMonad<T> */
$list = new ListMonad(null);
return $list;
}
/**
* @template T2
* @param Closure(T):static<T2> $f
* @return static<T2>
*/
public function bind(Closure $f): ListMonad
{
if ($this->car === null) {
return $this;
}
return $f($this->car);
}
/**
* @template TValue
* @param TValue $value
* @return static<TValue>
*/
public static function unit($value): ListMonad
{
return new ListMonad($value);
}
}
よし、これでモナドの条件を満たす最低限の定義のリストになったはずじゃ。なんとか半ページの実装で済んだぞ。
モナドの条件を満たす? モナドというのはなんかモナド則(Monad law)というやつを守らなきゃいけないみたいです。Haskellミリしらというほどのことではないですが、1センチくらいの理解度でPHPUnitに書き直すとこうなります。
/**
* @template TMonad of Monad
*/
abstract class MonadTest extends TestCase
{
/**
* @return iterable<array{TMonad<string>}>
*/
abstract public function monadsProvider(): iterable;
/**
* Test Monad laws
*
* @see https://wiki.haskell.org/Monad_laws
*/
public function testMonadLaws(): void
{
$subject = $this->getSubject();
$monad = get_class($subject);
/** @var \Closure(string): TMonad<string> */
$return = fn($v): Monad => $monad::unit($v);
$f = fn(string $n): Monad => $monad::unit($n . 1);
$g = fn(string $n): Monad => $monad::unit($n . 2);
// return x >>= f == f x
$this->assertEquals(
$monad::unit('x')->bind($f),
$f('x'),
'Monad law - Left identity'
);
// m >>= return == m
$this->assertEquals(
$subject->bind($return),
$subject,
'Monad law - Right identity'
);
// (m >>= f) >>= g == m >>= (\x -> f x >>= g)
$this->assertEquals(
$subject->bind($f)->bind($g),
$subject->bind(fn($x) => $f($x)->bind($g)),
'Monad law - Associativity'
);
}
}
どうでもいい… とまで言ってしまうとそれは明らかに言いすぎなのですが、紙幅を無限に使うことは今回の記事の本筋ではないので、さくさく次に行きます。ListMonad
のテストはこれだけです。
/**
* @extends MonadTest<ListMonad>
*/
class ListMonadTest extends MonadTest
{
public function monadsProvider()
{
yield 'unit' => ListMonad::unit('Monad');
}
}
PHPのabstract class
ってどういうときに使うのかと尋ねられることがありますが、これがユースケースのひとつだということになります。べんりですね。ここではPHPUnitのデータプロバイダーという機能を活用しています。一般にデータプロバイダーはテストメソッドの引数にしたい要素を、配列の要素としてに詰めて渡すことが多いですが、実はこのようにyield
を用いてジェネレータとして返すこともできます。ただしPHPUnitはジェネレータを配列として実体化してからテストメソッドを実行しはじめますのでテストメソッドの評価が無限に終わらないということはありません。
さて、今回の実装対象はリストということは、その要素に一個づつアクセスできたりしたくなります。
ListMonad
のメソッド定義にこのような定義を足します。
class ListMonad implements Monad, IteratorAggregate
{
//
// 実装中略
//
/**
* @return Generator<T>
*/
public function getIterator(): Generator
{
if ($this->car === null) {
return;
}
yield $this->car;
yield from $this->cdr;
}
/**
* @template T2
* @param Closure(T,T2):T2 $f
* @param T2 $z
* @return T2
*/
public function foldr(Closure $f, $z)
{
if ($this->car === null) {
return $z;
}
return $f($this->car, $this->cdr->foldr($f, $z));
}
}
これでIteratorAggregate
によって、このListMonad
はPHPの言語レベルでforeach
ループできるようになりました。 めでたしめでたし…?
yield 'list' => ListMonad::list(‘a', 'b');
あとがき
本稿はここで筆を措くのですが、掲載されているListMonad
は一見モナド則を満たしていたものの役割を果たせていません。テストケースって大事ですね。もちろんこの問題は本稿執筆時点で解決済みです。
また、この記事のコードは基本的にすべて上記のGitHubリポジトリに公開されています。
筆者はこれまでモナドや純粋関数型は恰好いいというイメージがあったり友人の書いた圏論の同人誌の売り子をやったりしつつ、実用的なプログラミングにおけるモナドの概念というものには馴染めていませんでした。
モナドが実用的なPHPプログラミングに使えるかは… どうでしょうね。こういう抽象化の手法もあるんだということを理解してもらうだけで十分かもしれません。
ただし、今回のモナド実装においてはPHPStanといった静的型検査ライブラリの支援なしには容易に実装できなかったことも付け加えておきます。
現代的なPHPのジェネリクスサポートによってTypeScriptには及ばないながらも最低限の静的解析ができるポテンシャルの証明となっているはずです。PHPStan作者のOndřej Mirtesには深く感謝します。
参考資料
以下の書籍をぱらぱらっと読みました。この記事を書き上げたいまこそリベンジします。
以下のWebサイトは2022年3月1日時点の最新版を参照しています。
- 不完全にしておよそ正しくないプログラミング言語小史
- Haskellには副作用がないのか? - あどけない話
- モナドとはモナドである - モナドとわたしとコモナド
- Monad tutorial
- モナド則とは (モナドソクとは) [単語記事] - ニコニコ大百科
- Monad laws - HaskellWiki
- Haskell/Understanding monads - Wikibooks
- JQueryがモナドかどうかとか - bonotakeの日記
- モナド: お前はもう知っている - TIM Labs
- 高階型について - Togetter
- 関数を扱えるだけでは、モナドを表現するには不十分過ぎる - xuwei-k's blog
また、以下の資料は本稿執筆時点では時間の都合上読めていません。
本稿の直接の参考資料ではないのですが、今回のモナドや、それの理論的な基盤になっている圏論という友人のあいや氏の書いたゆるふわな同人誌を紹介します。
「せつラボ 〜圏論の基本〜」
「矢澤にこ先輩といっしょに代数!」
また、以下の書籍は10年以上前に読んで楽しかったので、またリベンジします。
?>
2023年の後書き
冒頭でも触れましたが、私がモナドの概念に触れたのはいつだったか正確には思い出せませんが、おそらく2010年夏から2011年までの間だったはずです。早期に読んだ資料は「モナドは象だ」か「7つの言語 7つの世界」ではなかったかと思います。
2011年頃は数学科出身のボスや先輩たちやアルバイトとしてF# MVPが居る会社で働いていたり、インターネッツで仲良くなった人たちがHaskellでおもしろいことをやっていたり、その頃から10年ほど心に刺さり続けていた杭だったような気がします。そこから10年、私にとってモナドは近付いたり遠ざかったりを繰り返す謎の概念であり続けました。この記事の執筆途中ですらそうでした。
今回の試みは2020年代になってPHPStanでPHPに静的型を付けるというアイディアが夢物語ではなくなったとき、静的型付き純粋函数プログラミングというコンセプトともう一度向き合わなければならないといけないという決意と適当な思い付きからはじまりました。
実際この記事自体は有志幾度も繰り返されてきたモナドについての素人解説の焼き直しのひとつに過ぎませんが、この記事を書き上げることはものわかりの悪い筆者がモナドと和解をする上で不可欠のプロセスだったように感じます。
一時は和解不能だと諦めていたモナド則は、PHPUnitでabstract class MonadTest
を書くことですべてのモナドをテストできるのだと理解できたときは真の和解を果たせたと思っています。
さて、この記事は毎年PHPerKaigiのパンフレットに寄稿してる記事の第2弾です。
第1弾はこれ。
PHPerKaigi 2023でのパンフレットではPHPStanのまじめな導入記事を掲載する予定です。
PHPerKaigi 2023はオンラインとオフラインのハイブリッド開催ですが、冊子はどっちでも届きます。
チケット買ってね!
Discussion