Clojrueに入門したのでメモ。私自身は、PythonとCommon Lispに触れたことがあります。
気軽に走らせるならclojure-cli
Clojureを動かそうと思って調べると、Leiningen (lein
)、boot、clojure-cli (clj
)の3つが出てくる。
100行もないようなコードを書いて動かすのであれば、clojure-cliを利用するべき。他の2つは小さなコードを書く場合でも、プロジェクトフォルダを要求してくる。
ライブラリ・パッケージは deps.edn
+ require
Pythonでのpip install xxx
+import xxx
、Common Lispでの(ql:quickload xxx)
に相当する操作は、Leiningenとclojure-cliで異なる。
clojure-cliでは~/.clojure/deps.edn
に欲しいライブラリを書いておいて (Pythonのpip install
相当)、
;; ~/.clojure/deps.edn
;; 動かしたいコードが書いてあるファイルと同じフォルダに置いても良い。
{
:deps {
org.clojure/data.csv {:mvn/version "1.0.1"}
metasoarous/oz {:mvn/version "2.0.0-alpha5"}
}
}
動かしたコード内でrequire
をする (Pythonのimport
相当)。Python等とは異なり、ライブラリのバージョンを指定する必要がありそう。
;; 動かしたいファイルの先頭に下記を記載
(ns user
(:require [clojure.data.csv :as csv]
[oz.core :as oz]))
ライブラリによってはインストールにそこそこ時間がかかる。clj -Sverbose
をターミナルで実行すれば、①インストール状況と、②前述のdeps.ednが反映されているかどうかが確認できる。
Leiningenではdeps.edn
の変わりに、プロジェクトフォルダのルートフォルダにproject.clj
を置いて、下記のように記載をする。
(defproject ...
:dependencies [[org.clojure/clojure "1.11.1"]
[org.clojure/data.csv "1.0.1"]
[metasoarous/oz "2.0.0-alpha5"]]
...)
bootは触っていないが、多分Leiningenと同じ。
Emacsならcider-mode
EmacsでClojureを触るなら、cider-modeがデファクトスタンダード。Common LispのSlimeであれば(slime)
で動くが、Clojureでは(cider-jack-in)
を使う。clojure-cli/Leiningenのインストール状況次第では、(setq cider-jack-in-default 'lein)
等をしておく必要があるかもしれない。
init.el
でcider-jack-in
を何かのキーに割り当てる際、(cider-jace-in)
だけでは動かない。(cider-jack-in '(:jack-in-type clj :project-type clojure-cli :edit-project-dir t))
のようにする必要がある (cider.el
のcider-jack-in-universal-options
を参照すると良い)。
M-x cider-eval-buffer
でバッファ全体を評価できるが、毎回ファイルをセーブするか聞いてくる。これを防ぐため、init.el
に(setq cider-save-file-on-load t)
を書いておくと良い。
Vimならfireplace
Vimの場合、fireplaceというプラグインがスター数1.7kで人気。日本人開発のvim-icedもあるが、スター数が500前後とfireplaceの後を追っている状況。他にもConjureというプラグインのスター数が1.2k。
vim-iced/Conjureには触れていないので分からないが、fireplaceの場合だと、Vimとは別にターミナルを立ち上げてclj -Sdeps '{:deps {cider/cider-nrepl {:mvn/version "0.30.0"} }}' -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]"
を走らせておく必要がある (Emacsと違い、エディタがそこまで面倒を見てくれない)。またEmacsのcider-modeと異なり、出力結果はエコー領域に出るのみ (cider-modeだと出力はREPLに書き込まれていく) のためprintデバッグがやりにくい。
(追記)Conjureの方が簡単そうです。
Clojureのデータ型
恐らく正確ではないが、概要は下記の通り。
フォーム
フォーム ≒ 一般的に言うデータ型。
-
文字: Common Lispとほとんど同じ。
\a
で表される。 - 文字列: Common Lisp / Pythonとほとんど同じ。
-
数値: Common Lisp / Pythonとほとんど同じ。ただし
float
/int
/ratio
の区別あり ((= 2 2.0) ; false
かつ(= 0.5 (/ 1 2)) ; false
)。 -
シンボル: Common Lispのシンボルと同じだが、大文字小文字の区別あり (
(= 'hoge 'HOGE) ; false
)。 -
ブール値:
true
かfalse
。Common Lispのt
/nil
とほとんど同じ。 -
Nil: ただし
nil
も存在する。関数の返すべき値がないときに、nil
が返される印象。空リストではない ((= nil '()) ; false
) が、if
ではfalse
扱いされる ((if nil true false) ; false
)。 -
キーワード: コロンに続く文字列
:key
で表される。後述のマップで良く使われるイメージ。マップでなぜアトムを利用しないのか、分かっていない。 -
リスト:
'()
。 Common Lispのリストとほとんど同じ。もっぱら関数呼び出しのために使われて、データの入れ物としては使わない。空リストはfalse
ではない ((if '() true false) ; true
)。 -
ベクタ:
[x y z]
。Pythonの配列とほとんど同じ。ただし、あまり二次元以上のものは見ない。 -
マップ:
{:key val}
。Pythonの辞書とほとんど同じ。 -
セット:
#{:a :b :c}
。Pythonの集合とほとんど同じ。
コレクション
コレクションは、リスト・ベクタ・マップ・セットを引っくるめた総称。関数を調べていると、coll
という引数名で良く出てくる。他言語でも似た状況が見られるように、コレクション関数は上記4つのデータ型どれにも適用可能で便利な一方、遅いことも多い。例えばコレクションの最初の要素を返すfirst
は、上記のデータ構造全てに適用できる。
(first '(1 2 3)) ; 1
(first [1 2 3]) ; 1
(first {:a 1 :b 2}) ; [:a 1]
(first #{1 2 3}) ; 1
コレクションの中でも、リストとベクタは順番が意味を持つデータ構造なのでシーケンス (= 順序)と呼ばれる。関数を調べていると、seq
という引数名で良く見かける (分かっていないが、aseq
というのも見かける)。ただ入門者の身からすると、シーケンスであろうとなかろうと使える関数にほとんど差がないため、コレクション ≒ シーケンスの認識でも問題はなさそうに思える。
変数を利用した状態の管理
Clojureでは他言語のように、変数に次々と値を入れて状態を管理するというやり方が推奨されていない。例えば、while
文は存在していない。基本的には、数多く用意された組み込みのコレクション操作関数を駆使して全部解こうとする (Pythonでいう内包表記の変形版を利用するイメージ)。
どうしても状態の管理が必要なときは、ref
・atom
・agent
・var
を利用する (状態の同期が必要かどうか等で利用する関数が変わる)。例えばイベントの受付で、来場者 (誰が来るかは分からない) の名前を記録する (= 入場者の状態を管理する) ことを考える。Python / Clojureの書き方は、それぞれ下記のようになる。
# python
visitors = []
while True:
visitors += [input()]
;; clojure
(def visitors (atom []))
(loop []
(swap! visitors conj (read-line))
(recur))
;; こうは書かない
(def visitors [])
(loop []
(def visitors (conj vistors (read-line)))
(recur))
;; これは推奨されない (状態を再帰で管理している)
(loop [visitors []]
(recur (conj visitors (read-line)))
clojure
の方が (atom
を利用する) ひと手間多いところがミソで、①これで状態の同期・管理を明示的にするとともに、②状態を管理することに対して (精神的な) 税金をかけている1。
Common LispとClojureの異なる点
-
car
系の関数はない。 - 末尾再帰は最適化されないので使わない。
loop
/recur
という局所的な再帰構造を作る関数はあるが、まずはコレクション操作関数で頑張ることが推奨されている。- Common Lispは何でも再帰 (状態が入る余地がある) + マクロで抽象化しようとするが、Clojureは何でもコレクション操作関数 (状態が入らない) で抽象化しようとする。
- 関数の呼び出しは丸括弧で統一。
(xxx ...)
という形を見たら、それは必ず関数呼び出し (リストではない)。 - Schemeのように関数名/変数名の名前空間が一緒なので、mapする際に
#'
は必要ない ((map inc [1 2 3]) ; (2 3 4)
)。 - ベクタの中身が評価される。Common Lispだと
(list (something))
とか`(,something)
だが、Clojureだと[(something)]
。 - ベクタを関数のように利用できる (
([1 2 3] 2) ; 3
)。 - マップを関数のように利用できる (
({:a 1 :b 2} :a) ; 1
)。マップなら逆も可 ((:a {:a 1 :b 2}) ; 1
)。 -
(doc 関数名)
で、REPLで関数定義を確認できる。関数定義の記載もシンプルで良い。 - Clojurianという言葉があるように、言語の設計として特定のスタイルを推奨している。PerlよりもPython的。
- Clojureのコードは暗号のように見えがち。PythonよりもPerl的。
- 基本的な発想は、シェルがパイプでデータを流していくように、Clojureはコレクションでデータを流していくというもの。恐らく、絶対に必要な箇所以外で
let
/def(setq)
を利用するのはClojureらしくない。 - システム外部の状態は本質的だけど、システム内部の状態は無駄という発想。
- 基本的な発想は、シェルがパイプでデータを流していくように、Clojureはコレクションでデータを流していくというもの。恐らく、絶対に必要な箇所以外で
-
『関数的プログラミングを理想とするというのは,プログラムが決して副作用を使ってはいけないということではない.ただ必要以上に使うべきでないということだ.この習慣を育てるには時間がかかるかもしれない.一つの方法は,以下のオペレータは税金がかかっているつもりで扱うことだ:
set
setq
setf
...』, On Lisp (野田開 訳), ポール・グレアム, http://www.asahi-net.or.jp/~kc7k-nd/onlispjhtml/functionalProgramming.html ↩ -
Common Lispは、プログラマにとって (Simpleではなく) Easyであることを善としているとも考えられる。コードは、抽象化の層が重なることになる → 『本当の非効率性とは、マシンの時間を無駄にすることではなく、プログラマの時間を無駄にすることだ』, 百年の言語, ポール・グレアム (川合史朗 訳), http://practical-scheme.net/trans/hundred-j.html ↩
-
昔は計算資源を最適化するために人間の時間が取られていたが、Clojureは問題設定を最適化することに人間の時間を使おうとしているように思われる。ポール・グレアムは良いコードの条件を「解くべき問題が簡潔に表現されていること」とした一方で、リッチ・ヒッキーは「解くべき問題が簡潔になっていること」としているように思われる。 → 『私が自分のプログラムにパターンを見付けたら、それはどこかがおかしいというサインだ。プログラムの形は、それが解くべき問題のみを反映すべきだ』, 技術野郎の復讐, ポール・グレアム (川合史朗 訳), http://practical-scheme.net/trans/icad-j.html ↩