発端
調べてみたらPythonでflatMap相当の計算はlist comprehensionをネストすると書いてあった。
— SHIMADA (@_shimada) August 20, 2019
工夫すればScalaのfor comprehensionと同じようにOptionの連鎖がスッキリ書けるかも知れない?https://t.co/k0royEM0qr
PythonでflatMapを使いたくなって調べてみたら、PythonにはなんとflatMapがなく、そういう時はネストしたリスト内包表記使うといろいろなところに書いてあった
「ネストしたリスト内包表記」の字面を見ているとScalaのfor内包表記にそっくりだ。
見た目が似ているということは、同じような使い方ができるのではないか?と思い試してみた。
Option Monad
Scalaのfor式で最も使われる率の高い(当社しらべ)Option Monadの合成をやってみる
例として以下のような処理を考えてみる
1. 入力された文字列を整数に変換する
2. 得られた数値からユーザーを検索する
3. ユーザーに紐づくアイテムを検索する
4. アイテムの名称を得る
あくまでも例なので「ユーザーとアイテムはJOINして一緒に検索しろよ」などの野暮なツッコミは遠慮してほしい。
1~4の処理を手続き的に並べると、間に失敗したときのチェックを挟まなくてはならないのでコードがごちゃごちゃしてしまう。
そういうとき、HaskellやScalaでは Maybe / Option Monadを使う。
Hakellの例
name = do
code <- readMaybe "234" :: Maybe Int
user <- findUser code
item <- findItem $ userId user
return $ itemName item
print name -- Just "keyboard"
Scalaの例
name = for {
code <- parseToInt("1234")
user <- findUser(code)
item <- findItem(user.id)
} yield item.name
println(name) // -> Some("keyboard")
parseToInt, findUser, findItemなどの各関数のどこかで失敗したときは、そこで処理が打ち切られて name には Nothing
/ None
が代入される。
Pythonでやってみる
これと同じことをPythonで書いてみる。
Maybe / Option がないので Just 1
の代わりに [1]
、 Nothing
の代わりに []
を使う。
まず必要な部品を作る。
(1) 文字列を数字に変換する関数
def parseInt(input: str) -> List[int]:
try:
return [int(input)]
except ValueError:
return []
(2) 会員番号からユーザーを検索する関数
def findUser(code: int) -> List[dict]:
users = {
2012: {"id": 1, "code": 2012, "name": "yuzu"},
3308: {"id": 2, "code": 3308, "name": "yui"}
}
user = users.get(code)
if user:
return [user]
else:
return []
(3) ユーザーIDからアイテムを検索する関数
def findItem(userId: int) -> List[dict]:
items = {
1: {"userId": 1, "name": "keyboard"}
}
item = items.get(userId)
if item:
return [item]
else:
return []
(4) 定義できた関数を組み合わせて実際に使ってみる
うまく検索できるとアイテム名がリストに入って返ってくる
keybord = [item["name"]
for code in parseInt("2012")
for user in findUser(code)
for item in findItem(user["id"])]
print(keybord) # => ['keyboard']
数値以外の文字列を渡したときは変換に失敗して []
が返る
what = [item["name"]
for code in parseInt("WXYZ") # <- 失敗する
for user in findUser(code)
for item in findItem(user["id"])]
print(what) # => []
アイテムを持っていないユーザーを指定したときは2番目の検索に失敗して []
が返る
guitor = [item["name"]
for code in parseInt("3308")
for user in findUser(code)
for item in findItem(user["id"])] # <- 失敗する
print(guitor) # => []
ものすごく簡単に、do構文やfor式とほぼ同じ形で書くことができた。
ぱっと見で違うのは Scala では末尾にくるyield式が先頭にくることくらいだろうか。
まとめ
失敗するかも知れない計算を表すOption Monadを要素数0ないし1のリストにエンコードすることで、Pythonのリスト内包表記を使って簡潔に合成することができた。
Option Monadもどきが書けるようになると、今度はEither Monadもどきが欲しくなるところだが、残念ながらPythonのリスト内包表記はリストしか返せず、計算を打ち切った時は空リストしか受け取れない。
この問題は今後の課題としたいと思う。