Clojure学習メモ
本家
教材
1章
- 2021/03/02 (火)
- 13:45-17:00
Clojureの2つの重要な概念、簡潔さと力。
なぜClojureか
- 関数型プログラミング状態と同一性から計算を切り離すことで単純さを実現
- 関数型で書かれたプログラムは理解しやすく、テストしやすく、最適化、並列化がしやすい。
- JVM上で走るので、ClojureからJavaを利用することができる
- Lispは評価と読み込みが切り離されている意味で簡潔。また、実行時にコンパイラとマクロシステムを利用できるので、DSLを簡単に作れる(?)
- Clojureの時間のモデルが、値、同一性、状態、時間を切り離して扱うことで単純になっている
- プロトコルは多態を派生から切り離すことで単純さを提供する。型や抽象化を後から安全に拡張ができる
- Swiftにprotocolってあった気がするけど、それに近いかな?
ではなぜLispなのか
ClojureはLispのエッセンスを継承している。
言語に拡張が必要なことが多い。
- privateなフィールドが「プロダクトコード内では、privateだけれど、シリアライゼーションやユニットテストからはアクセス可能である」という再定義が必要
これらの拡張は、言語実装者にお願いする必要があるが、
Lispやそれを継承しているClojureなら、マクロを使って追加することができる。
それは本当にすごい。
それができるだけでも、Clojureを使う価値はある気がする
REPL環境でいくつか試す。
(4 / 2)
Execution error (ClassCastException) at clojure-study.core/eval1570 (form-init7809458444023561535.clj:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
(/ 4 2)
=> 2
中置演算は駄目?
(println "hello world")
hello world
=> nil
printlnした結果、hello worldという値は出たけど、戻り値としてはnilとなっている。
(defn hello [name] (str "Hello, " name))
=> #'clojure-study.core/hello
defn
が関数定義
[name]
が引数。← 特に型指定は無い?
str
は与えられた引数をすべて文字列にする
関数定義の戻り値には、clojure-study.coreというファイル上でREPLを起動したから、その中の名前空間でのvarに格納されているっぽい。
関数実行すると、以下のように結果が返ってくる
(hello "std")
=> "Hello, std"
(hello "Clojure")
=> "Hello, Clojure"
関数は()
で囲う必要があるみたい。
囲わなかった場合は、objectの参照位置を返しているっぽい?
hello "stu"
=> #object[clojure_study.core$hello 0x430274ca "clojure_study.core$hello@430274ca"]
=> "stu"
stacktrace を確認したい場合は、 pst
を使う
(0 / 1)
Execution error (ClassCastException) at clojure-study.core/eval1592 (form-init7809458444023561535.clj:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
(pst)
ClassCastException class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
clojure-study.core/eval1592 (form-init7809458444023561535.clj:1)
clojure-study.core/eval1592 (form-init7809458444023561535.clj:1)
clojure.lang.Compiler.eval (Compiler.java:7177)
clojure.lang.Compiler.eval (Compiler.java:7132)
clojure.core/eval (core.clj:3214)
clojure.core/eval (core.clj:3210)
clojure.main/repl/read-eval-print--9086/fn--9089 (main.clj:437)
clojure.main/repl/read-eval-print--9086 (main.clj:437)
clojure.main/repl/fn--9095 (main.clj:458)
clojure.main/repl (main.clj:458)
clojure.main/repl (main.clj:368)
nrepl.middleware.interruptible-eval/evaluate (interruptible_eval.clj:79)
=> nil
swap!
など、 !
がついた名前は慣習的に、状態を変える関数である
#{}
は空のセットの、リテラル表現
Clojureでは変数? の値を確認するためにリファレンス(ref)を使う。
最も単純なのがatom。
さらに名前をつけるために、defを使う
(def visitors (atom #{}))
=> #'clojure-study.core/visitors
参照するためには、 deref
を使用する。
略記として @
が使用できる
(deref visitors)
=> #{"Aaron" "Rich"}
@visitors
=> #{"Aaron" "Rich"}
状態の問題は、それまでにどんな手順で操作したかによって結果が異なってくる。
純粋な関数であれば、与えられた引数によって結果は毎回同じである。
そこに状態が絡まってくると、そこに至るまでの履歴を理解しないといけない。
状態とロジックはまた別で分離をするべきだろう
Clojureの場合は、
状態が必要だと感じた場合には、アトムなどのリファレンスを使って状態管理をする。
アトムなどのリファレンスは、マルチスレッド、マルチコアで使っても安心らしい。しかもロックを使わなくてもいいらしい
↑ここがClojureの肝みたいな感じがする。
状態とロジックを分離する。
そのための仕組みでアトムなどのリファレンスというものがある。
1.3章
- 2021/03/03(水)
- 10:30- 11:10
(doc name
)でドキュメント確認できる
※docはClojureのマクロらしい
doc str)
-------------------------
clojure.core/str
([] [x] [x & ys])
With no args, returns the empty string. With one arg x, returns
x.toString(). (str nil) returns the empty string. With more than
one arg, returns the concatenation of the str values of the args.
1行目:名前空間付き関数のフルネーム
2行目:コードから抽出した取りうる引数リスト
- この場合は、空か、文字列1つか、文字列 と、文字列リスト
- おそらくysは、文字列リストだと思われる。 Haskellとかでも似たような記法を見た。
- ここでの
[]
とは、たぶん空白のこと。空リストではない。(であれば、ysとかって表記しないだろうし)
引数名の慣習
- a:Java配列
- agt:エージェント
- coll:コレクション
- expr:式
- f:関数
- idx:インデックス
- r:リファレンス(ref)
- v:ベクタ
- val:値
(find-doc "reduce") みたいに、引数で指定した文字列にマッチする関数を検索することができる
(clojure.repl/source a-symbol) を使うと、指定した関数のソースを見ることが出来る
(clojure.repl/source identity)
(defn identity
"Returns its argument."
{:added "1.0"
:static true}
[x] x)
=> nil
ちょっとした気付きだけど、a-symbol
と言っているのは、シンボル。関数名とかなので、ダブルクォーテーションで囲む必要性はない。
【疑問】docで見ても引数の型とかは表示されないけど、Clojureは型って概念が無いんだっけ?
2章 Clojureひとめぐり
- 2021/03/03(水)
- 11:10-
Clojureの機能
- フォーム
- リーダマクロ
- 関数
- 束縛と名前空間
- フロー制御
- メタデータ
同図像性というのを初めて知った。
本書「Clojureのコードは、Clojureのデータによって組み立てられるということ」
Wiki「ある言語のコードをその言語で操作できる場合、その言語は同図像性があるという。 『プログラムコードをデータとして扱う』と呼ばれることもある」
[1,2,3]
=> [1 2 3]
[1 2 3]
=> [1 2 3]
結果同じになるのか。というかカンマ要らないんだね
算術演算子も、前置記法にしいてるメリットは、可変長の引数を取れるように拡張ができるというもの
(+ 1 2 3)
=> 6
これが中置記法だったら、 1 + 2 + 3
だから面倒ではある。
(+)
=> 0
引数が0だと、0と返ってくる。これによって、引数が無いパターンといった境界条件を特別扱いする必要がなくなって、バグの入り込む余地が減る、とのこと
Clojureの中のリーダ
と呼ばれる部分が、プログラムをフォーム
と呼ばれる単位ごとに読み込んで、それをClojureのデータ構造へと変換する。
それから、Clojureはそのデータ構造をコンパイルして実行する。
フォーム
は単位?
シンボルはフォームであり、文字列と文字もフォームである。
(.toUpperCase "hello")
のように、前のドットによって、Javaメソッドの呼び出しであると判断される。
文字リテラルのリスト(LazySeq)が返ってくる
(interleave "Attack at midnight" "The purple elephant chortled")
=>
(\A
\T
\t
\h
\t
\e
\a
\space
…
)
これを、strで1つの文字列にしようとすると、引数のリストをまとめて1つの引数として扱ってしまう。
(str (interleave "Attack at midnight" "The purple elephant chortled"))
=> "clojure.lang.LazySeq@d4ea9f36"
apply関数を使うことで、適用する関数と引数のリストを渡して、引数リストへと分解してくれる。
(apply str (interleave "Attack at midnight" "The purple elephant chortled"))
=> "ATthtea cpku raptl em iedlneipghhatn"
bool値の条件
(if () "true" "false")
=> "true"
(if 0 "true" "false")
=> "true"
; 述語関数。慣習的に? を付けている
; if では () と 0はtrue扱いだが、 true? は trueかどうかをチェックするだけなので、ここではfalseとなる
(true? ())
=> false
(true? 0)
=> false
Clojureはカンマを空白として扱う。
見やすさ重視で置けるやつ
【疑問】REPLでdefで同じシンボル名でやり直すと、値を変えることができちゃうが、これは不変性を残っていないか? ソースコード上ではできないようになっているのかな?
値オブジェクト的なものを、defrecordで設定ができる。
(defrecord Book [title author])
=> clojure_study.core.Book
(->Book "title" "author")
=> #clojure_study.core.Book{:title "title", :author "author"}
(def b (->Book "Anathem" "Neal Stephenson"))
=> #'clojure-study.core/b
b
=> #clojure_study.core.Book{:title "Anathem", :author "Neal Stephe"}
{}
でマップが定義できる。
マップのキー、バリューは何でも設定していいらしいが、
キーはkeyword(:
から始まるやつ)を使うといい。
(def inventors {:Lisp "McCarthy" :Clojure "Hickey"})
=> #'clojure-study.core/inventors
(inventors :Clojure)
=> "Hickey"
(:Clojure inventors)
defrecordで定義すると、中身のプロパティ?(title、author)はkeyword扱いになっている。
->Book
のあとに引数を指定すると、それに対応したマップが出来上がっている。
(defrecord Book [title author])
=> clojure_study.core.Book
(->Book "title" "author")
=> #clojure_study.core.Book{:title "title", :author "author"}
以下の書き方でもOK
(Book. "title" "author")
keywordを指定すれば値が取れるので、defrecordで定義した値も取れる
(def c (->Book 1 "abc"))
=> #'clojure-study.core/c
c
=> #clojure_study.core.Book{:title 1, :author "abc"}
(:title c)
=> 1
(:author c)
=> "abc"
マップ、関数も格納できるっぽいので、こういう使い方もできた
(defn helloworld
[x]
(println x "Hello, world!"))
=> #'clojure-study.core/helloworld
(helloworld 1)
1 Hello, world!
=> nil
(def a (->Book helloworld "test"))
=> #'clojure-study.core/a
a
=>
#clojure_study.core.Book{:title #object[clojure_study.core$helloworld
0x27601a25
"clojure_study.core$helloworld@27601a25"],
:author "test"}
((:title a) 1)
1 Hello, world!
=> nil
リーダマクロとマクロは全く違うものらしい。
;
コメント行が、一番よく使われるリーダマクロ。
'
評価抑制のシングルクォーテーション。
Clojureでは、プログラムが新しいリーダマクロを定義する方法を提供していない。
カスタムリーダマクロは複雑にするので、柔軟性を多少犠牲にしてでもClojureのコアを安定させるようにする選択だったみたい
Clojureの関数呼び出しは、単に最初の要素が関数を指すようなリストである
言われてみればたしかに!
引数なしと引数ありで動作を変えた関数定義をすることができる。
パターンマッチに近いのかな? ガード節はそのままじゃ使えないっぽいけど。
(defn greeting
"Returns a greeting of the from 'Hello, username.'
Default username is 'world'."
([] (greeting "world"))
([username] (str "Hello, " username)))
guard
なるものがあったが使い方がわからなかった。
condでも似たようなことが出来たので、これでよしとする
(defn fizz-buzz
[x]
(cond
(= 0 (rem x 15)) "fizzbuzz"
(= 0 (rem x 3)) "fizz"
(= 0 (rem x 5)) "buzz"
:else (str x)))
引数リストに &
を含めると、後の仮引数に、実引数に束縛されなかったもののシーケンスが束縛される。
; 可変長引数の関数定義
(defn date [person-1 person-2 & chaperones]
(println person-1 "and" person-2
"went out with" (count chaperones) "chapersones."))
(date "Romeo" "Juliet" "Friar Lawrence" "Nurse")
Romeo and Juliet went out with 2 chapersones.
無名関数
(fn [params*] body)
(filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+"))
-> ("fine" "day")
let で束縛。
lexical :語彙、辞書、辞書的な意味らしい
(defn indexable-word [text]
; このindexable-word関数内で、indexable-word? を束縛している
(let [indexable-word? (fn [w] (> (count w) 2))]
(filter indexable-word? (str/split text #"\W+"))))
無名関数の使いどころ
無名関数はとても短くかけるけれど、とにかく使えばよいというものでもない。
indexable-word? のようにきちんと名前を付けた関数にしておくほうが好ましいと思うこともあるだろう。それはそれで構わないし、indexable-word?が別の場所でも呼ばれるようなら名前を付けておくほうが正しい設計だ。
無名関数は必要なら使えるというだけで、何がなんでも使うべきというものではない。無名関数を使ったほうがコードが読みやすくなると感じたときだけ使えばいい
冗長だなって思った時が使いどころって感じだろうか。
あとは、その関数内でしか使わないような小さな関数とか。
【疑問】"foo"と:fooはどちらも評価されると自分自身を返すのでは? なんて思ったりする。
型は確認すると違う
(class :foo)
=> clojure.lang.Keyword
(class "foo")
=> java.lang.String
実際、自分自身を返すという動きはリテラルでも同じっぽい。
keywordはsymbolみたいにnamespaceを持ったりできるので、
動きとしてはsymbolに近い構造なんだけど、symbolみたいに他者を参照するみたいな機能が無い、ということなんかな。
【疑問】
結局フォームってなんだろう?
Docker環境を立てる
straceコマンドを打ちたいが為に、ちょっと寄り道。
# clojure image pull
docker pull clojure
# clojureプロジェクトのルートにcdしてから、実行
docker run -it --rm -w /home/ --mount type=bind,source=$(pwd),target=/home/ clojure bash
コンテナの中での設定
apt-get update && apt-get install -y strace && apt-get install -y less
strace付きでmain関数実行
strace lein run
REPL駆動開発。
REPLが実行環境で、そこでアプリ開発ができる…?
ライブラリのインポート。
名前空間付きで、名前を指定するのが面倒な場合はreferを作る
(require 'clojure-study.exploring)
(refer 'clojure-study.exploring)
束縛
var は名前に束縛されているが、他の種類の束縛もある。
たとえば関数を呼び出すと、実引数の値と仮引数の名前が束縛される
(defn triple [number] (* 3 number))
; この呼び出しの際に、実引数10と、triple関数内のnumberという名前が束縛されている
(triple 10)
関数の引数束縛はレキシカルなスコープを持つ。関数本体の中だけから見えるということだ。
レキシカルってなんだ?
静的スコープの別名? みたいなものらしい。
; 正方形の各頂点の座標を計算する
; topとrightは2回ずつ使われるのでletで一度束縛している
(defn square-corners [bottom left size]
(let [top (+ bottom size)
right (+ left size)]
[[bottom left] [top left] [top right] [bottom right]]))
(square-corners 1 2 3)
=> [[1 2] [4 2] [4 5] [1 5]]