Open45

Clojure学習メモ

jnuankjnuank

1章

  • 2021/03/02 (火)
  • 13:45-17:00
jnuankjnuank

Clojureの2つの重要な概念、簡潔さと力。

なぜClojureか

  • 関数型プログラミング状態と同一性から計算を切り離すことで単純さを実現
    • 関数型で書かれたプログラムは理解しやすく、テストしやすく、最適化、並列化がしやすい。
  • JVM上で走るので、ClojureからJavaを利用することができる
  • Lispは評価と読み込みが切り離されている意味で簡潔。また、実行時にコンパイラとマクロシステムを利用できるので、DSLを簡単に作れる(?)
  • Clojureの時間のモデルが、値、同一性、状態、時間を切り離して扱うことで単純になっている
  • プロトコルは多態を派生から切り離すことで単純さを提供する。型や抽象化を後から安全に拡張ができる
    • Swiftにprotocolってあった気がするけど、それに近いかな?
jnuankjnuank

ではなぜLispなのか

ClojureはLispのエッセンスを継承している。

言語に拡張が必要なことが多い。

  • privateなフィールドが「プロダクトコード内では、privateだけれど、シリアライゼーションやユニットテストからはアクセス可能である」という再定義が必要

これらの拡張は、言語実装者にお願いする必要があるが、
Lispやそれを継承しているClojureなら、マクロを使って追加することができる。

それは本当にすごい。
それができるだけでも、Clojureを使う価値はある気がする

jnuankjnuank

セットアップ

  • IntelliJにCursiceをインストール
  • この記事を参考にして、Leiningenをインストール
mkdir ~/.lein; cd $_
curl -L -O https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
chmod a+x lein
  • IntelliJから、Clojure -> Leiningenで Create Project。Leiningenのパス指定とかはしなかったから、そこはよしなにIntelliJがやってくれている?

jnuankjnuank

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
jnuankjnuank

swap! など、 ! がついた名前は慣習的に、状態を変える関数である

jnuankjnuank

Clojureでは変数? の値を確認するためにリファレンス(ref)を使う。
最も単純なのがatom。
さらに名前をつけるために、defを使う

(def visitors (atom #{}))
=> #'clojure-study.core/visitors

参照するためには、 derefを使用する。
略記として @が使用できる

(deref visitors)
=> #{"Aaron" "Rich"}
@visitors
=> #{"Aaron" "Rich"}
jnuankjnuank

状態の問題は、それまでにどんな手順で操作したかによって結果が異なってくる。
純粋な関数であれば、与えられた引数によって結果は毎回同じである。

そこに状態が絡まってくると、そこに至るまでの履歴を理解しないといけない。

状態とロジックはまた別で分離をするべきだろう

Clojureの場合は、
状態が必要だと感じた場合には、アトムなどのリファレンスを使って状態管理をする。
アトムなどのリファレンスは、マルチスレッド、マルチコアで使っても安心らしい。しかもロックを使わなくてもいいらしい

↑ここがClojureの肝みたいな感じがする。

状態とロジックを分離する。
そのための仕組みでアトムなどのリファレンスというものがある。

jnuankjnuank

1.3章

  • 2021/03/03(水)
  • 10:30- 11:10
jnuankjnuank

(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とかって表記しないだろうし)
jnuankjnuank

引数名の慣習

  • a:Java配列
  • agt:エージェント
  • coll:コレクション
  • expr:式
  • f:関数
  • idx:インデックス
  • r:リファレンス(ref)
  • v:ベクタ
  • val:値
jnuankjnuank

(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は型って概念が無いんだっけ?

jnuankjnuank

2章 Clojureひとめぐり

  • 2021/03/03(水)
  • 11:10-

Clojureの機能

  • フォーム
  • リーダマクロ
  • 関数
  • 束縛と名前空間
  • フロー制御
  • メタデータ
jnuankjnuank

同図像性というのを初めて知った。

https://ja.wikipedia.org/wiki/同図像性

本書「Clojureのコードは、Clojureのデータによって組み立てられるということ」
Wiki「ある言語のコードをその言語で操作できる場合、その言語は同図像性があるという。 『プログラムコードをデータとして扱う』と呼ばれることもある」

jnuankjnuank
[1,2,3]
=> [1 2 3]
[1 2 3]
=> [1 2 3]

結果同じになるのか。というかカンマ要らないんだね

jnuankjnuank

算術演算子も、前置記法にしいてるメリットは、可変長の引数を取れるように拡張ができるというもの

(+ 1 2 3)
=> 6

これが中置記法だったら、 1 + 2 + 3 だから面倒ではある。

(+)
=> 0

引数が0だと、0と返ってくる。これによって、引数が無いパターンといった境界条件を特別扱いする必要がなくなって、バグの入り込む余地が減る、とのこと

jnuankjnuank

Clojureの中のリーダと呼ばれる部分が、プログラムをフォームと呼ばれる単位ごとに読み込んで、それをClojureのデータ構造へと変換する。
それから、Clojureはそのデータ構造をコンパイルして実行する。

フォームは単位?

jnuankjnuank

シンボルはフォームであり、文字列と文字もフォームである。

jnuankjnuank

(.toUpperCase "hello") のように、前のドットによって、Javaメソッドの呼び出しであると判断される。

jnuankjnuank

文字リテラルのリスト(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"
jnuankjnuank

bool値の条件

(if () "true" "false")
=> "true"
(if 0 "true" "false")
=> "true"

; 述語関数。慣習的に? を付けている
; if では () と 0はtrue扱いだが、 true? は trueかどうかをチェックするだけなので、ここではfalseとなる
(true? ())
=> false
(true? 0)
=> false
jnuankjnuank

Clojureはカンマを空白として扱う。
見やすさ重視で置けるやつ

jnuankjnuank

【疑問】REPLでdefで同じシンボル名でやり直すと、値を変えることができちゃうが、これは不変性を残っていないか? ソースコード上ではできないようになっているのかな?

jnuankjnuank

値オブジェクト的なものを、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"}
jnuankjnuank

{}でマップが定義できる。

マップのキー、バリューは何でも設定していいらしいが、
キーはkeyword(: から始まるやつ)を使うといい。

(def inventors {:Lisp "McCarthy" :Clojure "Hickey"})
=> #'clojure-study.core/inventors
(inventors :Clojure)
=> "Hickey"
(:Clojure inventors)
jnuankjnuank

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")
jnuankjnuank

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
jnuankjnuank

リーダマクロとマクロは全く違うものらしい。

; コメント行が、一番よく使われるリーダマクロ。
' 評価抑制のシングルクォーテーション。

Clojureでは、プログラムが新しいリーダマクロを定義する方法を提供していない。
カスタムリーダマクロは複雑にするので、柔軟性を多少犠牲にしてでもClojureのコアを安定させるようにする選択だったみたい

jnuankjnuank

Clojureの関数呼び出しは、単に最初の要素が関数を指すようなリストである

言われてみればたしかに!

jnuankjnuank

引数なしと引数ありで動作を変えた関数定義をすることができる。

パターンマッチに近いのかな? ガード節はそのままじゃ使えないっぽいけど。

(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)))
jnuankjnuank

引数リストに & を含めると、後の仮引数に、実引数に束縛されなかったもののシーケンスが束縛される。

; 可変長引数の関数定義
(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.
jnuankjnuank

無名関数

(fn [params*] body)

(filter (fn [w] (> (count w) 2)) (str/split "A fine day" #"\W+"))
-> ("fine" "day")
jnuankjnuank

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+"))))
jnuankjnuank

無名関数の使いどころ

無名関数はとても短くかけるけれど、とにかく使えばよいというものでもない。
indexable-word? のようにきちんと名前を付けた関数にしておくほうが好ましいと思うこともあるだろう。それはそれで構わないし、indexable-word?が別の場所でも呼ばれるようなら名前を付けておくほうが正しい設計だ。
無名関数は必要なら使えるというだけで、何がなんでも使うべきというものではない。無名関数を使ったほうがコードが読みやすくなると感じたときだけ使えばいい

冗長だなって思った時が使いどころって感じだろうか。
あとは、その関数内でしか使わないような小さな関数とか。

jnuankjnuank

【疑問】"foo"と:fooはどちらも評価されると自分自身を返すのでは? なんて思ったりする。

型は確認すると違う

(class :foo)
=> clojure.lang.Keyword
(class "foo")
=> java.lang.String
jnuankjnuank

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
jnuankjnuank

ライブラリのインポート。
名前空間付きで、名前を指定するのが面倒な場合はreferを作る

(require 'clojure-study.exploring)
(refer 'clojure-study.exploring)
jnuankjnuank

束縛

var は名前に束縛されているが、他の種類の束縛もある。
たとえば関数を呼び出すと、実引数の値と仮引数の名前が束縛される

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

; この呼び出しの際に、実引数10と、triple関数内のnumberという名前が束縛されている
(triple 10)

関数の引数束縛はレキシカルなスコープを持つ。関数本体の中だけから見えるということだ。

レキシカルってなんだ?
静的スコープの別名? みたいなものらしい。

https://ja.wikipedia.org/wiki/静的スコープ

; 正方形の各頂点の座標を計算する
; 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]]