PythonでMaybeモナドを書いてみる

## はじめに
こちらはモナド・圏論を理解するための勉強メモ。
モナドや圏論をプログラミングの側から勉強しようとすると、Haskellのコードがよくでてくる。
しかし自分は（いずれ触りたいけど）Haskellに慣れていないのでよくわからない。
なのでそれなりに慣れていてかつ動作確認しやすいPythonで実装しながら攻めてみる。

## Maybeモナド
+ 計算結果が不定の値を扱うための抽象概念
+ 確定している値(a)は Just a と表す
+ 値が存在しない場合は Nothing と表す
+ 計算過程で例外が発生しうる処理を、例外処理の考慮なしに実装したい場合に使える

## 参考
+ http://labs.timedia.co.jp/2011/05/monad-in-python.html
+ https://qiita.com/south37/items/06cfa95aa9c8f2ecb2e9

## 実装
### モナドクラス(仮)

```python
class Monad():

    # モナドにはいろいろ種類があるので、継承したクラスで実装する。
    def __init__(self, a):
        raise NotImplementedError
        
    def _bind(self, func):
        raise NotImplementedError

    # 「|」を用いて関数をパイプできるようにする。
    def __or__ (self, func):
        return self._bind(func)

    # この関数でmaybeクラスのインスタンスをつくる
    @staticmethod
    def call_maybe(a=None):
        if a is not None:
            return Just(a)        
        else:
            return Nothing()
```

### Maybeクラス
```python
class Maybe(Monad):
    # このクラスを継承したJust・Nothingクラスでそれぞれ実装する
    def __init__(self, a):
        raise NotImplementedError

    def _bind(self, func):
        try:
            # 保持している値がNoneでない場合は受け取った関数をバインドし、返却値にJustを適用する。
            # None あるいは関数の処理で例外が発生した場合にはNothingを適用する。
            return Just(func(self.value)) if self.value is not None else Nothing()
        except :
            return Nothing()
```

### Justクラス
```python
class Just(Maybe):
    def __init__(self, a=None):
        if a is not None:
            self.value = a
        else:
            raise ValueError
    def __repr__(self):
        return 'Just %r' % self.value
```

### Nothingクラス
```python
class Nothing(Maybe):
   def __init__(self, a=None):
       self.value = None
       
   def __repr__(self):
       return 'Nothing'
```
## 動かしてみる
```python
Monad.call_maybe(2)
```
> Just 2

```python
Monad.call_maybe()
```
> Nothing

### 割り算の例
0で割ると例外が発生する。
処理としては単に割り算したいだけなのに、いざ実装しようとすると割る数が0かどうかチェックする必要があり、煩わしい。
場合によっては数値かどうかも気にしなければならない。
もしくは例外をキャッチして別の処理を挟み込むか？
こっちは割り算したいだけなんじゃ！
#### 割り算
単に引数で割るだけ。0かどうかとか気にしない。

```python
def divide(divisor):
    return lambda x: x / divisor
```

#### 1 ÷ 2 = 0.5
```python
Monad.call_maybe(1) | divide(2)
```
> Just 0.5

#### (2 ÷ 2) ÷ 2 = 0.5
```python
Monad.call_maybe(2) | divide(2) \
                    | divide(2)
```
> Just 0.5

#### 0 ÷ 2 = 0
```
Monad.call_maybe(0) | divide(2)
```
> Just 0.0

#### 0割
```python
Monad.call_maybe(2) | divide(0)
```
> Nothing

#### 計算途中で0割
```python
Monad.call_maybe(2) | divide(0) \
                    | divide(2)
```
> Nothing

#### 計算最後で0割
```python
Monad.call_maybe(2) | divide(2) \
                    | divide(0)
```
> Nothing

#### 0割後0割
```python
Monad.call_maybe(2) | divide(0) \
                    | divide(0)
```
> Nothing

### たまに例外を投げる処理の例
予期せぬ例外が発生しなければ苦労しない。
ここでは、0から10までの整数をランダムに取得し、
0以上7以下であれば割り算を適用、それ以外は例外を投げる関数を定義し、
そいつにMaybeモナドを適用してみる。

```python
def raise_runtime_error():
    raise RuntimeError
```
```python
import random
def random_raise():
    u = random.randint(0, 10)
    if 0 <= u and u <= 7:
        print("/ %r" %u)
        return divide(u)
    else:
        print("%r 例外発生" %u)
        return lambda: raise_runtime_error()
```
3回適用して得られる結果をみてみる。

```python
def test():
    return Monad.call_maybe(100) | random_raise() \
                                 | random_raise() \
                                 | random_raise()
```

#### 正常なケース
```python
test()
```
> / 2
> / 7
> / 6
> Just 1.1904761904761905

#### 2回目に例外スロー
```python
test()
```
> / 3
> 9 例外発生
> / 4
> Nothing

#### 全部例外スロー
```python
test()
```
> 8 例外発生
> 10 例外発生
> 8 例外発生
> Nothing

#### 例外はスローしなかったが0割になった
```python
test()
```
> / 5
> / 0
> / 7
> Nothing

#### 0割りと例外スローの両方
```python
test()
```
> / 0
> / 7
> 10 例外発生
> Nothing

## おわりに
以上のようにMaybe モナドを使うと

+ 例外を畳み込める
+ 本来やりたい処理の実装に専念できる

ようだ。