Python で関数型プログラミング(っぽいもの)に入門する


はじめに

最近、 exercism.io を始めたのですが、Pythonでちょっと面白い問題に遭遇しました。Pythonで関数型プログラミングをやってみろという課題です。この課題を通じて関数型プログラミングに入門するための基本的な考え方を解説を試みます。なお、筆者は、Pythonも関数型プログラミングもずぶの素人です。

なお、ここでは、Python 3.7.0 を動作確認に使ってます。

課題

(全部、そのまま掲載するのはどうかと思うので、代表して1つだけ記載します。)

Python の既存の関数を使わずにリストの基本操作の1つ append関数を実装してください。

append([], []) # => []
append([], [1, 2, 3, 4]) # => [1, 2, 3, 4]
append([1, 2], [2, 3, 4, 5]) # => [1, 2, 2, 3, 4, 5]

関数型プログラミング(っぽいもの)の基本的な考え方

関数型プログラミングでは、リスト(配列)を扱うのは、基本中の基本です。リストの先頭の値を求める、リストの先頭を除いた残りを求める、リストの先頭に1つ値を追加して新たなリストを作る、などです。こういった基本的な操作を組み合わせて、リストに対する関数を実装していくことになります。
大体、関数型プログラミングを勉強すると最初の方で、基本的な操作が登場してきます。(Scheme だと car, cdr, cons などですね。)

Pythonでリストの先頭を取り出す

Pythonのリスト list の先頭を取り出すやり方はいろいろあるでしょうが、ここでは、以下のやり方を使います。

head, *tail = list

ここで、head には、listの先頭が入ります。

>>> list = [1, 2, 3]
>>> head, *tail = list
>>> head
1
>>>

list[0] とやってもいいのでしょうが、関数型プログラミング言語 Elixir を少しだけ触ったことがある筆者はこちらの方が好みです。

ちなみに、Elixir だとこんな感じです。

[head|tail] = list

あと、Pythonでは、リストが空のときは、ValueErrorになるので注意しましょう。

>>> list = []
>>> head, *tail = list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected at least 1, got 0)

Python でリストの先頭を除いた残りを求める

もう既に登場してますね。

head, *tail = list

tail が先頭を除いた残りになります。

>>> list = [1, 2, 3]
>>> head, *tail = list
>>> tail
[2, 3]

Python でリストの先頭に1つ値を追加して新たなリストを作る

head が1つの値、tailがリストだとすると、tail に head を先頭につけたリストは以下のようにするとできます。

list = [head, *tail]

実際の例はこんな感じです。

>>> head = 1
>>> tail = [2, 3]
>>> list = [head, *tail]
>>> list
[1, 2, 3]

Python でリストの末尾に1つ値を追加して新たなリストを作る

Python では、同じ要領で末尾にひとつ要素を追加して新たなリストを作ることもできます。headがリストで、tailが1つの要素だとすると

list = [*head, tail]

とできます。実際の例はこんな感じ。

>>> head = [1, 2]
>>> tail = 3
>>> list = [*head, tail]
>>> list
[1, 2, 3]

append の実装

これまでの操作を組み合わせるとこんな感じで append を実装することができます。

def append(xs, ys):
    while ys != []:
        head, *ys = ys
        xs = [*xs, head]
    return xs

以下のように再帰呼出しにする方が関数型プログラミングっぽいのですが、リストの要素の数が多くなると、RecursionError になるので、避けた方が良いでしょう。

def append(xs, ys):
    if ys == []:
        return xs
    head, *ys = ys
    return append([*xs, head], ys)

まとめ

Pythonで関数型プログラミング(っぽいもの)に入門してみました。同様にして、map関数、filter関数、foldl関数、foldr関数なども実装することができます。
なお、私が実装したコードに対して、mentor さんからのコメントが、まだついてません。ですので、これが正解かどうかはわかりません

おまけ

試してないですが、同じようなことは、Ruby でもできるはずです(社名が社名なので、やっぱりRubyを無理矢理にでも絡めておかないと )

$ irb
irb(main):001:0> list = [1,2,3]
=> [1, 2, 3]
irb(main):002:0> head, *tail = list
=> [1, 2, 3]
irb(main):003:0> head
=> 1
irb(main):004:0> tail
=> [2, 3]
irb(main):005:0> [head, *tail]
=> [1, 2, 3]
irb(main):006:0> [*tail, head]
=> [2, 3, 1]
irb(main):007:0>

追記 (2018-09-13)

その後、メンターさんから well done とコメントもらいました。ただ、アドバイスとして、

while ys != []:

while ys:

と書いた方が速いと言われました。 [] が false になるんですね。

>>> a = []
>>> bool(a)
False
>>> a = [1]
>>> bool(a)
True

Ruby は(確か Elixir も) nil と false だけ false なので、その癖が出てしまいました