Help us understand the problem. What is going on with this article?

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away