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.py
にNone
に起因する問題があったため、実行前にエラーが表示されました。foo.py
スクリプトの1行目にNone
になるかもしれない値を返す関数があるのに、その返値をNone
にならない値としてあつかってしまったことが原因です。
また、APPLe
にはfmap
およびmaybe
という構文があり、このNone
かもしれない値のあつかいを楽にしてくれます。
fmap
fmap
はNone
をそのまま他の処理に渡したいときに便利です。
(fmap f nullable)
と記述することで、
nullable
がNone
のときには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
maybe
はNone
の場合のデフォルト値を設定できます。
(maybe "who r u?" f nullable)
と記述することで、
nullable
がNone
のときにはデフォルト値の"who r u?"
nullable
がその他の値の時にはその値にf
を適用した返値
を返します。
Goodbye for文 forever!
最近の多くのPythonistaは、for
文なんて使いたがりませんよね?
for
文はなぜよくないか
蛇足ですが、for
文のイケてなさの一端について述べておきます。for
文の問題点は、その可読性の低さにあります。
次のfor
文を使った3つのコードから、for
文がいかに可読性を損なうかをおさらいしましょう。
xs = []
for n in ls:
xs.append(n+1)
print xs
xs = []
for n in ls:
if n % 2 == 0:
xs.append(n)
print xs
sum = 0
for n in ls:
sum += n
print sum
これらはそれぞれ、異なる意味を持っています。
map.py
: リストのそれぞれの要素に対して同じ操作を行うfilter.py
: リストの中から条件を満たす要素のみを抜き出すreduce.py
: リストの左側の要素から順番に、これまでの要素の計算結果を用いた処理を行う
ほとんどのfor
文はこのいずれかの処理を行うために使われていますが、コードが複雑になるとどういう意図なのか判然としなくなってしまう上に、余計な変数への再代入が何度も行われるため、バグを生む温床になってしまいます。
そのため、すてきなPythonistaのみなさんなら、map
, filter
, reduce
の関数を使った楽しい日々を送っていることでしょう。
詳細は省きますが、APPLe
の標準モジュールには、for
文を駆逐すべくfoldr
, scanl
, scanr
, iterate
などの便利な関数が用意されており、快適な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
は、a
, b
, c
の型の値をそれぞれ引数にとって、d
の型を返値として返すことを意味することにします。
つまり、sortBy
の記述では、
- 引数1: 以下の型の関数
- 引数1: 任意の型
a
- 引数2: 引数1と同じ型
a
- 返値: 順序を示す型
- 引数1: 任意の型
- 引数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
を使った仕事がしたくて、あなたが十分に優秀であるのであれば、弊社に連絡してください。
厳密には拡張機能という表現は正しくありませんが、簡単のためこう表現することにします。 ↩
- 3日目の記事: type roleについて学ぶ
0 件のコメント:
コメントを投稿