FP:モナドで現実データ世界を安全に過ごそう
このワークシートはMath by Codeの一部です。
モナドについては、Pythonの関数型プログラミングの解説本でもスルーしているものが多くあります。
実用性はあまりなく、学習のための実装とかいているという人もいました。
もともとはHaskellのようなレジェンド関数型言語の専売特許だったもので、Pythonの標準モジュールにも入ってないし、Pythonのドキュメントにもありません。
しかし、Monadの手法はRust,Reat,Typescriptなどモダンな言語の関数型パラダイムとして
取り入れられてきています。
なぜか?
それは記述が用意で、役立つからです。
扱うデータが巨大になり、欠損値が紛れ込んだりしても止まらない
クリーンなコード、スケーラブルなコード作成というのが
Web,AIの開発がさかんになったことで、日常化してきたからです。
理想と現実の境界線でいえば、
「現実の中の多様な処理の中にも安全地帯を作らないといけなくなってきた」のです。
モナドにかぎらず、
型を明示する言語やメモリ安全、
メモリ効率の追求というものの重要性が
高まっていいるのです。
ということで、今回は、Pythonistでもなじみのうすい
「モナド」について、未来のために学びましょう。
1.エラーで止まらずエラーキャッチ文の嵐にならないために
モナドとはそもそも何かではなく、
どんな場面でどんなツール・発想が未来を明るくしてくれるのか?
そんなユースケース中心で見ていこう。
<Maybeモナド>
jupyternotebookの場合、
pip install pymonadでインストールします。
Maybeモナド、ワンちゃん失敗ありモナドです。
これは、ちょうど値を返せるかダメかもというときに
値を返せるのを子クラスJustでくるみ、ダメ、NoneならNothingクラスを返すという、
失敗を許容するクラスです。
データまんじゅう、辞書DMから項目Aを抜き出したいとします。
項目Aに情報DM["A"]があれば、それをJustでくるみ、なければNothingを返します。
これを使うにはJust,Nothingという、maybeクラスの子クラスを入れましょう。
from pymonad.maybe import Just, Nothing
def extract_A(DM):
return Just(DM["A"]) if "A" in DM else Nothing
この関数がモナドです。Justをデータをカプセルにつつみ、失敗してもNothingで次に送れます。
項目Aの次に項目B、項目Cでも同じような関数extract_B、extract_Cを作り
モナドのバケツリレーをします。
def pipeLine(DM):
return Just(DM).then(extract_A).then(extract_B).then(extract_C)
pipeLine(OKdata)#A,B,Cと成功の連鎖なら連続チェックを通ったデータが返ります。
pipeLine(NGdata)#A,B,Cのどこかでこけてもエラーが出ずに、最後にNothingが返ります。
初めから失敗の可能性の入った型で値をカプセルに入れてやり取りすることにより、
バケツリレーの最後に、パターンマッチでどう処理するかをかくHaskellゆずりの書き方ができますね。
エラーがあったらどうしようとか、
エラーが起きてからtrycatch構文にくるむのと
発想が真逆ですね。
次はその利用例です。
from pymonad.maybe import Just, Nothing
# 各ステップ:結果をJustかNothingのカプセルに入れて返す
def extract_profile(d):
return Just(d["profile"]) if "profile" in d else Nothing
def extract_address(p):
return Just(p["address"]) if "address" in p and p["address"] is not None else Nothing
def extract_zip(a):
return Just(a[:3]) if len(a) >= 3 else Nothing
def get_zip_code_monad(raw_data):
return (
Just(raw_data) # Justでくるむ
.then(extract_profile) # プロフィール抽出
.then(extract_address) # 住所抽出
.then(extract_zip) # 郵便番号抽出
)
# OKデータ
user1 = {"profile": {"address": "105-0011 東京都港区芝公園"},"Name":"東京タワ"}
user2 = {"profile": {"address": "106-0032 東京都港区六本木"},"Name":"ぽん木"}
# NGデータ
user3 = {"profile": {"address": None,"Name":"住所不定無職"}}
user4 = {"profile": {"address": "52"},"Name":"京都"}
user_data=[user1,user2,user3,user4]
for user in user_data:
print(get_zip_code_monad(user))
[OUT]
Just 105
Just 106
Nothing
Nothing
欠損値があろうがなかろうが、データが大量になっても涼しい顔して、答えてくれる
それがMaybeモナドのバケツリレーですね。
<Eitherモナド>
Maybeモナドはある意味、強引に安全に動く仕組みでした。
でも、ただ機械がとまらなければよいのではなく、
そうして、ダメなのか、理由も知りたいときがあります。
全か無か、OKかNothingか
ではなく、
右か左か、正しいか変か、RightかLeftかです。
左利きの人は気にしないでください。左翼の人も気にしないでください。
これは右が正しいという意味もあることからくる言語的な規約です。
Rightなら通し、Leftならコメントをつけらえるのが、Eitherモナドです。
from pymonad.either import Left, Right
a = Right(2)
b = Left('文字2')
print(a.either(lambda x: f'ダメ, {x}', lambda x: x)) # OK
print(b.either(lambda x: f'ダメ, {x}', lambda x: x)) # NG
[OUT]
2
ダメ, 文字2
さっきのモナドをMaybeからEitherに直してまたバケツリレーしてみよう。
from pymonad.either import Left, Right
# 各ステップ:成功時はRight(値)、失敗時はLeft(エラーメッセージ)を返す
def extract_profile(d):
return Right(d["profile"]) if "profile" in d else Left("Profile がありません")
def extract_address(p):
return Right(p["address"]) if "address" in p and p["address"] is not None else Left("Address がありません")
def extract_zip(a):
return Right(a[:3]) if len(a) >= 3 else Left("住所の先頭の郵便番号の長さが3未満です")
def get_zip_code_monad(raw_data):
return (
Right(raw_data) # 最初はRightでくるむ
.then(extract_profile) # プロフィール抽出
.then(extract_address) # 住所抽出
.then(extract_zip) # 郵便番号抽出
)
# テストデータ
user1 = {"profile": {"address": "105-0011 東京都港区芝公園"}, "Name": "東京タワ"}
user2 = {"profile": {"address": "106-0032 東京都港区六本木"}, "Name": "ぽん木"}
user3 = {"profile": {"address": None}, "Name": "住所不定無職"}
user4 = {"profile": {"address": "52"}, "Name": "京都"}
user_data = [user1, user2, user3, user4]
for user in user_data:
print(f"Name: {user.get('Name')} -> {get_zip_code_monad(user)}")
[OUT]
Name: 東京タワ -> Right 105
Name: ぽん木 -> Right 106
Name: 住所不定無職 -> Left Address がありません
Name: 京都 -> Left 住所の先頭の郵便番号の長さが3未満です
これで、「Nothing」という不愛想な報告が詳細になりました。
しかも、NGデータで途中で止まることもありません。すばらしいです。
10億件のデータでいちいちコメントがでたらたまらないですね。
MaybeがいいかEitherがいいかは目的しだいです。
<振り返り>
エラーを外からナントカするのは古典的なアプローチでした。
しかし、モナドは、エラーさんいらっしゃいと言わんばかりに
現実の欠損値だろうが何だろうが、取り込みつつエラーにならないラベルつきのデータ、
カプセル化したクラスです。
逆転の発想ですね。
汚い、不完全な現実から目をそむけず、その存在を中に取り込むことにしたのですね。
この現実主義でありながら、妥協ではない、純粋関数をリレーするという理想を捨ててはいない。
モナドは、すごい発想です。
プログラミングをただの写経やチュートリアルのまねで終わらわせずに、
主体的な開発者の視点に立つと、
AIとも他の同僚とも、問題領域の関係者、ステークホルダーとも会話できるようになると思います。
さて、Geogebraは辞書構造も再帰構造も一般関数ありませんから、モナド構造もありません。
しかし、リスト、文字列という数値以外のデータ構造があることに着目すると、
課題:geogebraでモナド的なしかけを作ってみよう。
タイトルは「monadは止まらない」
a=slider(-10,10,1)
JorN={"Just " +a ,"Nothing" }
Monad=if(a>0, JorN(1),JorN(2)}
モナドはaが負の数になっても停止せずに表示を続けらえれます。