はじめに
こちらはモナド・圏論を理解するための勉強メモ。
モナドや圏論をプログラミングの側から勉強しようとすると、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
実装
モナドクラス(仮)
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クラス
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クラス
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クラス
class Nothing(Maybe):
def __init__(self, a=None):
self.value = None
def __repr__(self):
return 'Nothing'
動かしてみる
Monad.call_maybe(2)
Just 2
Monad.call_maybe()
Nothing
割り算の例
0で割ると例外が発生する。
処理としては単に割り算したいだけなのに、いざ実装しようとすると割る数が0かどうかチェックする必要があり、煩わしい。
場合によっては数値かどうかも気にしなければならない。
もしくは例外をキャッチして別の処理を挟み込むか?
こっちは割り算したいだけなんじゃ!
割り算
単に引数で割るだけ。0かどうかとか気にしない。
def divide(divisor):
return lambda x: x / divisor
1 ÷ 2 = 0.5
Monad.call_maybe(1) | divide(2)
Just 0.5
(2 ÷ 2) ÷ 2 = 0.5
Monad.call_maybe(2) | divide(2) \
| divide(2)
Just 0.5
0 ÷ 2 = 0
Monad.call_maybe(0) | divide(2)
Just 0.0
0割
Monad.call_maybe(2) | divide(0)
Nothing
計算途中で0割
Monad.call_maybe(2) | divide(0) \
| divide(2)
Nothing
計算最後で0割
Monad.call_maybe(2) | divide(2) \
| divide(0)
Nothing
0割後0割
Monad.call_maybe(2) | divide(0) \
| divide(0)
Nothing
たまに例外を投げる処理の例
予期せぬ例外が発生しなければ苦労しない。
ここでは、0から10までの整数をランダムに取得し、
0以上7以下であれば割り算を適用、それ以外は例外を投げる関数を定義し、
そいつにMaybeモナドを適用してみる。
def raise_runtime_error():
raise RuntimeError
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回適用して得られる結果をみてみる。
def test():
return Monad.call_maybe(100) | random_raise() \
| random_raise() \
| random_raise()
正常なケース
test()
/ 2
/ 7
/ 6
Just 1.1904761904761905
2回目に例外スロー
test()
/ 3
9 例外発生
/ 4
Nothing
全部例外スロー
test()
8 例外発生
10 例外発生
8 例外発生
Nothing
例外はスローしなかったが0割になった
test()
/ 5
/ 0
/ 7
Nothing
0割りと例外スローの両方
test()
/ 0
/ 7
10 例外発生
Nothing
おわりに
以上のようにMaybe モナドを使うと
- 例外を畳み込める
- 本来やりたい処理の実装に専念できる
ようだ。