Edited at

PHPをHaskellっぽくする (続:FizzBuzz)

More than 3 years have passed since last update.


ハードル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に変換してから実行していたが、今回はそれも要らない。