Edited at

PythonistaのためのLisp入門:補足

More than 1 year has passed since last update.


はじめに

PythonistaのためのLisp入門(Hyチュートリアル和訳)の補足的な記事です.Hy Tutorialでは「Pythonでこう書くものはHyではこう書く」的な内容がほとんどでしたがそれも実用には十分でないところが有ります.この記事ではHyならではの,言い換えるとPythonにはないHyの機能およびHy独自の特徴について補足的に解説したいと思います.これを読めばとりあえずPythonの代わりにHyを使うには十分だと思います.


あわせて読みたい

Qiita以外だと以下の良記事を見つけたので併せてご参照ください.(前の投稿とモロかぶりのうえこちらのほうが有用です...)


Pythonと同様の型をサポートしています.ただし,Python2.x系と3.x系で異なる部分が有ります.挙動は同じですが型が異なります.


整数型

Python2.xではlong,3.x系ではintとなります.


Python2での整数

=> (type 2)

<type 'long'>


Python3での整数

=> (type 2)

<class 'int'>


文字列型

Python2.xではunicode,3.x系ではstrとなります.


Python2での文字列

=> (type "a")

<type 'unicode'>


Python3での文字列

=> (type "a")

<class 'str'>

ちなみに文字列はダブルクォーテーション(")で括ります.クォーテーション(')はダメです.また,Pythonと異なり,途中での改行が認められています.


nil

HyはLispなのでnilがないと始まらないように思います.しかしHyはPythonでもあるのでどちらかというと型に関してはPython準拠です.従ってnilはありません

文脈に応じて空のリスト([] or ()),空のタプル((,)),FalseNoneなどを用いましょう.LisperでないPythonistaな方は今までどおりで大丈夫です.


括弧の種類

Clojureと同様,他のLispたちとは異なり括弧に区別が有ります


(...)

リストを表します.ただし,Pythonのlistとは微妙に異なる概念ですので区別のためにLispのリストと呼ぶことにします.

Lispのリストでは最初の要素を関数とし,残りの要素をその引数として適用します.

従って(function value1 value2 ...)はPythonでいうfunction(*[value1, value2 ...])に相当します.


Lispのリスト

=> (+ 1 2 3)

6
=> (1 2 3)
Traceback (most recent call last):
File "/usr/local/lib/python3.5/dist-packages/hy/cmdline.py", line 102, in runsource
ast_callback)
File "/usr/local/lib/python3.5/dist-packages/hy/importer.py", line 176, in hy_eval
return eval(ast_compile(expr, "<eval>", "eval"), namespace)
File "<eval>", line 1, in <module>
TypeError: 'int' object is not callable

二番目の例ではPythonでいう1(*[2, 3])を評価したこととなり,もちろん1callableでは無いのでエラーとなります.

関数としての評価をしたくない時にはクォーテーション(')を用います.


Lispのリストwithクォーテーション

=> '(1 2 3)

(1 2 3)


[...]

Pythonにおけるいわゆるリストを表します.またClojureに習ってベクタとも呼びます.これは先程のクォートしたLispのリストと等価です.


Pythonのリスト

=> [1 2 3]

[1, 2, 3]
=> (= [1 2 3] '(1 2 3))
True


{...}

Pythonと同じく辞書(dict)をあわらします.キーと要素を交互に並べます.


dict

=> {"a" 1 "b" 2}

{'b': 2, 'a': 1}

またキーは:keyの形でも良いです.


dictその2

=> {:a 1 :b 2}

{'\ufdd0:a': 1, '\ufdd0:b': 2}

要素のアクセスはgetを用います.


dictの要素にアクセス

=> (get {"a" 1 "b" 2} "a")

1
=> (get {:a 1 :b 2} :a)
1

また,その2の書き方(:key)の場合は以下のアクセスも可能です.


dictの要素にアクセスその2

=> (get {:a 1 :b 2} :a)

1
=> (:a {:a 1 :b 2})
1


まとめ

ここまでを表にまとめます.また説明していませんでしたがタプルも表に含めています.

Hyでの表現
対応するPythonでの表現

リスト
(function arg1 arg2 ...)

関数の呼び出し
function(*[args1, arg2, ...])

クォートしたリスト
'(elem1 elem2 ...)

リスト
[elem1, elem2, ...]

ベクタ
[elem1 elem2 ...]

リスト
[elem1, elem2, ...]

タプル
(, elem1 elem2 ...)

タプル
(elem1, elem2, ..., )

辞書その1
{"key1" elem1 "key2" elem2 ...}

辞書
{"key1": elem1, "key2": elem2, ...}

辞書その2
{:key1 elem1 :key2 elem2 ...}

辞書
{"key1": elem1, "key2": elem2, ...}


関数のオーバーロード(多重定義)

PythonではC++やJavaのように引数の型や個数に拠って関数の本体を切り替えることはできません.Hyではhy.contrib以下のモジュールにこれを実現するマクロが用意されています.


hy.contrib.multi.defn

組み込みのマクロであるdefnを拡張して引数の個数による多態を実現します.


defnによる個数オーバーロード

(require [hy.contrib.multi [defn]])

(defn function
([a] (+ "a = " (str a)))
([a b] (+ "a = " (str a) ", b = " (str b))))

(print (function 3))
;; > a = 3
(print (function 3 4))
;; a = 3, b = 4


上記の例では引数の個数がひとつの場合とふたつの場合で処理を切り替えています.なお,通常のdefnと同じように使えばそのまま問題なく使えるので安心してください.


hy.contrib.multi.defmulti, defmethod, default-method

多重ディスパッチによる特殊化を実現します.いわゆるマルチメソッドと呼ばれるものです.


defmultiによるマルチメソッド

(require [hy.contrib.multi [defmulti defmethod default-method]])

(defmulti add [x y] (, (type x) (type y)))

(defmethod add (, int str) [x y]
(+ (str x) y))

(defmethod add (, str int) [x y]
(+ x (str y)))

(default-method add [x y]
(+ x y))

(print (add 1 "st"))
;; > 1st
(print (add "FF" 14))
;; > FF14
(print (add 2 4))
;; > 6
(print (add "Hello, " "world!"))
;; > "Hello, world!"


defmultiマクロでは引数の条件に用いる要素を定義します.今回の場合だと,2つの引数をとってその引数の型を格納したタプル(, (type x) (type y))をトリガーとしてディスパッチしています.defmethodマクロでは条件を設定し,条件ごとの実行内容を定義します.またdefault-methodマクロはどの条件にも当てはまらなかった場合の実行内容を定義できます.上記の例では型をトリガーにしていますが,ぶっちゃけ何でもトリガーにできます.

例えば以下のようなコードも大丈夫です.


defmultiによるマルチメソッドその2

(require [hy.contrib.multi [defmulti defmethod default-method]])

(defmulti funtion [&rest args] (first args))

(defmethod funtion 1 [&rest args]
(print "the list of arguments starts with 1"))

(defmethod funtion 2 [&rest args]
(print "the list of arguments starts with 2"))

(default-method funtion [&rest args]
(print "the list of arguments starts with something other than 1 and 2"))

(funtion 1 3 4)
;; > the list of arguments starts with 1
(funtion 2 3)
;; > the list of arguments starts with 2
(funtion 4 8 9 0)
;; > the list of arguments starts with something other than 1 and 2


上記の例では複数個の引数を受け取ってその先頭要素が何かでディスパッチしています.


遅延シーケンス

Pythonのジェネレータのようなものですが,ジェネレータとは異なり同じ要素に何度でもアクセスできます.もちろん,ジェネレータ同様遅延評価されます.要は,遅延評価+メモ化が備わったリストのようなものです.hy.contrib.sequences.defseqマクロで定義できます.例えばフィボナッチが以下のように書けます.


遅延シーケンスでFibonacci

(require [hy.contrib.sequences [defseq]])

(import [hy.contrib.sequences [Sequence]])
;; マクロ展開後に使用されているのでimportしておかいといけない

(defseq fibonacci [n]
(if (<= n 1) n
(+ (get fibonacci (- n 1)) (get fibonacci (- n 2)))))

(print (get fibonacci 100))
;; > 354224848179261915075


上記のコードは遅延評価のおかげでミリ秒で終了しますが,普通に再帰関数で書くと恐ろしく時間がかかります.


先行ドット構文(2017/06/06追記)

Hyではメソッド呼び出しに先行ドット構文(leading dot syntax)というものが使えます.(object.method args...)(.method object args...)のように書けるシンタックスシュガーです.


先行ドット構文(メソッド呼び出し)

=> (def a [1 2 3])

=> (.append a 4)
=> a
[1, 2, 3, 4]


先行ドット構文(モジュール名前空間へのアクセス)

=> (import ast)

=> (.parse ast "print(\"Hello, world!\")" :mode "eval")
<_ast.Expression object at 0xb6a2daec>


スレッドマクロ(2017/06/06追記)

チュートリアルの最後にも出てきましたが,Hyにはスレッドマクロという可読性を向上させる非常に便利な機能があります.これはClojureから引き継いだものです.いくつか種類が有りますので個別に説明します.


->, ->>

(func3 (func2 (func1)))(-> func1 func2 func3)のように書けます.->の場合次の式の最初の引数としてチェーンされ,->>の場合最後の引数としてチェーンされます.具体例を示します.


->と->>の例

=> (def a 8.)

=> (-> a (/ 4) (- 1)) ;; (- (/ a 4) 1)
1.0
=> (->> a (/ 4) (- 1)) ;; (- 1 (/ 4 a))
0.5


as->

->および->>では最初か最後にしか引数を渡せません.途中に渡したい場合や,関数によって渡す位置が変わる場合に対応できません.そこで活躍するのがas->です.


as->の例

=> (as-> a it

... (/ it 4)
... (- 1 it))
-1.0

ここではaitという一時的な名前をつけています.


doto

すこし毛色が異なりますがまとめて紹介します.D言語のWith構文のようなもので,単一のオブジェクトに対するメソッド呼び出しの連続を簡単にするものです.(obj.method1) (obj.method2) ...(doto obj .method1 .method2 ...)のように書けます.


dotoの例

=> (doto [2 3] (.append 1) .sort)

[1, 2, 3]


スレッドマクロの使いどころ

括弧のネストが深くなる場合に特に有効です.Pythonではイマイチになってしまう表現をスマートに書ける場合も多く有ります."tanaka taro""Taro Tanaka"に変換する例を考えてみましょう.


Pythonで田中太郎

" ".join(map(str.capitalize, reversed("tanaka taro".split())))


処理の流れを追うのが少し面倒です.これと同様のことをHyでは以下のようにできます.


Hyで田中太郎

(->> "tanaka taro" .split reversed (map str.capitalize) (.join " "))


非常にすっきりした見た目であるだけでなく,処理の流れも左から右へスラスラ読めるようになりました.

要するにスレッドマクロとはRubyのメソッドチェーンやD言語のUFCSのようなものです.可読性が格段に向上しますので積極的に使いましょう.


おわりに

その他思いつけば追記すると思います.また間違い等あればぜひご指摘ください.