Posted at
iRidgeDay 7

Pythonで書かれたイカしたLisp: Hy

More than 1 year has passed since last update.

これはiRidge Advent Calendarの七日目の記事です。

この記事では、Pythonで実装されたLisp方言のHyを紹介します。


動機

株式会社アイリッジのサーバサイドエンジニア、tanaka.lispです。ウェブとPythonの世界を見てみたいと思い、Pythonの会社であるアイリッジでお仕事しています。

突然ですが、ぼくがこよなく愛する言語はCommon Lispです。業務ではもっぱらPythonを読んだり書いたりするわけですが、PythonはLispではないので、自動インデントに翻弄されたり、if pred1 and pred2: ...if (and pred1 pred2): ...なんて書いてしまったりと苦労が絶えません。Lisper特有の禁断症状が顔を出すこともあります。S式のことを考え始めたり、おもむろに"Hyperspec 関数名"でググったり、EmacsにS式を打ちこみ始めたり。果ては業務時間中に頭をよぎるPythonがLispになればいいのにという意味不明な考え──

ああ、PythonがS式だったら。どこぞの言語のように、Pythonの膨大な標準ライブラリ群にタダ乗りしつつS式を書けたなら。ああ、ああ。

しかし、あるのです。そんな夢のような、イカした言語が。


Hy

HyはPythonで書かれたLisp方言です。

Clojureの影響の強い構文を持ち、Hy Style Guideにも、迷ったらPython > Clojure > Common Lispの順でその言語の慣習に従え、とあるくらいです。

さらにはClojureよろしくPythonとのinteropが可能で、なんとDjangoアプリをHyで書けるようです。ああ、もうこれで、Hyでよくない??

ちなみにイカした言語といえばHaskellが有名でゲソが、Hyのマスコットもイカのようです。イカしてますね。ドキュメントの三分チュートリアルは最高に面白いので一読することをオススメします。ぼくはこれでノックアウトされました。


Hyのインストール

それではインストールしていきましょう。ここではvirtualenv環境にインストールします1

まずはvenv環境を作って、

$ virtualenv venv

# ...たくさんの出力
$ cd venv
$ source ./venv/bin/activate

次にpipでgithubからインストールします。

(venv) $ pip install git+https://github.com/hylang/hy.git

# ...たくさんの出力

あとはREPLを起動してこんにちは。

(venv) $ hy

hy 0.11.0+320.g5b87932 using CPython(default) 2.7.12 on Linux
=> (print "Hy!")
Hy!
=>

ね、簡単でしょう?


Hyひとめぐり

では、どんな言語なのか見ていきます。


基本構文


データ構造

;;; 数値と真偽値

=> 42
42L
=> False
False
;;; 文字列
=> "forty two"
u'forty two'

まず、データ構造はPythonで使えるものがほぼそのまま使えます。が、,とか:とかデリミタを書くのは禁止らしいです。

;;; リスト. `,`を書いてはダメ

=> ["life" "universe" "everything"]
[u'life', u'universe', u'everything']
;;; 辞書. これもデリミタ不要
=> {"arthor" "dent" "ford" "prefect"}
{u'arthor': u'dent', u'ford': u'prefect'}

そしてこちらはLisp特有のデータ型たち。シンボルの表示がUnicode文字列なのはなんとなく違和感ありますね。

;;; シンボル. 表示は文字列だけど、型はちゃんとシンボル

=> 'marvin
u'marvin'
=> (type 'marvin)
<class 'hy.models.symbol.HySymbol'>
;; キーワード
=> :deep-thought
u'\ufdd0:deep-thought'
;; 他と常に非等値になるシンボル
=> (gensym)
u':G_1236'

そしてなんと、nilがない! ああ、神よ、これではどうやって空リストを書けばいいのですか。

;;; nilがない!

=> nil
Traceback (most recent call last):
File "<input>", line 1, in <module>
NameError: name 'nil' is not defined

神「()を使えばいいじゃない!」


ぼく「なるほど!」

=> ()

[]

というか、Lispの伝統的なリスト記法でもPythonのリストになる場合があるようです。

=> '(a b c)

(u'a' u'b' u'c')
=> (cons 'googolplex (cons 'star (cons 'thinker ())))
[u'googolplex', u'star', u'thinker']

そしてコンス。Lispの父John McCarthyはコーンスと発音したらしいですね。

=> (cons 'arther 'dent)

(u'arther' . u'dent')
;;; ドット対で書いてquoteしてもOK
=> '(arther . dent)
(u'arther' . u'dent')


関数

HyはLisp語族の言語なので、関数呼び出しはポーランド記法で書きます。

;;; 文字列連結

=> (+ "forty" " " "two")
u'forty two'
;;; 四則演算
=> (+ 1 (* 5 8) 1)
42L

関数定義はClojureふうにこんな感じで。ついついdefunと入力して戸惑うあたりもいつもの感じです。

=> (defn factorial [n]

... (if (<= n 0)
... 1
...       (* n (factorial (- n 1)))))
=> (factorial 10)
3628800L

ところで、リストはコンスの糖衣構文です。Lispのコードはリストで書かれます。ということはコードをコンスで書くことだってできるはずです。Common Lispではドット対でプログラムを書いてもちゃんとreadされますが果たして…。

=> (print . ("forty" . ("two" . ())))

forty two

おお! やりますね! これで難読化への道が開けました。


Pythonの関数にアクセス

Pythonの豊富な標準ライブラリを利用する例として、httplibを用いた通信の例をHyでやってみます。

;;; インポート. fromインポートもできます

=> (import httplib)

=> (setv conn (httplib.HTTPSConnection "www.python.org"))
=> (conn.request "GET" "/")
=> (setv res (conn.getresponse))
=> (print res.status res.reason)
200 OK

S式に読み替えるだけで、できてしまいました。Pythonで書いてたスクリプトをHyに移行するチャンスです。


マクロ

さて、HyがただPythonに親しいだけではなく、ちゃんとLispの仲間であることも確認します。Lisperではない諸氏のために説明すると、ここでいうマクロとはCのマクロではなく、抽象構文木を操作するアレのことです。RustとかNimとか、Scalaなんかがこの機能を持っています。ちなみに、Hyはcontributor moduleとしてアナフォリックマクロを既に持っています

例として、とりあえずアナフォリックマクロなんて恩恵がわかりやすくて適当かなと思いました。アナフォリックマクロとは、たとえばif文の条件部分の式の値を後ろから参照できるようにするものですが、詳しくはOn Lispの該当の章でも読んでください。

とりあえずOn Lispからの引き写しで実装し、使ってみます。

=> (defmacro aif [test-form then-form &optional else-form]

... `(let [it ~test-form]
...   (if it ~then-form ,else-form)))
=> (aif (+ 1 2)
... (print 'cheese it it))
cheese 3 3

itでif式 (ここではaif) の条件式の評価結果を参照できています。式のほうがどのように変換されているかを見てみますと

;;; 出力は見やすいよう手で整形

=> (macroexpand '(aif (+ 1 2)
... (print 'cheese it it)))
((u'fn' [] (u'setv' u'it' (u'+' 1L 2L))
(u'if' u'it'
(u'print' (u'quote' u'cheese') u'it' u'it')
u',else_form')))

シンボルがUnicode文字列で表示されてキモいですが、よさそうです。

こんなのの何が嬉しいのかというと


  • 既に(条件分岐するときに)計算した値を利用できる

  • 同じ条件式を何度も書かなくていい

というところが嬉しいポイントです。

例として、条件式が猛烈に時間の掛かる式の場合で考えてみます。

=> (reduce (fn [a b] (+ a b)) (range 1 1000000000))

499999999500000000

この式、とりあえず打ってみたら一分くらいREPLが沈黙してしまいました:sob:。この結果を何度も使用したいときに、

(aif (reduce (fn [a b] (+ a b)) (range 1 1000000000))

(print (+ it it))

などとしておけば複数回の計算を避けることができるという寸法です。局所変数に代入しておけよという話ですが、アナフォリックマクロを書くと、裏で局所変数の作成と代入を自動でやってくれるのです。


HyからPythonへアクセス

最後に。HyのコードをPythonコードに変換することができます。こうすることでREPLでもたもたするHyの危うさを解消したり開発するときはHyで書きコミットするときPythonに変換したりすることができます。

やってみると、実際にパースするオーバーヘッドが減ったりするのか、結果が返ってくるまでのもたつきがなくなります。

(venv) $ cat upto.hy

(defn up-to-n [n]
(list-comp n (n (range n)) (= (% n 2) 0)))

(print (up-to-n (integer (raw-input "input number: "))))

# upto.hyを変換
(venv) $ hy2py upto.hy > upto.py

# 変換結果
(venv) $ cat upto.py
from hy.core.language import integer, range

def up_to_n(n):
return [n for n in range(n) if ((n % 2L) == 0L)]
print(up_to_n(integer(raw_input(u'input number: '))))
(venv) $ python upto.py
input number: 10
[0, 2, 4, 6, 8]


まとめ

Pythonで書かれたLisp方言Hyを紹介しました。


  • Hyのインストールはpipで簡単

  • Hyの構文は簡単・便利・S式

  • HyはPythonの資産にアクセスできる

  • HyはLispのメタプログラミング力を持っている

  • HyのコードをPythonのコードに変換して速くできる

結構しっかりした造りの言語だな、というのが触ってみて書いてみた感想です。

この記事は本来一年前に発信しようと考えていた2ものでした。しかしというかやはりというか、Pythonの世界を知ってから触れるとHyのやりかたのバックグラウンドが推測できました。Pythonに馴染んだ今が丁度よい時期だった、ということなのかもしれません。

Hy自体はなかなかいい言語だと思うので、Python業に従事するLisper諸兄におかれましては、「Pythonを使わないといけないけど構文がS式じゃないなあ」というようなときなどに導入を検討してみてはいかがでしょうか。





  1. ちなみにubuntuのaptリポジトリにpython-hyという名前で登録されているようです。 



  2. 社内LTで発表しようと考えていましたが、この記事LTでやるの大変そうですね…。