Edited at

Pythonのリスト内包表記を悪用してHaskellのdo構文のように使う


発端

PythonでflatMapを使いたくなって調べてみたら、PythonにはなんとflatMapがなく、そういう時はネストしたリスト内包表記使うといろいろなところに書いてあった

https://hydrocul.github.io/wiki/programming_languages_diff/list/flat-map.html

「ネストしたリスト内包表記」の字面を見ていると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のリスト内包表記はリストしか返せず、計算を打ち切った時は空リストしか受け取れない。

この問題は今後の課題としたいと思う。