静かなる名辞

pythonとプログラミングのこと


【python】hyを触ってみる

 なんか、たまにlisp使ってみたくなるんだよね。

 去年のこれくらいの時期にもcommon lispをやってみた記憶があるが、たぶん言語として悪くはないんだろうけど、ちゃんと書けるようになる前に飽きてしまった*1

 という訳で、今回はpython方言(lisp方言と言うべきか?)のhyを使う。これが何なのかというと、pythonにコンパイルされるlisp。・・・その発想がなかった訳じゃないけど、本当にやるとは思わなかった。

 たぶん前回common lispに飽きてしまったのは「lispしなきゃ」って気張りすぎたのが原因なので、今回はpythonだと思って気楽に書くことにする。その上でpythonで書くのと比較してメリットが見えれば今後何かに使うのもありだし、なければさっさとゴミ箱に放り込むつもりである。

 公式サイト
Welcome to Hy’s documentation! — hy 0.15.0 documentation

試す

 hyのチュートリアルを一通り読んで「雰囲気はわかったけど、実際のプログラムがこんなので書けるの?」と思った。そこで簡単なプログラムをまずpythonで書いた。

# coding: UTF-8

import numpy as np
from sklearn.datasets import load_iris
from sklearn.svm import SVC
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import precision_recall_fscore_support as prf

def main():
    iris = load_iris()
    svm = SVC(C=5, gamma=0.01)

    trues = []
    preds = []
    for train_index, test_index in StratifiedKFold().split(
            iris.data, iris.target):
        svm.fit(iris.data[train_index], iris.target[train_index])
        trues.append(iris.target[test_index])
        preds.append(svm.predict(iris.data[test_index]))

    scores = prf(np.hstack(trues), np.hstack(preds), average="macro")
    print("p:{0:.6f} r:{1:.6f} f1:{2:.6f}".format(
        scores[0], scores[1], scores[2]))

if __name__ == "__main__":
    main()

 sklearnのsvmでirisを分類する。たった26行の、機械学習初心者が書いたみたいなコードだ。ちなみに0.96くらいのF1値になった(irisってつくづくちょろい奴だな)。

 これを一行ずつhyに翻訳していく。まあこの程度の内容が書ければなんとか実用にはなるだろう、というつもりである。

 ちなみに、公式ドキュメント以外で以下のページを参考にさせて頂きました。

ゼロから始めるHy(hylang)
ChainerのMNISTの例をHyに翻訳してみる
Pythonで書かれたイカしたLisp: Hy - Qiita
PythonistaのためのLisp入門(Hyチュートリアル和訳) - Qiita

encoding指定

 マジックコメントはしょせんコメントなのでコメントとして書けば良いらしい(こう書いてるところがあった)。

;; -*- coding:utf-8; mode:hy -*-

 でもこれ、emacsに認識させるだけなのでは? sjisとかにしたらちゃんとhy側で認識するのかしら。わざわざ試す必要は感じないけど。

 あとhy-modeは入れた。

import

 こうなりました。

(import [numpy :as np]
        [sklearn.datasets [load_iris]]
        [sklearn.svm [SVC]]
        [sklearn.model_selection [StratifiedKFold :as SKF]]
        [sklearn.metrics [precision_recall_fscore_support :as prf]]) 

 階層構造をカッコで表現する、理にかなってるような、カッコいいような、キモいような、微妙な感じ。

main関数

 なんかそれ用のマクロ(defmain)もあるっぽいが、便利マクロに頼り切るのもどうかと思ってifで素直に書いてみる。とりあえずhogeるmain関数を作成。

(defn main []
  (print "hoge"))

(if (= __name__ "__main__")
  (main))

 そういや=の比較演算子なんだけど、eq、eq?、equal?、==の順に試してぜんぶダメで、最後に試した=が正解だった。勘弁してくれ。

 で、hy2pyでpythonコードに変換してみた結果は、

def main():
    return print('hoge')


main() if __name__ == '__main__' else None

 なにこれ。めっちゃウケる。「main() if __name__ == '__main__' else None」とか、まさか条件演算子にされるとは思わなかった(確かにその方がlispのifの機能を考えると自然だが)。この書き方にはけっこう「萌えた」ので、友達に渡したりするpythonコードで今後使っていきたい。

残り

 あとは淡々とググりながら(グダりながら)書いただけだった。

 ハマったポイント(最初わからなかったこと)としては、

  • クラスからオブジェクトのインスタンスを作るときは「オブジェクトのインスタンスを返す関数呼び出し」だと思ってやればできる
  • リストのスライスはgetで書く
  • シンボルに束縛していないインスタンスのメソッドは「(. instance method-name)」で呼ぶ(これで関数オブジェクトが返るので、もう一回外にカッコを付けると呼び出せる)

 あたり。後はなんか適当にドキュメント通りに書くとか、適当にpythonの記法で書いても(ドットとか)受け付けるので、そんなに苦労はしなかった。

完成したもの

 もしかしたら変なところがあるかもしれないが、とりあえず動きはした。

;; -*- coding:utf-8; mode:hy -*-

(import [numpy :as np]
        [sklearn.datasets [load_iris]]
        [sklearn.svm [SVC]]
        [sklearn.model_selection [StratifiedKFold :as SKF train_test_split]]
        [sklearn.metrics [precision_recall_fscore_support :as prf]])

(defn main []
  (setv iris (load_iris))
  (setv svm (SVC :C 5 :gamma 0.01))
  
  (setv trues [])
  (setv preds [])

  (for [[train_index test_index] ((. (SKF) split) iris.data iris.target)]
    (svm.fit (get iris.data train_index) (get iris.target train_index))
    (trues.append (get iris.target test_index))
    (preds.append (svm.predict (get iris.data test_index))))
  (setv scores (prf (np.hstack trues) (np.hstack preds) :average "macro"))
  (print ((. "p:{0:.6f} r:{1:.6f} f1:{2:.6f}" format)
                                              (get scores 0)
                                              (get scores 1)
                                              (get scores 2))))

(if (= __name__ "__main__")
  (main))

 何回か実行したところ、心なしかpython版よりF1値が高めになった気がする(冗談です)。

感想

 スライスをS式で書けないせいで、どうやっても冗長になるね、というのが率直な感想。まあ、こればっかりはしょうがないだろう。

 良かったところは・・・意外となんとかなったところかな。lisp特有の機能は特に使わなかったので、事実上pythonから構文をトランスレートしただけだが、とりあえず書けない構文はなかった。正直どこかで素直には書き写せない場所が出てきてもおかしくないと思ってたけど(numpy配列のファンシーインデックスとか)、とりあえず詰みはしなかった。でもnumpy配列の列抽出とか、できるのだろうか・・・。

 あと、「これはlispだから、インスタンスメソッドだろうがなんだろうが、どんな処理だってしょせんは関数オブジェクトの呼び出しなんだ」と思って書いたので、初心者でもそんなに戸惑わなかった。文法が単純なのは素晴らしい。二週間もやればだいたい書けるようになると思う。

 悪かったところは、残念ながらいろいろ思いつく。

  • 冗長(python比で)

 これはしょうがない。許す。

  • 支援ツール(flymakeやjedi相当のもの)がなさそう

 これもしょうがないんだけど・・・マイナー言語って不利だよね。

  • hyインタプリタの吐くエラーメッセージが黄色かった)

 いつも使ってる開発環境(emacsのシェル)では凄いどぎつい色で表示された。これはしょうがなくない。なんで黄色にしたんだよ。全然読めねえんだけど。

 使えなくはないんだけど、日常的にnumpyとかsklearn走らせるのに使うかと言われると・・・って感じだな。でもpython環境上にいる強みはそのへんが使えることだと思うので、けっきょく存在意義がよくわからなくなってくる。

 まあ、「lispで書いた方が楽」な処理*2をpythonで書きたくなったらその部分だけhyで書くとか、あるいはpython使いが趣味のlispの勉強に使うとか、ネタとして愛でるとか、そういう用途で使うものだな、これは。

 やるメリットがあるのかどうかは微妙な感じだけど、関数型というか宣言型言語は面白そうと思ってるので、なんか強みを活かせるような感じでちょくちょくいじっていきたい。prologとかもやってみたいし*3

*1:それからこのブログにも飽きて一年近く放置していたのを、最近になって性懲りもなく再開した訳だが・・・

*2:真っ先に思い浮かんだのはS式のパースだった。あと構文解析器とか言語処理系、探索とか自動証明みたいな辺りは歴史的に使われてきたので、書きやすいのだろうか?

*3:いずれ気が向いたらhyでprologインタプリタでも書いてみようかしら