Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

発端

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のリスト内包表記はリストしか返せず、計算を打ち切った時は空リストしか受け取れない。
この問題は今後の課題としたいと思う。

_shimada
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした