はじめに

皆さん、python書いてますか?

なんだか最近は機械学習やらなにやらで当然pythonでやっているよね?という空気が皆さんの職場にも暗黙のうちにあると思います。
こういった職場環境は自分のような肩身の狭い隠れlisperにとってみれば、ハラスメントと言っても過言ではありません。
確かにnumpy, pandas, matplotlib, scipy, tensorflowといろいろ便利なものがpythonにはあります。
だけどやっぱりおもちゃはおもちゃです。妙なスコープ、貧弱な構文でたくさん頑張るマゾ言語としか思えません。
どうにかして、甘い蜜(資産)だけ吸いながら脱出したいとずっと思っていました。

そこで、hylangです。

lispっぽいpythonです。リッチではないですが、十分なマクロがあります。
lispでマクロがあるなら、形はどうにでもできます。(やっていいかは別として)

というわけで、pythonを一切書かずにhylangで快適に作業をするための準備をここ最近でやったので、それをまとめます。
hylangそのものに関する説明はすでにQiitaに素晴らしい記事がいくつかありますので、そちらを参考にするといいと思います。
説明は以下の流れでやります。スキップしたかったら(※)まで右のoutlineから行くと楽です。

  • hylangの見た目が気に入らないのでCommon Lispっぽくよせる
  • loopをもうちょっと高級にする

--------ここまで宗教の話(スキップ推奨)--------

  • Calysto Hy+einでグラフの可視化まで行う
  • githubで共有するためのnotebookに仕上げる

hylangの見た目をどうにかする

(※ここでは、言語の個人的な好みによるカスタムという、マクロでやるべきでないことをやっていますし、興味ない人はスキップでもいいです。というかStyle Guideをガチンコで無視しているので、もうごめんなさいとしかいえないです。)

さて、一見すれば明らかなことですが、hylangはClojureの影響を強く受けています。そのため見た目はモダンでいい感じです。
文法的な意味で、ClojureはRich Hickeyが作り上げたCommon Lisp上のオレオレマクロ集だと個人的には思っていて、Common Lisperからするとわかるーwあるあるーwという設計になっている場合が多いです。
しかし、しばらく書いていると「これは俺のLispではない!」という気持ちが強くなってきてCommon Lispに帰ってきたくなります。
あと、Common Lispによせておけば、Common Lispで書かれたライブラリの一部がそのまま動くかもしれないなど、嬉しいこともあります。
だから、もう新しいlispを作るのはいい加減やめよう。ANSI Common Lispに従えばみんなが嬉しいのに。。。

すこしだけimport

では、やっていきます。
必要なもののimportからですね。すごく少ないのがhylangの強力さを物語っています。しかも半分くらいがなくてもいいものです。

(import (hy (HyKeyword)))
(require (hy.contrib.loop (loop)))
(import
  re
  glob
  fnmatch
  (functools :as ft))

よく使う見慣れた名前の関数たち

次は基本的な関数などです。
eqがSymbol(HySymbol)に対して実はCommon Lispのようには動かないとか、cdrの実装きもすぎとか色々ツッコミどころがありますが、細かい話はあとでまとめて別の記事にしようかと思います。
ここにおけるポイントは、これらの関数はマクロのために主に使おうと思っているため、作るにあたって性能は大して気にしていないし、もっというとそれら全てがなるべくHyExpressionを返すようにしています。
nilがないとかもう言いたいことは尽きないですが、あがいた結果という感じです。

nilはグローバルに作ったので、破壊的な操作を行うと宇宙の法則が乱れます。

(eval-and-compile
  (import (functools :as ft))

  (defmacro setf (&rest args)
    `(setv ~@args))

  (defmacro typep (obj objtype)
    `(is (type ~obj) ~objtype))

  (defmacro defun (name lambda-list doc &rest body)
    (if (not (typep doc str))
        `(defn ~name ~lambda-list ~@(cons doc body))
        `(defn ~name ~lambda-list doc ~@body)))

  (defun eq (x y)
    (is x y))

  (defun equals (x y)
    (= x y))

  ;; numerical functions
  (defun mod (n m)
    (% n m))

  (defun zerop (n)
    (= n 0))

  (defun oddp (n)
    (zerop (mod n 2)))

  (defun evenp (n)
    (not (oddp n)))

  (defun divisible (n m)
    (zerop (mod n m)))

  (defmacro incf (n &optional (delta 1))
    `(setf ~n (+ ~n ~delta)))

  (defmacro decf (n &optional (delta 1))
    `(setf ~n (- ~n ~delta)))

  ;; list functions
  ;; ------------------- DO NOT SET nil!!-----------------------------
  (setf nil (HyExpression ()))
  ;; ------------------- DO NOT SET nil!!-----------------------------

  (defun null (ls)
    (= nil ls))

  (setf HyCons (type '(1 . 2)))

  (defun lst (&rest args)
    (HyExpression args))

  (defun length (list)
    (len list))

  (defun emptyp (ls)
    (zerop (length ls)))

  (defun consp (el)
    (and (not (= el nil))
         (or (typep el HyExpression)
             (typep el HyCons))))

  (defun car (ls)
    (if (typep ls HyCons)
        (. ls car)
        (first ls)))

  ;; sane cdr
  (defun cdr (ls)
    (if (emptyp ls)
        nil
        (if (typep ls HyCons) ;; malformed cons
            (. ls cdr)
            (HyExpression (rest ls)))))

  (defun cadr (ls)
    (-> ls car cdr))

  (defun cdar (ls)
    (-> ls cdr car))

  (defun mapcar (func &rest seqs)
    (HyExpression
      (apply (ft.partial map func) seqs)))

  (defmacro push (el ls)
    `(setf ~ls (cons ~el ~ls)))

  (defun nreverse (ls)
    (.reverse ls)
    ls)

  (defun nconc (x y)
    (.extend x y)
    x)

  (defun last (ls)
    (get ls (dec (length ls))))

  (defun mapcan (func ls)
    (loop
      ((ls ls)
        (acc ()))
      (if ls
          (recur (cdr ls) (nconc acc (func (car ls))))
          (HyExpression acc))))

  (defun append (ls1 ls2)
    (+ ls1 ls2))

  (defmacro progn (&rest body)
    `(do ~@body))
)

letがないんですね

次はもうちょっと高級なマクロたちです。ここまで無理やりCommon Lispにする理由はあとで判明します。
飛行機の中で暇だったという理由で書きなぐったマクロですので、実装は割とお粗末なやつが多いです。(言い訳)
あとデフォルトでdefmacro/g!があるのはちょっと嬉しいですね。なぜかonce-onlyとdefmacro!は無いようですが。
letがないのはpythonのスコープ(global, local, nonlocal)がゴミすぎて完璧なletがどうしても作れないことによると思われます。
苦渋の判断としてletは外したんだろうなあと考えると心が苦しくなりますね。
このあたりにバグは抱えたくないので、早急に解決策を考えたいところです。

(eval-and-compile

  (defmacro lambda (lambda-list &rest body)
    `(fn ~lambda-list ~@body))

  (defmacro/g! let (var:val &rest body)
    `((lambda ~(mapcar car var:val) ~@body)
       ~@(mapcar cdar var:val)))

  (defmacro/g! let* (var:val &rest body)
    (loop
      ((ls (nreverse var:val))
        (acc body))
      (if ls
          (recur (cdr ls) `(let (~(car ls))
                                ~@(if (= acc body)
                                      body
                                      `(~acc))))
          acc)))

  (defmacro/g! prog1 (&rest body)
    `(let ((~g!sexp-1 ~(car body)))
          (progn
            ~@(cdr body)
            ~g!sexp-1)))

  (defmacro when (condition &rest body)
    `(if ~condition
         (progn
           ~@body)))

  (defmacro unless (condition &rest body)
    `(if (not ~condition)
         (progn
           ~@body)))  

  (defun pushl (ls el)
    (.append ls el))

  (defun flatten-1 (ls)
    (let ((acc ()))
         (for (el ls)
           (if (consp el)
               (nconc acc el)
               (.append acc el)))
         acc))

  )

これがないとコード書けない!ってなるものたち(+便利なリーダマクロ)

最後です。条件系、束縛系、error系、リーダーマクロです。
ここでCommon Lispによせたことでちょっと楽ができるところに来ました。
dbind(destructuring-bind)の実装はOn Lispの18章から持ってきてほんの少しだけ変更したものです。
そう、The Tao of hyを無視する代償として、On Lispの資産が飲み込めるようになりました。ぐへへ。
Paul Grahamのコードはクラシックな感じで味がありますね。

(eval-and-compile
  (defmacro cond/cl (&rest branches)
    `(cond ~@(map list branches)))

  (defmacro/g! case (exp &rest branches)
    `(let ((~g!val ~exp))
          (cond/cl ~@(list (map (lambda (br)
                                  (if (= (car br) 'otherwise)
                                      `(True ~@(cdr br))
                                      `((eq ~g!val ~(car br)) ~@(cdr br))))
                                branches)))))

  (defun subseq (seq start end)
    (case (type seq)
          (str (.join "" (islice seq start end)))
          (list (list (islice seq start end)))
          (HySymbol (HySymbol (.join "" (islice seq start end))))
          (HyExpression (HyExpression (islice seq start end)))
          (HyKeyword (HyKeyword (.join "" (islice seq start end))))
          (otherwise (raise TypeError))))

  (defun destruc (pat seq n)
    (if (null pat)
        nil
        (let ((rest (cond/cl ((not (consp pat)) pat)
                             ((eq (car pat) '&rest) (cadr pat))
                             (True nil))))
             (if rest
                 `((~rest (subseq ~seq 0 ~n)))
                 (let ((p (car pat))
                        (rec (destruc (cdr pat) seq (+ n 1))))
                      (if (not (consp p)) 
                          (cons `(~p (get ~seq ~n))
                                rec)
                          (let ((var (gensym)))
                               (cons (cons `(~var (get ~seq ~n))
                                           (destruc p var 0))
                                     rec))))))))

  (defun dbind-ex (binds body)
    (if (null binds)
        `(progn ~@body)
        `(let ~(mapcar (lambda (b)
                         (if (consp (car b))
                             (car b)
                             b))
                       binds)
              ~(dbind-ex (mapcan (lambda (b)
                                   (if (consp (car b))
                                       (cdr b)
                                       nil))
                                 binds)
                         body))))

  (defmacro/g! dbind (pat seq &rest body)
    `(let ((~g!seq ~seq))
          ~(dbind-ex (destruc pat g!seq 0) body)))


  (defmacro values (&rest returns)
    `(HyExpression (list ~returns)))

  ;; multiple-value-bind
  (defmacro mvb (var-list expr &rest body)
    `(dbind ~var-list ~expr ~@body))

  ;; errors
  (defmacro/g! ignore-errors (&rest body)
    `(try
       ~@body
       (except (~g!err Exception)
         nil)))

  (defmacro/g! unwind-protect (protected &rest body)
    `(try
       ~protected
       (except (~g!err Exception)
         ~@body
         (raise ~g!err))))

  ;; sharp macros
  (defmacro/g! pr (&rest args)
    `(let ((~g!once ~(car args)))
          (print ~g!once ~@(cdr  args))
          ~g!once))

  (defsharp p (code)
    "debug print"
    `(pr ~code))

  (defsharp r (regex)
    "regexp"
    `(re.compile ~regex))

  (defsharp g (path)
    "glob"
    `(glob.glob ~path))

  (defun path-genr (fname dir)
    (for (tp (os.walk dir))       
      (for (f (get tp 2))
        (if (fnmatch.fnmatch f fname)
            (yield (os.path.join (get tp 0) f))))))

  (defsharp f (dir-fname)
    "find file name"
    `(path-genr ~(get dir-fname 1) ~(get dir-fname 0)))
  )

以上は全部まとめて、pypiに登録しておきました。コードはこちらです。

$ pip install hycl

使い方はあとで見せることになるので、先に行きましょう。

強力なloop構文を導入する

pythonは式(expression)と文(statement)が混在できないという病気にかかった言語です。全部が式でいいじゃないか!と思うんですが、複雑なことを縛るなら確かに強力な制約です。
そのせいで何が起きたかというと、lambdaがゴミと化し、forが返り値を持てなくなり、リスト内包表記なる式の世界のforが生まれたりしました。
リスト内包表記は一つのリストしか作れないので、複数のリストを1ループで作るには使えなかったり、あるいは余計に難しいことをする必要が出てしまいます。その代わりに速かったりはしますけど。

気に入らなければ強力な構文を導入しましょう。lispならできます。
スクラッチから設計するのは筋が悪いので、頭の良いlisperたちの過去の仕事を参考にします。
CLtL2にはloopマクロというのが仕様で入っています。
お手本のようなマクロで記述されたloop記述用の小言語、DSLなのですが、極めて強力です。
慣れるとこれなしにアルゴリズムを書けなくなる病気にかかったり、do系のマクロ(do, dotimes, dolist)を全部忘れたりする中毒性の高いマクロです。

一方これには大きな欠点があり、各節(clause)がS式になっていないため、S式のメリットが一部死んでいます。
せっかくカッコをいっぱいつけたから、マクロが書けるし、paredit-modeで他言語とは別次元で編集しやすくなったのに、それはもったいないというものです。

当然、改善案があります。iterateです。
どう違うのか説明するより、見たほうが早いです。
0~9の整数をループで回して、偶数と奇数に分けて集めてみます。ちなみにサンプルコードはCommon Lispになります。
まずはloop版です。

CL-USER> (loop
            for i from 0 below 10
            if (= (mod i 2) 0)
              collect i into evns
            else
              collect i into odds
            finally
              (return (values evns odds)))
(0 2 4 6 8)
(1 3 5 7 9)

びっくりするくらい豊富な機能が実装されてますよー。

で、お次がiterateです。

CL-USER> (iter
           (for i from 0 below 10)
           (if (= (mod i 2) 0)
               (collect i into evns)
               (collect i into odds))
           (finally
             (return (values evns odds))))
(0 2 4 6 8)
(1 3 5 7 9)

美しいですねー。カッコがあると落ち着きます。
これを見てloopのほうがいいとか言う人いるのかっていうレベルですよね。
そのため今回もiterateを採用し実装しました。
これもpypiに登録しておいたので使ってください。
本家iterateは拡張性のために結構実装が大きいので、独自実装になってしまいました。そのためクソコードです。

$ pip install hyiter

Calysto Hy + einでグラフ描画まで行う(※)

(ここから本題、emacsを使う万人向けの内容です。
ここまでスキップされた人とって、たまに、ん?ってなるマクロや関数が登場しますが、適宜脳内変換してください。)

最近は色々な言語をJupyter notebook上で動かすカーネルが用意されているようで、Calysto Hyはhylang用のkernelです。
以下でインストールできます。

$ pip install git+https://github.com/Calysto/calysto_hy.git
$ python -m calysto_hy install

そうするとCalysto Hyのnotebookがjupyterから作れるようになります。
もうこれで完成じゃないかと思いましたが、まだ実はそれはそれは大きな障害があります。
Calysto Hyは%matplotlib inlineに対応していません。画像がipynbに埋め込めないのです。
これはまずいというかこれ何に使えるんだという気持ちが高まり絶望しかけましたが、emacsにはeinがあります。
Jupyter notebookをGUIのEmacsに引きずり込む素晴らしいプラグインですが、こいつでEmacsに持ってこれれば画像の表示なんてipynbとは関係なく実現できます。
iimage-modeを使えばバッファに画像パスがあるだけで、勝手に埋め込んで表示してくれます。
つまり、図を描画して、そいつを画像として適当なパスに吐き出して、そのパスをバッファにプリントしてやれば、まあ概ねmatplotlib inlineと同じ動きになります。
簡単です。そういうマクロを用意します。

(defmacro vis ()
  `(let ((desc-fname (tempfile.mkstemp :suffix ".png")))
        (os.close (car desc-fname))
        (let ((fname (get desc-fname 1)))
             (plt.savefig fname)
             (print fname))))

(defmacro rvis (figsize &rest body)
  `(progn
     (plt.clf)
     (if ~figsize
         (plt.figure :figsize ~figsize))
     ~@body
     (plt.legend :loc "best")
     (vis)))

これをein上で使うと以下のような感じになります。
Screenshot from 2018-02-10 00-07-25.png

einは素晴らしいです。もともと流石にブラウザの上でコーディングはきついので使っていましたがこんな風に使用を正当化できる日がくるとは思いませんでした。html出力さえできればもう何も欠点はありませんねー。
ただ、自分の環境だとxsrfまわりでリモートのJupyterに接続するときにこけるので、現状アドホックですが、以下のようにein-query.elを変更しています。次はEmacs Lispです。
どうやってもloginが失敗するようなら適用してみてください。
一度.emacs.d/request/curl-cookie-jarを消して、ファイルがちゃんとできるまでやってみください。何度かloginに失敗するかもしれませんが、jupyterを落としてみたりと何度か粘ると成功するようになります。一度成功すればそれ以降は大丈夫です。

(defun ein:query-prepare-header (url settings &optional securep)
  "Ensure that REST calls to the jupyter server have the correct
_xsrf argument."
  (let* ((parsed-url (url-generic-parse-url url))
         (cookies (request-cookie-alist (url-host parsed-url)
                                        "/" securep))
         (host (aref parsed-url 1)))
    (let* ((cont (split-string (f-read-text "~/.emacs.d/request/curl-cookie-jar") "\n")))
      (dolist (line cont)
        (let ((splited (split-string line "\t")))
          (if (string= (nth 0 splited) host)
              (progn
                (message "-------------yay-------------------------")
                (setf cookies (cons `("_xsrf" . ,(car (last splited))) cookies)))))))        
    (ein:aif (assoc-string "_xsrf" cookies)
        (setq settings (plist-put settings :headers (list (cons "X-XSRFTOKEN" (cdr it))))))
    (ein:aif (ein:jupyterhub-url-p (format "http://%s:%s" (url-host parsed-url) (url-port parsed-url)))
        (progn
          (unless (string-equal (ein:$jh-conn-url it)
                                (ein:url (ein:$jh-conn-url it) "hub/login"))
            (setq settings (plist-put settings :headers (append (plist-get settings :headers)
                                                                (list (cons "Referer"
                                                                            (ein:url (ein:$jh-conn-url it)
                                                                                     "hub/login")))))))
          (when (ein:$jh-conn-token it)
            (setq settings (plist-put settings :headers (append (plist-get settings :headers)
                                                                (list (cons "Authorization"
                                                                            (format "token %s"
                                                                                    (ein:$jh-conn-token it))))))))))
    settings))

あとは、iimage-modeはactivateしたときに画像を埋め込むようにできてるので、画像パスを吐き出したらその都度onoffする必要があります。
そのため、自分は以下のようなコマンドを用意しています。

(define-key ein:notebook-mode-map (kbd "\C-ci") (lambda ()
                                                   (interactive)
                                                   (turn-off-iimage-mode)
                                                   (turn-on-iimage-mode)))

githubで共有するためのnotebookに仕上げる

以上でCalysto Hy+ein+iimage-modeで快適に作業ができます。よしこれで!全部解決!と思いましたが、まだ問題があります。
この状態で作業する分には快適ですが、その後ipynbをgithubで共有する時に、ipynbファイルには画像実体が埋め込まれていないので、なんだかよくわからんtempfileのパスが書いてあるだけで図が見えません。
では、自分で埋め込みましょう。

(import (nbformat :as nbf))

(defun transform-output (cell)
  (iter
    (for output in (get cell 'outputs))
    (if (and (= (get output 'output-type) 'stream) (os.path.exists (get output 'text)))
        (let ((new-output (nbf.v4.new_output 'display-data)))
          (setf (. new-output data) {"image/png" (base64.encodestring
                                                   (.read (open (get output 'text) 'rb)))})
          (collect new-output into outputs))
        (collect output into outputs))
    (finally
      (setf (. cell outputs) outputs)
      (return cell))))

(defun export-nb (path out-path &optional (version 4))
  (defun match-path (text)
    (os.path.exists text))
  (iter
    (with nb = (nbf.read path version))
    (for i = 0 then (+ i 1))
    (for cell in nb.cells)    
    (collect (if (= (get cell 'cell-type) 'code)
                  (transform-output cell)
                  cell)  into cells)
    (finally
      (setf nb.cells cells)
      (nbf.write nb (open out-path 'w)))))

ipynbファイルを直接読みに行って、それらしいパスがoutputにあればその画像を読み込んでbase64エンコードしてipynbにぶち込みます。大したことはありません。
結果として、これこうなります。すばらしい!!
lispが読めないチームメイトも見慣れた可視化結果にはニッコリです。

ここまでして別にemacsで作業したくないわと思う人もいるかもしれないですが、以下のようなメリットがあります。

  • ブラウザのショートカットを気にせず自由にコマンドを仕掛けられる
  • セルの編集時に補完、チェック、編集コマンド(paredit)などのエディタにある機能がフルで使える。
  • セルに対する動作もいくらでも定義可能(cellをひっくり返す、コピー、などを組み合わせたなにか)
  • 作業用のnotebookには画像実体が埋め込まれていないので、ロードが爆速

おわり

どうでしたか。誰向けなんだという感じですね。

自分としては機械学習やらでpythonを強要されている隠れlisperたちに「俺もこんなふうに頑張ってるぞ!」というのが伝わればと思って、今回記事にしてみました。
lispを知らないかわいそうな人たちには不便なpythonでせっせと便利なライブラリを作ってもらって、我々はマクロとparedit、まだ貧弱だけどslimeっぽいhy-modeがある素晴らしい環境でそれを利用しようじゃありませんか。
lisperが、喰われる側ではなく、喰う側にまわる時がきました。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.