PythonでScala-likeにコレクション操作を行う

# Background

Scalaのコレクション操作かっこいいですよね。余計な中間変数とか作らずにすっきり書けます。

```scala:Scala
val result = (0 to 10000)
      .filter(_ % 3 == 0)
      .map(_ + 1)
      .groupBy(_ % 10)
      .map { it =>
        val k = it._1
        val v = it._2.sum
        (k, v)
    }.toList
```

このコードは0から10000までの数字を3の倍数だけ残して1足して10で割ったあまりでグループ分けした時のそれぞれの数値の和です。この計算に特に意味は無いですが，データ処理フローをこのように思考の順番と全く同じで非常にわかりやすく(かっこ良く)書ける例です。

これをPythonでやろうとすると...

```py3:Python
import itertools
result = range(0, 10001)
result = filter(lambda x: x % 3 == 0, result)
result = map(lambda x: x + 1, result)
result = map(lambda x: (x % 10, x), result)
result = sorted(result)
result = itertools.groupby(result, lambda x: x[0])
result = map(lambda x: (x[0], sum(map(lambda _: _[1], x[1]))), result)
result = list(result)
```

見難くて目も当てられません。ちなみに中間変数を使用せずに一発で書くと


```py3:Python
result = list(
    map(lambda x: (x[0], sum(map(lambda _: _[1], x[1]))),
        itertools.groupby(
            sorted(
                map(lambda x: (x % 10, x),
                    map(lambda x: x + 1,
                        filter(lambda x: x % 3 == 0,
                               range(0, 100001)
                        )
                    )
                ), lambda x: x[0]
            )
        )               
    )
)
```

可読性0のコードの出来上がり。これがすんなり読めるようになると何かに目覚めるかも知れません。関数の宿命というか，どうしてもf -> g -> hの順で処理しようと思うとh(g(f(x)))のように逆の順番に書かないといけないのが原因です。

実はこれを解決するライブラリがあるんです。そう`toolz`と`scalafunctional`と`fn.py`ならね。この記事では**Scalaで書け**など意見はNGワードです。

# Toolz, CyToolz

`toolz`はPythonのbuilt-inの`itertools`や`functools`を拡張してよりfunctionalに書けるようにするためのライブラリです。`cytoolz`はそれをCythonで作りなおしてより高速化したもです。これらに実装されている`pipe`とカリー化された関数群が非常に便利です。先ほどのひどいコードは次のように書けます。

```py
from cytoolz.curried import *
import operator as O
result = pipe(range(0, 10001),
    filter(lambda x: x % 3 == 0),
    map(lambda x: x + 1),
    reduceby(lambda x: x % 10, O.add),
    lambda d: d.items(),
    list
)
```

どうでしょう。1つ目の引数で処理したいデータを与え，2つ目以降の引数で適用したい関数を次々に与えます。R言語に詳しい方でしたらの`dplyr`を連想するかもしれません。ちなみにここで使用している`filter`,`map`,`reduceby`は全てカリー化されたもので，`map(f, data)`が`map(f)(data)`のようにかけるのでこのように`pipe`で繋げられます。カリー化されたものを使用しない場合は`pipe`を`thread_last`に置き換えると各関数の最後の引数に前の関数で処理したデータを次々に渡していきます。

# ScalaFunctional

`scalafunctional`はその名の通り，Scalaのコレクション-likeに操作できるようにするライブラリです。~~そこまでするならScalaをつ(ry~~このライブラリでは`seq`という専用のクラスに`list`や`dict`等を入れてdotの連鎖で処理します。

```py3
from functional import seq
result = seq(range(0, 10001)) \
    .filter(lambda x: x % 3 == 0) \
    .map(lambda x: x + 1) \
    .map(lambda x: (x % 10, x)) \
    .reduce_by_key(O.add) \
    .to_list()
```

これが最もScalaに近いですね。ただしPythonだと行末にバックスラッシュが必要になるので多少面倒です。あとはPythonの`lambda`式はScalaの関数ほど柔軟ではないので複雑な処理の場合は一度関数を`def`する必要が出てくるかもしれません。いずれにせよ，非常にシンプルで美しいですね。

# fn.py

`fn.py`もPythonの関数型プログラミングのためのライブラリです。最大の特徴はScalaのプレースホルダーのような書き方ができることです。

```py
from fn import _
result = map(_ + 1, range(10))
```

単純に`lambda`の代わりに使用できます。


```py
f = _ + 1
f(10)

>>>
11
```

`toolz`や`scalafunctional`との相性がいいです。


```py:toolz
result = pipe(range(10),
    map(_ + 1),
    list
)
```

```py:scalafunctional
result = seq(range(10)) \
    .map(_ + 1) \
    .to_list()
```

ちなみんIPythonなどでは`_`は最後の出力を表す予約後のようなのでそこで使用する際には別名で`import`する必要があります。

```py
from fn import _ as it
```

# まとめ

`toolz`た`scalafunctional`を使用すればPythonでも関数型プログラミングが捗ります。`scalafunctional`はScalaのコレクション操作と全く同じように書けます。一方の`toolz`の`pipe`を使用すればコレクション操作だけでなく，より汎用性のあるデータ処理フローを書くことができます。これらを`fn.py`を上手く組み合わせてFunctional Python Lifeを満喫してください[^pandas]。

今回使用したライブラリは全てGitHubで公開されています。もちろんPyPIにも登録されているので`pip`でインストールできます。

- Toolz : https://github.com/pytoolz/toolz
- CyToolz : https://github.com/pytoolz/cytoolz/
- ScalaFunctional : https://github.com/EntilZha/ScalaFunctional
- fn.py : https://github.com/kachayev/fn.py


[^pandas]: 定番のPandasでもDataFrameに対してある程度dot連鎖で処理を繋げられるようにもなっています。