Skip to content

Latest commit

 

History

History
2482 lines (2198 loc) · 68.3 KB

20210926143813-clojure.org

File metadata and controls

2482 lines (2198 loc) · 68.3 KB

Clojure

:header-args+: :wrap

概要

ClojureはProgramming Language、LISPの方言の1つ。 プログラムはJava仮想マシン、.NETで動作する。

Memo

インストール

  • Javaのランタイム
  • Leiningen…依存関係管理。
sudo apt-get install leiningen
lein repl

Hello, world

(println "hello world")
(defn hello [name] (str "Hello, " name))
(defn hello-world [username]
  (println (format "Hello, %s" username)))

状態を共有する。 セットに要素を1つ追加する。

(conj coll item)
(conj #{} "Stu")

セットを知る。 conjupdate-fn として渡す。

(def visitors (atom #{}))
(swap! visitors conj "Stu")

atom はClojureが持つリファレンスタイプのひとつ。 ref の中身は deref で見ることができる。略記として @ を使える。

(deref visitors)
@visitors
(defn hello
  "Writes hello message to *out*. Calls you by username.
Knows if you have been here before."
  [username]
  (swap! visitors conj username)
  (str "Hello, " username))
(hello "Rich")
@visitors

状態の問題。それまでにどういう動作をしたかによって、結果が異なってくる。 純粋な関数なら、局所的な知識だけで動作を理解できるが、状態が絡むとそこに至るすべての履歴を理解しなければならない。

ライブラリ

userequirerefer を一度にやってくれる。

(require 'clojure.java.io)
(use 'clojure.java.io)

再読込。

(use :reload 'clojure.java.io)

(find-doc "reduce")

clojure関数のソースを見る

repl ライブラリを使う。

(use 'clojure.repl)
(source identity)

ClojureのコレクションがJavaのコレクションであることを確かめる。

(ancestors (class [1 2 3]))

フォーム

  • Clojureは同図像性を持つ。
  • ClojureのコードはClojureのデータによって組み立てられる。
  • Clojureのプログラムを走らせるときには、まずClojureの中の リーダ と呼ばれる部分がプログラムを フォーム と呼ばれる単位ごとに読み込んで、それをClojureのデータ構造へと変換する。そしてデータ構造をコンパイルして実行する。

フォームにはブール値、文字、キーワード、リスト、マップ…などがある。

シンボル

シンボルは、Clojure内のあらゆる種類のものの名前として使う。

ドットでJavaメソッドの呼び出しであると判断する。

(.toUpperCase "hello")
(str "hello")

リテラル表記は \{letter}

(str \h \e \y \space \y \o \u)

文字を交互に結合。

(interleave "Attack at midnight" "The purple elephant chortled")

文字列に戻す。

(apply str (interleave "Attack at midnight" "The purple elephant chortled"))

復元する。

(apply str (take-nth 2 "ATthtea cpku raptl em iedlneipghhatn"))

Clojureでは空リストは偽ではない。

(if () "We are in Clojure!" "We are in Common Lisp!")

true? は値が true そのものであるかを調べる。

(true? true)
(true? "foo")
(zero? 0.0)

述語一覧。

(find-doc #"\?$")

マップ/キーワード/レコード

マップ。

(def inventors {"Lisp" "McCarthy" "Clojure" "Hickey"})

みやすさのためにコロンを置くこともできる。

(def inventors {"Lisp" "McCarthy", "Clojure" "Hickey"})

マップは関数としても動作する。

(inventors "Lisp")

get 関数も使える。存在しない場合の値を指定できる。

(get inventors "Lisp" "not find")
(get inventors "Foo" "not find")

キーワード はシンボルに似ているが、コロンで始まる。評価されると自分自身を返す。

:foo

シンボルとは異なる。シンボルは評価されるとそれに結び付けられた何かを返そうとする。

foo

キーワードをキーにして書き直す。

(def inventors {:Lisp "McCarthy" :Clojure "Hickey"})

defrecord を使って構造体を定義することで、各マップがどういうキーを取りうるか明示し、強制できる。

(defrecord Book [title author])
(->Book "title" "author")

マップと同じように扱える。

(def b (->Book "Anathem" "Neal Stephenson"))
(:title b)

リーダマクロ

Clojureのフォームはリーダによって読まれて、テキストからClojureのデータに変換される。 リーダマクロは前置される特定のマクロ文字によって起動される、リーダの特殊な動作。 コメントのセミコロンや、評価抑制のクォートもリーダマクロ。

関数

関数呼び出しは単に最初の要素が関数を指すようなリストである。 自分で関数を定義するには defn を使う( def ではない)。

(defn greeting
  "Returns a greeting of the form 'Hello, username.'"
  [username]
  (str "Hello, " username))
(greeting "world")

同名の関数で受け取る引数が違うものを呼び出す。 ゼロ引数の greeting は1引数の greeting に渡して移譲できる。

(defn greeting
  "Returns a greeting of the form 'Hello, username.'
     Default username is 'world'."
  ([] (greeting "world"))
  ([username] (str "Hello, " username)))

(greeting)

フィルタ。 まず述語を定義する。

(defn indexable-word? [word]
  (> (count word) 2))

適用する。

(require '[clojure.string :as str])
(filter indexable-word? (str/split "A fine day it is" #"\W+"))

無名関数バージョン。

(filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+"))
(defn make-greeter [greeting-prefix]
  (fn [username] (str greeting-prefix ", " username)))

(def hello-greeting (make-greeter "Hello"))
(hello-greeting "world")

(def aloha-greeting (make-greeter "Aloha"))
(aloha-greeting "Hawaii")

名前を与えなくてもいい。

((make-greeter "Howdy") "pardner")

var、束縛、名前空間

オブジェクトを def defn で定義すると、オブジェクトはClojureの var に格納される。

リーダマクロで user/foo に結び付けられた var を見る。

(def foo 10)
#'foo

実引数と仮引数のnumberが束縛される。

(defn triple [number] (* 3 number))
(triple 10)

関数の引数束縛はレキシカルスコープ…関数本体の中だけから見える。

コレクション全体を変数に束縛する。 姓と名を両方とも保存するけど、名だけが必要な場合。 ↓引数authorを取るが、必要なのは名だけで、ちょっとわかりにくい。

(defn greet-author-1 [author]
  (println "Hello," (:first-name author)))

必要な部分だけを束縛する。

(defn greet-author-2 [{fname :first-name}]
  (println "Hello," fname))

アンダースコアは慣用的に値を気にしない束縛を示すのに使う。

(let [[_ _ z] [1 2 3]] z)

as でコレクション自体に名前をつけることもできる。

(let [[x y :as coords] [1 2 3 4 5 6]]
  (str "x: " x ", y: " y ", total dimensions " (count coords)))

分配束縛を使う。 [w1 w2 w3] への分配束縛によって最初の3つの単語が取り出される。

(require '[clojure.string :as str])
(defn ellipsize [words]
  (let [[w1 w2 w3] (str/split words #"\s+")]
    (str/join " " [w1 w2 w3 "..."])))

(ellipsize "The quick brown fox jumps over the lazy dogs.")
(resolve 'foo)
(in-ns 'myapp)
(clojure.core/use 'clojure.core)

(def rnd (new java.util.Random))
(. rnd nextInt)
(. rnd nextInt 10)

. はインスタンスのメソッドだけでなく、クラスメンバへのアクセスにも使える。

(. Math PI)

ブラウザで該当ドキュメントに移動する。

(javadoc java.net.URL)

フロー制御

(defn is-small? [number]
  (if (< number 100) "yes" "no"))

(is-small? 50)
(is-small? 101)

分岐後に複数のアクションを起こす、には do を使う。 do は副作用をもたらすという明示になる。

(defn is-small? [number]
  (if (< number 100)
    "yes"
    (do
      (println "Saw a big number" number)
      "no")))
(is-small? 200)

loop を使っての再帰。

(loop [result [] x 5]
  (if (zero? x)
    result
    (recur (conj result x) (dec x))))
(defn countdown [result x]
  (if (zero? x)
    result
    (recur (conj result x) (dec x))))

(countdown [] 5)

さまざまなカウントダウン。

(into [] (take 5 (iterate dec 5)))
(into [] (drop-last (reverse (range 6))))
(vec (reverse (rest (range 6))))
(defn indexed [coll] (map-indexed vector coll))
(indexed "abcde")
(indexed "abcde")

clojureの for はループではなく、シーケンスの内包表記である。

(defn index-filter [pred coll]
  (when pred
    (for [[idx elt] (indexed coll) :when (pred elt)] idx)))
(index-filter #{\a \b} "abcdbbb")
(index-filter #{\a \b} "xyz")
(defn index-of-any [pred coll]
  (first (index-filter pred coll)))
(index-of-any #{\z \a} "zzabyycdxx")
(index-of-any #{\b \y} "zzabyycdxx")

メタデータ

(meta #'str)
(defn ^{:tag String} shout [^{:tag String} s] (.toUpperCase s))
(meta #'shout)

あるいは略記法で書ける。

(defn ^String shout [^String s] (.toUpperCase s))

シーケンス

シーケンスはいろんなデータ構造の抽象。

(class (rest [1 2 3]))
(first ["a" "b"])
(rest ["a" "b"])
(cons "a" "b")

マップはキー/値のペアを要素とするシーケンスとして扱える。

(first {:fname "Aaron" :lname "Bedra"})
(rest {:fname "Aaron" :lname "Bedra"})
(cons [:mname "James"] {:fname "Aaron" :lname "Bedra"})

conjinfo は効率的に追加できる位置に要素を加える。 対象となるデータ構造について一番効率の良い操作をしてくれるので、具体的な実装に結び付けなくても効率的なコードが書ける。

(conj '(1 2 3) :a)
(into '(1 2 3) '(:a :b :c))

シーケンスライブラリ

シーケンスを生成する関数

(range 10)
(range 10 20)
(range 10 20 2)

↓なぜか実行できない。

(repeat 5 "x")

シーケンスは無限に続くので取り出しが必要。

(take 10 (iterate inc 1))
(defn whole-numbers [] (iterate inc 1))
(take 4 (repeat 1))
(take 10 (cycle (range 3)))
(interpose "," ["apple" "banana" "grapes"])
(use '[clojure.string :only (join)])
(join \, ["apples" "banana", "grapes"])

シーケンスをフィルタする関数

(take 10 (filter even? (whole-numbers)))
(take 10 (filter odd? (whole-numbers)))

最初の母音に出会うまで文字列から文字を取り出す操作。

(take-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox")
(drop-while (complement #{\a\e\i\o\u}) "the-quick-brown-fox")
(split-at 5 (range 10))
(split-with #(<= % 10) (range 0 20 2))

シーケンスに対する述語

述語をそれぞれ適応して真偽値を返す。

(every? odd? [1 3 5])
(every? odd? [1 3 8])

いずれかが条件に合わないときはnilを返す。

(some even? [1 2 3])
(some even? [1 3 5])
(some identity [nil false 1 nil 2])
(not-every? even? (whole-numbers))
(not-any? even? (take 10 (whole-numbers)))

シーケンスを変換する関数

各リストに関数を適用する。

(map #(format "<p>%s</p>" %) ["the" "quick" "brown" "fox"])

シーケンスの中身をまとめ上げる。

(reduce + (range 1 11))
(reduce * (range 1 11))
(sort [42 1 7 11])
(sort-by #(.toString %) [42 1 7 11])

ソートの順番を変えられる。

(sort > [42 1 7 11])
(sort < [42 1 7 11])

リスト内包表記バージョン。

(for [word ["the" "quick" "brown" "fox"]]
  (format "<p>%s</p>" word))

:when 節を使うと filter をエミュレートできる。

(take 10 (for [n (whole-numbers) :when (even? n)] n))

1番右側の束縛を最初に繰り返し、1度繰り返すたびにその左側の操作を1つ進める。 なのでrankの繰り返しが先に行われる。

(for [file "ABCDEFGH" rank (range 1 9)] (format "%c%d" file rank))

遅延シーケンスと無限シーケンス

リストは必要なときに作られる。

(use 'examples.primes)
(def ordinals-and-primes (map vector (iterate inc 1) primes))
(take 5 (drop 1000 ordinals-and-primes))

遅延なので、副作用が動かないように見える。

(def x (for [i (range 1 3)] (do (println i) i)))
x ;; 標準入力に出ない

シーケンスの要素をすべて計算する。

(doall x)
(def x (for [i (range 1 3)] (do (println i) i)))
x

シーカブル

シーケンス関数に渡されると、Javaのコレクションは自動的にシーケンスとして振る舞う。 シーケンスとして振る舞えるコレクションをシーカブルであるという。

配列はシーカブル。

(first (.getBytes "hello"))
(rest (.getBytes "hello"))
(cons (int \h) (.getBytes "ello"))

文字列もシーカブル。

(first "Hello")
(rest "Hello")
(cons \H "ello")

逆にして、戻す。

(reverse "hello")
(apply str (reverse "hello"))

Javaのコレクションを使うのは、JavaのAPIを扱うときだけ(Clojure組み込みのコレクションの方が便利)。

正規表現

マッチした文字列からなる変更不可なシーケンスを作り出す。 シーケンスライブラリの関数群がただで正規表現にも使えるようになる。

(re-seq #"\w+" "the quick brown fox")
(sort (re-seq #"\w+" "the quick brown fox"))
(drop 2 (re-seq #"\w+" "the quick brown fox"))
(map #(.toUpperCase %) (re-seq #"\w+" "the quick brown fox"))
(import '(java.io File))
(map #(.getName %) (.listFiles (File. "./public/css")))

ディレクトリを深さ優先で辿る。

(count (file-seq (File. "./public")))

最近更新されたファイルだけを調べる。

(defn minutes-to-millis [mins]
  (* mins 1000 60))
(defn recently-modified? [file]
  (> (.lastModified file)
     (- (System/currentTimeMillis) (minutes-to-millis 30))))
(filter recently-modified? (file-seq (File. ".")))

読み込む(readerをオープンしたまま)。

(use '[clojure.java.io :only (reader)])
(take 2 (line-seq (reader "public/index.html")))

リーダーをちゃんと閉じる。

(with-open [rdr (reader "./public/index.html")]
  (count (line-seq rdr)))

空行を除いた行数。

(with-open [rdr (reader "./public/index.html")]
  (count (filter #(re-find #"\S" %) (line-seq rdr))))

orgコードの行数を数え上げる。

(use '[clojure.java.io :only (reader)])
(defn non-blank? [line] (if (re-find #"\S" line) true false))
(defn non-svn? [file] (not (.contains (.toString file) ".svn")))
(defn org-source? [file] (.endsWith (.toString file) ".org"))
(defn org-loc [base-file]
  (reduce
   +
   (for [file (file-seq base-file)
         :when (and (org-source? file) (non-svn? file))]
     (with-open [rdr (reader file)]
       (count (filter non-blank? (line-seq rdr)))))))
(org-loc (java.io.File. "./"))

特定の構造に特化した関数

シーケンス関数を使うと汎用性の高いコードが書ける。 リスト、ベクタ、マップ、セットに特化した関数もある。

(peek '(1 2 3))
(pop '(1 2 3))
(get [:a :b :c] 1)
(get [:a :b :c] 5) ;; nil

指定のインデックスに新しい値を入れる。

(assoc [0 1 2 3 4] 2 :two)
(subvec [1 2 3 4 5] 3)
(take 2 (drop 3 [1 2 3 4 5]))

特定の構造向けの関数がある場合、それは性能のためであることがほとんど。

(keys {:sundance "spaniel", :darwin "beagle"})
(vals {:sundance "spaniel", :darwin "beagle"})
(get {:sundance "spaniel", :darwin "beagle"} :darwin)
({:sundance "spaniel", :darwin "beagle"} :darwin)
(:darwin {:sundance "spaniel", :darwin "beagle"})

キーとして持っていれば値に関わらずtrueを返す。

(def score {:stu nil :joey 100})
(contains? score :stu)
(def song {:name "Agnus Dei"
           :artist "Kryzysztof Penderecki"
           :album "Polish Requiem"
           :genre "Classical"})

song
(assoc song :kind "MPEG Audio File")
(dissoc song :genre)
(select-keys song [:name :artist])
(merge song {:size 8118166, :time 507245})

同じキーを持っていた場合、どうやって値を組み合わせるかを指定する関数を与える。

(merge-with
 concat
 {:rubble ["Barney"], :flintstone ["Fred"]}
 {:rubble ["Betty"], :flintstone ["Wilma"]}
 {:rubble ["Bam-Bam"], :flintstone ["Pebbles"]})

セットを扱う関数

(use 'clojure.set)
(def languages #{"java" "c" "clojure"})
(def beverages #{"java" "chai" "pop"})

与えられたセットの少なくとも片方にある要素を集める。

(union languages beverages)

最初のセットに含まれ、しかし2番めのセットには含まれないような要素を集めたセット。

(difference languages beverages)

言語の名前でもあり、かつ飲み物の名前でもあるようなもの。

(intersection languages beverages)

1文字だけの言語。

(select #(= 1 (.length %)) languages)

関係演算はSQLなどの問い合わせ言語の基礎となっている。

関係演算データベースClojureの型システム
関係テーブルセットとして動作するものすべて
タプルマップとして動作するものすべて
(def compositions
  #{{:name "The Art of the Fugue" :composer "J. S. Bach"}
    {:name "Musical Offering" :composer "J. S. Bach"}
    {:name "Requiem" :composer "Giuseppe Verdi"}
    {:name "Requiem" :composer "W. A. Mozart"}})
(def composer
  #{{:composer "J. S. Bach" :country "Germany"}
    {:composer "W. A. Mozart" :country :Austria}
    {:composer "Giuseppe Verdi" :country "Italy"}})
(def nations
  #{{:nation "Germany" :language "German"}
    {:nation "Austria" :language "German"}
    {:nation "Italy" :language "Italian"}})
(rename compositions {:name :title})

SQL SELECT文に該当する抜き出し。

(project compositions [:name])

Tasks

プログラミングClojure

  • 37, 39, 44, 56, 67, 74, 81

Reference

clojureでのshell実装。

clojure開発者リッチ・ヒッキーのプレゼン。

リッチ・ヒッキーのプレゼンの解説。 簡単とシンプルさを分ける。抽象化によってシンプルさを保つ。

clojureの本のサンプルコード。

clojureの依存解決ライブラリ。

オブジェクト指向では名詞が重要な地位を占めている。

clojureの特徴がわかる。

Archives