ハードル1:オブジェクト指向の設計方針と合わない
オブジェクト指向ではデータと操作をひとまとまりにして定義する。
これをベースに関数型を移植しようとすると、次の二点で結構つらい。
- クラス実装が大きくなる
- 後からインスタンスを追加できない
例えばFunctorやApplicativeなどを色々インポートした状態でMaybeを調べると
*Main> :i Maybe
data Maybe a = Nothing | Just a -- Defined in `Data.Maybe'
instance Eq a => Eq (Maybe a) -- Defined in `Data.Maybe'
instance Monad Maybe -- Defined in `Data.Maybe'
instance Functor Maybe -- Defined in `Data.Maybe'
instance Ord a => Ord (Maybe a) -- Defined in `Data.Maybe'
instance Read a => Read (Maybe a) -- Defined in `GHC.Read'
instance Show a => Show (Maybe a) -- Defined in `GHC.Show'
instance MonadFix Maybe -- Defined in `Control.Monad.Fix'
instance MonadPlus Maybe -- Defined in `Control.Monad'
instance Applicative Maybe -- Defined in `Control.Applicative'
instance Monoid a => Monoid (Maybe a) -- Defined in `Data.Monoid'
instance Alternative Maybe -- Defined in `Control.Applicative'
こんな感じでずらずらっと表示される。
オブジェクト指向的には、Maybeに対する操作はMaybeに実装すべきなんだろうけども、これを全部実装していくとimplementsとかtraitが大変なことになる。
EqやShowなど使わないものを抜いたとしても、インターフェースが多すぎてなんか違う感が半端ない。
他にも、fmapは
*Main> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
なんだけども、オブジェクト指向で普通に書くと第一引数と第二引数が逆転する。Scalaでもそうなっている。
これが関数を貼り合せるのに向いていない。
ハードル2:型推論がないとメソッドが呼び出せない
端的に言うと、pureやmemptyを実装できない。
mempty >>= bind とかは更にキツイ。
例えばguard関数を書こうとしてHaskellの定義を見て
guard :: (Alternative f) => Bool -> f ()
guard True = pure ()
guard False = empty
詰まる。
これが例えばap(Applicativeの<*>)なら
ap :: (Monad m) => m (a -> b) -> m a -> m b
引数で型が決まるので、第一引数または第二引数のクラスの ap を呼びだせば良い。
それが guard だと具体的な型がまったく分からない。
ハードル3:スカラー型を使いたい
arrayのモナド版 MonadArray を作ったり、文字列のモナド版 MonadString を作ったりしたくない。
巷で不評のPHP版arrayだけども、ガッチリ制限の付いたMonadArrayを作るよりゆるいPHP版arrayを使ってfmapとかmappendとかしたい。
回避1:型クラスに対応するクラスを分ける
そもそもPHPにJavaScriptやRubyのような柔軟性があれば問題なかったんだけども、スクリプト言語よりもむしろJavaっぽいので仕方がない。
仕方がないのでデータ型と型クラス階層を分ける。
/*
* data Maybe a = Nothing | Just a
*/
class Maybe
{
protected $value;
public function __construct($a)
{
$this->value = $a;
}
}
namespace Maybe;
use Maybe;
class Just extends Maybe
{
public function fromJust()
{
return $this->value;
}
}
class Nothing extends Maybe
{
public function __construct($a) {}
}
/*
* class Monad m where
*/
interface Monad extends Applicative
{
public static function bind($m, callable $f);
public static function ret($a);
}
namespace Monad;
use Applicative;
use Monad;
use Maybe as Instance;
/*
* instance Monad Maybe
*/
class Maybe extends Applicative\Maybe implements Monad
{
public static function bind($m, callable $f)
{
assert($m instanceof Instance, 'First argument must be Maybe');
if ($m instanceof Instance\Just)
$ret = $f($m->fromJust());
else
$ret = $m;
assert($ret instanceof Instance, 'Return value must be Maybe');
return $ret;
}
public static function ret($a)
{
return new Instance\Just($a);
}
}
これで、後からMonoidやMonadPlusに対応させようとしても大本のMaybeをいじらなくて済む。
ついでに arrayやstring型でも対応できる。
その代わり、Haskell風型クラスのインスタンスを探すための処理が必要になる。
回避2:何かの型だよクラスを作る
PHP5の戻り値の型が書けない状態では、型推論に近づけるためのうまい方法を思い付けなかったので、何でもありクラスを作る。
class Any
{
...
public function castByName($name){
....
}
public function cast($m){
....
}
}
実際の型が必要になったら、そのときに何とかする。
何ともできなかったらエラーになるんだけど。
途中まで作って、結局Scalaっぽくなるのかと気付いた。
実装サンプル
例によって https://github.com/nishimura/laiz-monad ここに置いた。
var_dump(fromMaybe('NONE', ret('a')));
// "a"
var_dump(fromMaybe('NONE', mempty()));
// "NONE"
$a = ret('a');
var_dump(mappend($a, []));
// ['a']
var_dump(mappend($a, ''));
// 'a'
var_dump(mappend($a, Nothing()));
// Just 'a'
var_dump(mappend($a, Just('b')));
// Just 'ab'
このように動く。
もう一度 PHPでFizzBuzz
これは実は完全ではなかった。
guard関数の中に直接 Just と Nothing を書いている。これではMaybe専用のguardだ。
今回でやっと正しいguardのfizzbuzzが書ける。
<?php
require 'vendor/autoload.php';
function calc(...$args){
return f(function($d, $s, $n){
return fconst($s, guard($n % $d === 0));
}, ...$args);
}
$fizzbuzz = fromMaybe()->ap(mappend(calc(3, "Fizz"), calc(5, "Buzz")));
function pr(...$args){
return f(function($a){
echo $a, "\n";
return $a;
}, ...$args);
}
$ret = fmap(pr()->compose($fizzbuzz), range(1, 100));
前回は MonadList::cast(range(1, 100)) とか書いてPHPのarrayをMonadListに変換してから実行していたが、今回はそれも要らない。