2015年12月5日土曜日

Pythonへのバグの混入を防ぎ、可読性も向上させるAPPLe

Pythonへのバグの混入を防ぎ、可読性も向上させるAPPLe

はじめに

Pythonはさくっと書けてさくっと実行できて、しかも他のスクリプト言語と比べてチーム開発にもある程度耐えられるきちっとした構造ですばらしいですよね!
今日はそんなPythonプログラミングをもっと楽しいものにする珠玉のツールAPPLeを紹介します。
APPLeはPythonに拡張機能1を提供する処理系で、今までと比べものにならないくらい可読性が高く、さらに厄介なバグの多くを排除したコードを書くことを助けてくれます。

本記事は、APPLeの紹介として、なぜAPPLeを使うと嬉しいかに終始しており、具体的なAPPLeの機能にはあまり触れません。

実際にAPPLeを使う方法や、より詳しく学ぶ方法については最後にご紹介しているので、本記事を最初から通して読んでみて「いいな」と思ったら実際にAPPLeを導入してみてくだい。
私が保守運用している会社でも内部的にAPPLeを本番投入しているので、導入を検討の企業さまはお声掛けいただけるとなにかいいことがあるかもしれないです。

※ これはHaskell Advent Calendarの記事です。

環境

本記事はUbuntu 14.04において、Python 2.7.6での動作結果をもとに記述しています。

APPLeの使いドコロ

Noneなんてナンセンス

Pythonプログラミングをしていると必ず出くわすエラー。

>>> "Hello " + foo()
TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'

関数を呼び出したら結果がNoneになっていたため、ランタイムエラーになってしまっています。
そもそも

  • 確実に返値がNoneにならない関数
  • もしかしたら返値がNoneになってしまうかもしれない関数

を明確に見分ける方法がPythonに提供されていないことに問題があります。

このようなわかりやすい例だとテストでバグが簡単に見つかるのですが、条件分岐の隅っこの方にこんなバグが隠れているのに気づかずに本番環境で用いてしまったら。。。
恐ろしいものです。

APPLeはこの問題を解決します。

$ apple foo.py

foo.py:1:8:
    Couldn't match expected type `[Char]'
                with actual type `Maybe [Char]'

appleコマンドはfoo.pyを静的解析し、ランタイムエラーをもたらす原因となるバグがない場合に限り、実際にそのコードを実行します。

この例では、foo.pyNoneに起因する問題があったため、実行前にエラーが表示されました。
foo.pyスクリプトの1行目にNoneになるかもしれない値を返す関数があるのに、その返値をNoneにならない値としてあつかってしまったことが原因です。

また、APPLeにはfmapおよびmaybeという構文があり、このNoneかもしれない値のあつかいを楽にしてくれます。

fmap

fmapNoneをそのまま他の処理に渡したいときに便利です。

(fmap f nullable)

と記述することで、

  • nullableNoneのときにはNone
  • nullableがその他の値の時にはその値にfを適用した返値

を返します。

fmapには、さらに便利な代替記法として>>==<<があります。

カッコが多くてまるでLISPのようになるこんな式も

ans = (fmap f4 (fmap f3 (fmap f2 (fmap f1 x))))

もっと単純に

ans = f4 =<< f3 =<< f2 =<< f1 x
ans = f1 x >>= f2 >>= f3 >>= f4

と書くことができます。
まじいけてる。

maybe

maybeNoneの場合のデフォルト値を設定できます。

(maybe "who r u?" f nullable)

と記述することで、

  • nullableNoneのときにはデフォルト値の"who r u?"
  • nullableがその他の値の時にはその値にfを適用した返値

を返します。

Goodbye for文 forever!

最近の多くのPythonistaは、for文なんて使いたがりませんよね?

for文はなぜよくないか

蛇足ですが、for文のイケてなさの一端について述べておきます。
for文の問題点は、その可読性の低さにあります。
次のfor文を使った3つのコードから、for文がいかに可読性を損なうかをおさらいしましょう。

map.py
xs = []
for n in ls:
  xs.append(n+1)
print xs
filter.py
xs = []
for n in ls:
  if n % 2 == 0:
    xs.append(n)
print xs
reduce.py
sum = 0
for n in ls:
  sum += n
print sum

これらはそれぞれ、異なる意味を持っています。

  • map.py: リストのそれぞれの要素に対して同じ操作を行う
  • filter.py: リストの中から条件を満たす要素のみを抜き出す
  • reduce.py: リストの左側の要素から順番に、これまでの要素の計算結果を用いた処理を行う

ほとんどのfor文はこのいずれかの処理を行うために使われていますが、コードが複雑になるとどういう意図なのか判然としなくなってしまう上に、余計な変数への再代入が何度も行われるため、バグを生む温床になってしまいます。
そのため、すてきなPythonistaのみなさんなら、mapfilterreduceの関数を使った楽しい日々を送っていることでしょう。

詳細は省きますが、APPLeの標準モジュールには、for文を駆逐すべくfoldrscanlscanriterateなどの便利な関数が用意されており、快適なPythonライフを過ごすことを可能にします。

処理効率の向上

関数を提供するだけなら、別にAPPLeを使わずとも、イケてるライブラリをimportすれば良いだけです。
APPLeを使うと、可読性を保ちながら処理効率を向上させることができます。

まずはfor文を使わないと処理効率が問題になる場合について確認しましょう。

filter(lambda x: x % 2 == 0, map(lambda x: x + 1, ls))

len(ls) == 100として、この式について考えてみます。

  • リストの各要素に1を足す (100回の処理)
  • その中から偶数のみを抜き出す (100回の処理)

合計、200回のリスト要素への処理が発生しています。

もちろん、本来のプログラムの意図がわからなくなってしまっても良いのなら、次のように変換することもできます。

map(lambda x: x + 1, filter(lambda x: x % 2 == 1, ls))
  • リストの中から奇数のみを抜き出す (100回の処理)
  • 抜き出されたのリストの各要素に1を足す (50回の処理)

こうすることで、リスト要素への処理は150回に減り、実際に計算時間も減りますが、必ずしも常にこのような変換ができるとは限りませんし、可読性の低下にもつながります。

APPLeを使えば、そんな心配は無用です。
詳細はここでは省きますが、Lazy Evaluationという仕組みにより、最初の形式で記述しても、リスト要素への処理はfor文を使うのと同等の150回で済みます。

もっと型をつかったらいいじゃん

まずは、この関数定義を見てください。

def sortBy(arg1, arg2) :
    """
    HOFなソート関数.
    @param  arg1 比較可能な2つの値を引数として受け取り、GT, EQ, LTのいずれかを返す関数
    @param  arg2 arg1の関数の引数になりうる型の値のリスト
    @return arg2をarg1の比較方法を用いて昇順に並べ替えた結果のリスト
    """
    ...

高階関数とかを使おうとすると、型情報を日本語(またはその他の自然言語)で説明するのが億劫で仕方ないですね。。。

sortBy :: (a -> a -> Ordering) -> [a] -> [a]

そこで関数定義の前に、こういった補助情報を記述してみることにしましょう。

a -> b -> c -> dは、abc の型の値をそれぞれ引数にとって、dの型を返値として返すことを意味することにします。
つまり、sortByの記述では、

  • 引数1: 以下の型の関数
    • 引数1: 任意の型a
    • 引数2: 引数1と同じ型a
    • 返値: 順序を示す型
  • 引数2: 引数1の関数の引数の型と同じ型aのリスト
  • 返値: 同じく型aのリスト

を意味しています。
日本語で書くよりも、事前にみんなでルールを決めて、形式的に記述した方がわかりやすいですよね?

APPLeは、プログラム中にこのような関数の型に関する記述を見つけると、静的解析時に、その型に違反している箇所を教えてくれます。
例えば、このsortBy関数にString -> Int -> Orderingという型を持つ関数を最初の引数に与えると、

Couldn't match type `Int' with `[Char]'
Expected type: String -> String -> Ordering
  Actual type: String -> Int -> Ordering

このようなエラーを吐き出して、ランタイムエラーを未然に防いでくれます。
もちろん、こういった型情報は「この関数は不安だな」とか「日本語で説明するのめんどくさいな」みたいな場所にだけ書いて、他の関数には型情報を付加しなくても、APPLeがよろしくやって、うまくエラーを見つけてくれます。

mypyを使っても同様のType Hintは実現可能ですが、APPLeにはAlgebraic Data Typeを定義する仕組みもあり、より厳密なテストが可能です。

APPLeをもっと知るには

Ubuntu 14.04 の場合、下記のコマンドで簡単にインストールできます。

sudo apt-get install -y ghc
sudo ln -s `which runghc` /usr/bin/apple

残念ながら、APPLeにはPythonに対して下位互換性がなく、Haskellという独自の言語仕様に従っています。
シンボリックリンクを貼るのが面倒な方には、直接Haskellコンパイラを使うのがオススメです。

つまりなにが言いたいかというと、「PythonもいいけどHaskellもいいでしょ?」ということです。
より詳細に勉強するにはすごいH本を買いましょう。

古い情報を見てhaskell-platformとかcabalとかをいきなりインストールしてしまわないように、@nobsunさんの記事などを参照するといいと思います。

おわりに

APPLeはその本体を提供する公式サイトにある"An advanced purely-functional programming languag e"が命名の由来です。

Haskellはめっちゃいい言語なのに、なかなか人を寄せ付けない雰囲気を醸し出しているので、Pythonみたいな感覚でさくっと使ってみてほしいなと思ってこういった形で紹介しました。
以前、同じような動機で「Haskellは命令形の気持ちで書けるよ」という入門記事のようなものを書いたことがありますが、やっぱりPythonとかと比べて簡単に入門できる環境が整っていないのも事実です。

あとHaskell Advent CalendarでPythonの話をしてごめんなさい。
「内容がないよー」な記事ではないので許してください。

どうしてもAPPLeを使った仕事がしたくて、あなたが十分に優秀であるのであれば、弊社に連絡してください。

  1. 厳密には拡張機能という表現は正しくありませんが、簡単のためこう表現することにします。 

この投稿は Haskell Advent Calendar 2015 の 4日目の記事です。

0 コメント:

コメントを投稿