8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-14

はじめに

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

Maybeモナド

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

参考

実装

モナドクラス(仮)

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 モナドを使うと

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

ようだ。

8
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?