Clojureキャッチアップの旅
目的
Clojure使いこなしたいのでキャッチアップをする。
気ままに行う予定なので、雑多な内容になると思われます。
書籍
キャッチアップするにあたって買った本を虫干し。
関数型デザイン
偉大なるアンクル・ボブの書いた本。
Clojureを使って、OOPと関数型の対比を行うことによって関数型を学ぶ本。
だと思う。まだ読み始めたばかり。
プログラミングClojure 第2版
Clojureの解説本。
おそらく日本語で詳しいのはこの本だけ?
翻訳は、Gauche Schemeの作者であり、Lisp界隈に名のしれた川合四郎さん。
聖典、Land of Lispの翻訳者でもある。
つまり、この本も実質聖典。
まだ読み始めたばかり。
追記:第2版はversion1.3の解説らしい。
第3版は1.9に対応しているらしいが、翻訳は出てない。
1.3から1.9への変更点を書いた記事があったのでこれでお茶を濁そうかな。
おいしいClojure入門
Cojureでいろいろなことができることを示したある種のレシピ本。
いろいろなライブラリを使用して、いろいろなことを実現しているから眺めているだけでも楽しい。
まだ読み始めたばかり。
7つの言語7つの世界
パラダイムの違う7つの言語を見ることで、プログラミングの奥深い世界を知る本。
「Clojureの解説もあるし、他の言語との対比も書かれてるかなー?」と思って購入。
まだ読み始めたばかり。
データ指向プログラミング
データ指向プログラミングの解説本。
Clojureが話に出てきた覚えがあるので、ここに挙げた。
読みさして止まっているので、この気に読み込んでいきたい。
未読
The Joy of Clojure
記事で気になっているもの
ゼロからClojure入門した振り返り
関連リンク集
ClojureによるWeb開発
末尾再帰最適化
マクロ
オブジェクト指向
アルゴリズム
クイックソート
ライブラリなどに関して気になるもの
型validate
文字列操作
Kindle本
Kindleで出ていて気になった電子書籍の虫干し。
まだ購入していない。
動画
Clojureに関する動画がまとまっているチャンネル。
まだ見ていないが、Clojure作者のRich Hickeyさんによる講演動画もあるっぽい。
自分をClojure化する方法
Clojureのイントロダクション動画。
Clojureの魅力を楽しく解説している動画。
なお、「タイトルは釣りです」とのこと。
2010年の動画だが、色褪せない魅力があると思う。
Clojureによるバイトコードプログラミング
発表者自作のインラインアセンブラマクロであるiasmの話。
まだインラインアセンブラマクロがどのようなものか理解していないが、挑戦として面白いと思う。
気になっているもの
まだ見ていないものをメモとして。
見たら、順次簡単な解説を書こうと思う。
Leiningenを使ってHello world
Leiningenを使ってHello worldを行う。
Leiningenはデファクトスタンダードのプロジェクト管理ツールらしい。
プロジェクト作成したら基本的なものを作成してくれる。
lein new app hello-clojure
ディレクトリを移動して実行してみよう。
cd hello-clojure
lein run
# Hello, World!と表示される。
プロジェクトのディレクトリ構成
Leiningenを使って、デフォルトで作成されるのディレクトリ構成。
.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│ └── intro.md
├── project.clj
├── resources
├── src
│ └── hello_clojure
│ └── core.clj
├── target
│ ├── classes
│ │ └── META-INF
│ │ └── maven
│ │ └── hello-clojure
│ │ └── hello-clojure
│ │ └── pom.properties
│ └── stale
│ └── leiningen.core.classpath.extract-native-dependencies
└── test
└── hello_clojure
└── core_test.clj
14 directories, 9 files
本体のコード
Hello worldを出力してくれるが、どのような関数となっているのか。
hello-clojure/src/hello_clojure/core.clj
の中身を見てみる。
(ns hello-clojure.core
(:gen-class))
(defn -main
"I don't do a whole lot ... yet."
[& args]
(println "Hello, World!"))
よくわからないので、ChatGPTに聞いてみる。
VSCodeにいれたプラグイン
VSCodeを主に使用しているので、プラグインを入れていく。
何をいれるかもわかってないので、とりあえず動かしてみて、入れ替わり立ち替わりしてみる。
Calva
snippets
Linter
Calvaの使い方についての調査
Calvaの使い方のイメージが沸かなかったので、以下の動画を視聴。
Calvaを使うとREPLサーバーが立ち上がり、ファイルに書き込んだ内容を実行することが可能らしい。
つまり、
- ファイルへのコードの書き込み
- 書いたコードをREPLみたいに、その場で実行して確かめることができる
という、REPLとファイルにコードを書くことのいいとこどりのような形式で使えるらしい。
なにそれ最強じゃんね。
CalvaTVチャンネル
探してる途中で見つけたので、メモとして記載。
Calvaを使ってみる
Calvaを使って簡単なコードを書いてみるテスト。
起動の仕方
まずはREPLサーバーの起動の仕方。
コマンドパレットを開き、Calva: Start a Project REPL and Connect
を選択。
REPLを実行したいディレクトリを選択する。
今回はPractice-Clojure/hello-clojure
が該当。
ProjectのTypeを選択する。
よくわかっていないが、Leiningenを導入しているのでそれを選択。
すると、REPLが起動する。
起動したREPLのファイルは特にいじらないでOK。
あとは、コードを書く方のcljファイルにコードを書いていけば良い。
実行の仕方
コードを書いた行の末尾で、Cmd + Enter
(Mac)あるいはCtrl + Enter
(Windows)を入力すれば実行できる。
試しに実行している様子。
なお、行の中間などで実行した場合は、シンタックスエラーが出るようだ。
カーソルの位置が重要みたい。
Hello,
のスペースのあたりで実行した場合こうなった。
終了の仕方
REPLサーバーを終了させる方法。
コマンドパレットを開き、Calva: Disconnect from the REPL Server
を選択する。
fizzbuzzを書いてみる
とりあえずAIに聞きながらfizzbuzzを書いてみる。
(defn fizzbuzz [n]
(cond
(and (zero? (mod n 3)) (zero? (mod n 5))) "FizzBuzz"
(zero? (mod n 3)) "Fizz"
(zero? (mod n 5)) "Buzz"
:else (str n)))
(doseq [i (range 20)]
(println (fizzbuzz i)))
Clojureでは副作用が無いコードが推奨されるらしいので、その形で書いた。
副作用のない形で関数を書くのは馴染がないため、今度も注意して実装していきたい。
Calvaの開発体験
開発体験かなりいい。
何がいいかというと、「本番用のファイルに実験コードを書いて実行できる」ということ。
Calvaでは、コードを書いて、その場で実行ができる。
つまり、REPLでコードを実行するのと体験が変わらない。
しかも、その書いたコードはファイルに残る。
つまり、REPLで実行することとファイルにコードを書くことを、両立して行える。
具体的に何が嬉しいかというのを一つのケースで示す。
- 開発中に、いままで使ったことの無い関数を使う必要が出てきた。
- 関数の動きを確かめるコードを書いて、関数の使い方を学ぶ。
- 使い方がわかった関数で、必要なコードを書く。
という流れになると思うが、従来、自分は以下のように行っていた。
- 別ファイルや実験用のプロジェクトを作成し、動きを確かめるコードをそこに書いて確かめる。
- 本番用のファイルに、必要なコードを書く。
という感じで、ファイルを行きつ戻りつしており、集中が途切れること甚だしかった(ワーキングメモリーが小さいのでわりと困っていた)。
Calvaを使うと、同じファイル内で完結できる。
- 同じファイルに実験用のコードを書いて動きを確かめる。
- 本番用のコードを、そのまま同じファイルに書く。
- 不必要になった実験用のコードを削除する。
これはかなりいい。
自分が探してきた理想の開発方法かもしれない。
追記
こんな記事を見つけた。
Clojureでの開発方法はREPL駆動開発というらしい。
Calvaでやってることが、その考え方の一つの実装と言えるのかも。
テストの方法
テストのやり方を書く。
テストコードを書く
テストコードを書く。
test
ディレクトリの中にあるファイルに記載する。
今回で言えば、以下のディレクトリにあるcore_test.clj
に記載する。
# ディレクトリは一部のみ抜粋しているので、実際のディレクトリ構成とは異なることに注意
├── src
│ └── hello_clojure
│ └── core.clj
└── test
└── hello_clojure
└── core_test.clj
今回はfizzbuzz関数のテストコードを書いている。
(ns hello-clojure.core-test
(:require [clojure.test :refer :all]
[hello-clojure.core :refer :all]))
(deftest fizzbuzz-test
(testing "FizzBuzzの動作")
(is (= "Fizz" (fizzbuzz 3)))
(is (= "Buzz" (fizzbuzz 5)))
(is (= "FizzBuzz" (fizzbuzz 15)))
(is (= "2" (fizzbuzz 2))))
CalvaのREPLにロードする
書いたテストファイルをREPL上にロードする必要がある。
ショートカットキーでも可能らしいが、vimキーバインドと競合していたため、コマンドパレットを用いた方法で実行してみる。
コマンドパレットを開き、Calva: Load/Evaluage Current File and its Requires/Dependencies
を選択する(画像は表示の関係上見切れている)。
REPL側で読み込まれていることが確認できれば成功。
テストの実行
コマンドパレットを開き、Calva: Run Current Test
を選択。
REPLでテストが実行されていることを確認する。
追記補足
設定をいじったら、クリックだけで実行できるようになったので、メモ。
VSCodeの以下の項目にチェックを入れる。
すると、他の言語ではおなじみの、テスト実行ボタンが出現する。
注意点として、core.cljの実行しか成功しなかったこと(同じネームスペース内にテスト対象の関数とテストコードが併存している必要がある?)。
core_test.cljでの実行が失敗しているので、これについては今後の課題。
テストの方法2
ファイルを分けるのではなく、同一ファイル内に一旦書くことはできないだろうか?
Rustだと同一ファイル内にテストコードを書けるので、書けないかなと。
CalvaのRefresh
先ほど読み込んだテストコードをアンロードする必要がある。
しかし、個別のアンロード機能は無いらしい(AI情報。裏取りはまだしていない)ので、全体読み込み直す必要がある。
コマンドパレットから、Calva: Refresh All Namespaces
を選択する。
すると、Calvaが読み込み直しされた結果でStopするようなので、再度Calvaを立ち上げ直す。
やり方は以前に書いているので省略。
REPLサーバーを立ち上げるときと全く一緒。
同一ファイルにテストコードを書く
関数コードと同じファイルにテストコードを記載する。
ポイントとしては、インポートにテストライブラリを追加するくらい。
(ns hello-clojure.core
(:gen-class)
(:require [clojure.test :refer :all]))
(defn -main
[& args]
(println "Hello, Clojure!"))
(defn fizzbuzz [n]
(cond
(and (zero? (mod n 3)) (zero? (mod n 5))) "FizzBuzz"
(zero? (mod n 3)) "Fizz"
(zero? (mod n 5)) "Buzz"
:else (str n)))
(doseq [i (range 20)]
(println (fizzbuzz i)))
(deftest fizzbuzz-test
(testing "FizzBuzzの動作")
(is (= "Fizz" (fizzbuzz 3)))
(is (= "Buzz" (fizzbuzz 5)))
(is (= "FizzBuzz" (fizzbuzz 15)))
(is (= "2" (fizzbuzz 2))))
テストの実行
テストの実行は、ファイルを分けたときと同じくCalva: Run Current Test
でOK。
REPLへのロードは特に必要なかったので、おそらくRefresh時に読み込まれているのではないかと思う。
テストコードの更新
テストコードを更新したら、REPLに読み込ませる必要がある。
これは通常のREPL実行のショートカットキーと同様に、Control + Enter
でできるようだ。
なお、これは別ファイルに分けた場合もできることが確認できた。
更新後は、通常のテスト実行と同様に行えば良い。
別ファイルに移動
満足するまでテストできたら、別ファイルに移動したらいいのかなと思う。
本来、そこに書かれるのが正しいだろうし。
課題
後々のTodo。
- アンロードと再起動がめんどくさい。一つのコマンドで実行できないか。
- できればコマンドパレットではなく、ショートカットキーなどで手軽に実行したい。
TDD
TDDでなにか実装してみる。
簡単な四則演算アプリを実装してみる。
これに関しては結果だけ貼っておく。
(ns hello-clojure.core
(:gen-class)
(:require [clojure.test :refer :all]))
(defn calculation [op a b]
(cond
(= op "+") (+ a b)
(= op "-") (- a b)
(= op "*") (* a b)
(= op "/") (cond
(= b 0) (str "Division by 0 is not allowed.")
:else (/ a b))
:else "The operator is incorrect."))
(deftest calculation-test
(testing "四則演算関数のテスト"
(is (=(calculation "+" 1 2) 3))
(is (=(calculation "-" 2 1) 1))
(is (=(calculation "*" 2 3) 6))
(is (=(calculation "/" 10 2) 5))
(is (=(calculation "/" 2 10) 2/10))
(is (=(calculation "/" 10 0) "Division by 0 is not allowed."))
(is (=(calculation "n" 1 2) "The operator is incorrect."))
))
リファクタリング
Clojure流に書けているかが気になったので、AIに聞いてみて改良する試み。
AI回答のポイントとしては、
- 演算子をHashMapにしてはどうか
- ハッシュマップにない場合のエラーを表示する形にしてはどうか
といった形であった。
AIに言われたそのままの形であるが、以下のように変更。
(defn calculation
[op a b]
(let [operations {"+" +
"-" -
"*" *
"/" (fn [a b] (if (= b 0)
"Division by 0 is not allowed."
(/ a b)))}]
(if-let [operation (operations op)]
(operation a b)
"Invalid operator.")))
(deftest calculation-test
(testing "四則演算関数のテスト"
(is (=(calculation "+" 1 2) 3))
(is (=(calculation "-" 2 1) 1))
(is (=(calculation "*" 2 3) 6))
(is (=(calculation "/" 10 2) 5))
(is (=(calculation "/" 2 10) 2/10))
(is (=(calculation "/" 10 0) "Division by 0 is not allowed."))
(is (=(calculation "n" 1 2) "Invalid operator."))
))
さらに改良をお願いしたら、
- デフォルトの場合をバインドする
- テストに名前をつける
という話だったので、コードを変更。
(defn calculation
[op a b]
(let [operations {"+" +
"-" -
"*" *
"/" (fn [a b]
(if-not (= b 0)
(/ a b)
"Division by 0 is not allowed."))}
default-fn (fn [_ _] "Invalid operator.")]
((or (operations op) default-fn) a b)))
(deftest calculation-test
(testing "四則演算関数のテスト"
(is (=(calculation "+" 1 2) 3) "Addition test failed")
(is (=(calculation "-" 2 1) 1) "Subtraction test failed")
(is (=(calculation "*" 2 3) 6) "Multiplication test failed")
(is (=(calculation "/" 10 2) 5) "Division test failed")
(is (=(calculation "/" 2 10) 2/10) "Fraction division test failed")
(is (=(calculation "/" 10 0) "Division by 0 is not allowed.") "Zero division test failed")
(is (=(calculation "n" 1 2) "Invalid operator.") "Invalid operator test failed")
))
いままでのパラダイムと違うので、AIにバンバン質問しながらClojureのスタイルに慣れていきたい。
バブルソート実装
入門したてで右も左もわからないので、AIに聞きながら実装してみる。
TDDで実装。
空のリストを返すテストを書く
まず、空のリストをもらったときにそのまま返すテストを書く。
(deftest bubble-sort-test
(testing "バブルソート関数のテスト"
(is (= (bubble-sort []) []))))
空のリストを返す関数を書く
次に、空のリストを返す関数を書く。
(defn bubble-sort
[coll]
coll)
テストが通るのを確認してフィニッシュ。
要素一つの場合
要素を一つだけの場合を書いていく。
(deftest bubble-sort-test
(testing "バブルソート関数のテスト"
(is (= (bubble-sort []) []))
(is (= (bubble-sort [1]) [1]))))
実装の方は変える必要がないので、テストが通ることを確認したらフィニッシュ。
要素2つの場合
要素2つの場合。
まずテストコードを追加。
(deftest bubble-sort-test
(testing "バブルソート関数のテスト"
(is (= (bubble-sort []) []))
(is (= (bubble-sort [1]) [1]))
(is (= (bubble-sort [2 1]) [1 2]))
(is (= (bubble-sort [1 2]) [1 2]))))
ソートされてないものと、ソートしているものの両方のケースを追加。
さて、最小限度のコードで実装する。
(defn bubble-sort
[coll]
(if (= (count coll) 2)
(if (> (first coll) (second coll))
[(second coll) (first coll)]
coll)
coll))
テストが通ったことを確認してフィニッシュ。
要素が3つ以上の場合
要素が3つ以上の場合はちょっと飛躍がいる。
Clojureにふさわしい書き方で実装したいので、AIに聞きながら実装したものがこちら。
テスト含めて、以下のコードになった。
(defn bubble-sort-pass
[coll]
(loop [unsorted coll
result []]
(cond
(empty? unsorted) result
(empty? (rest unsorted)) (conj result (first unsorted))
:else
(let [[a b & rest] unsorted]
(if (> a b)
(recur (cons a rest) (conj result b))
(recur (cons b rest) (conj result a)))))))
(defn bubble-sort [coll]
(loop [coll coll]
(let [sorted (bubble-sort-pass coll)]
(if (= coll sorted)
sorted
(recur sorted)))))
(deftest bubble-sort-test
(testing "バブルソート関数のテスト"
(is (= (bubble-sort []) []))
(is (= (bubble-sort [1]) [1]))
(is (= (bubble-sort [2 1]) [1 2]))
(is (= (bubble-sort [1 2]) [1 2]))
(is (= (bubble-sort [2 1 3]) [1 2 3]))
))
選択ソート
選択ソートを実装してみる。
(defn selection-sort [coll]
(loop [unsorted coll
sorted []]
(if (empty? unsorted)
sorted
(let [smallest (apply min unsorted)
remainder (remove (fn [x] (= x smallest)) unsorted)]
(recur remainder (conj sorted smallest))))))
(deftest selection-sort-test
(testing "選択ソート関数のテスト"
(is (= (selection-sort []) []))
(is (= (selection-sort [1]) [1]))
(is (= (selection-sort [2 1]) [1 2]))
(is (= (selection-sort [1 2]) [1 2]))
(is (= (selection-sort [2 1 3]) [1 2 3]))
(is (= (selection-sort [3 2 1]) [1 2 3]))
(is (= (selection-sort [5 4 3 2 1]) [1 2 3 4 5]))))
modとremの違い
割り算用の関数にmodとremの2つが存在しており、違いわからなかったため色々調べてみた。
以下の記事に詳しかった。
しかし
remはわかる。
あまりが1で被除数の符号に揃えるということで理解ができた。
しかし、なぜmodだと2という数字が出てくるのだろうか。
不思議でならない。
;; modの場合
(mod 10 3)
;; 1
(mod -10 3)
;; 2
(mod 10 -3)
;; -2
;; remの場合
(rem 10 3)
;; 1
(rem -10 3)
;; -1
(rem 10 -3)
;; 1
AIにきいてみた
ググってもよくわからないので、AIに聞いてみた。
切り捨てしてるんだってさ(まだよくわかってない)。
modが正しい?
この記事を読む限り、modの方が正しそうに見える。
modのソースコード
modのソースコード中にremが使用されてると聞いたので、調査。
source関数を使用してmodのソースコードを表示する。
(source mod)
; (defn mod
; "Modulus of num and div. Truncates toward negative infinity."
; {:added "1.0"
; :static true}
; [num div]
; (let [m (rem num div)]
; (if (or (zero? m) (= (pos? num) (pos? div)))
; m
; (+ m div))))
肝は以下の部分だと思う。
; (if (or (zero? m) (= (pos? num) (pos? div)))
; m
; (+ m div))))
mが0、あるいはnumとdivの符号が一致する場合は、mをそのまま返している。
つまり、
- numが+、divが+
- numが-、divが-
- mが0
の場合はそのまま返す(ちなみに短絡評価である)。
+の場合はいいが、-同士の場合はどうだろうか。
(rem -10 -3)
;; -1
そして、それ以外のばあいは以下のコードが該当する。
; (+ m div))))
mにdivを足している。
つまり、考えられるケースは、
- numが-で、divが+
- numが+で、divが-
の2通り。
具体例で書くと
;; numが-で、divが+
(rem -10 3)
;; -1
;; numが+で、divが-
(rem 10 -3)
;; 1
これらの計算結果にdivを足すので、それぞれ以下のようになる。
(+ -1 3)
;; 2
(+ 1 -3)
;; -2
ソースコードは読み解けたが、なぜこのようにするかは謎のまま。
modの挙動について
数学のモジュロ演算に基づいているらしい。
剰余演算
wikiに表あり。
remのソースコード
参考だが、remのソースコードはこちら。
; (defn rem
; "remainder of dividing numerator by denominator."
; {:added "1.0"
; :static true
; :inline (fn [x y] `(. clojure.lang.Numbers (remainder ~x ~y)))}
; [num div]
; (. clojure.lang.Numbers (remainder num div)))
remのドキュメント
clojure.coreをみると、次のような記載がある。
;; rem and mod are commonly used to get the remainder.
;; mod means Knuth's mod (truncating towards negativity).
;; rem implements ANSI C's % operator
;; Absolute value stays the same, always the distance
;; towards zero.
;; sign depends on dividend.
modはドナルド・クヌース大先生のいうところの切り捨て法[1]らしい。
また、remはANSI Cの%[2]を実装したものらしい。
補足
clojure.coreにも色々書いてあるので、参考。
決定的にどう、というのは書いてなさそう?
-
wikiによるとこの本に記載があるらしい。https://tatsu-zine.com/books/taocp-vol1 ↩︎
-
ANSI C (ISO/IEC 9899:1990) に記載があるとのこと。 ↩︎
文字列操作
clojure.stringが文字列操作用のライブラリと聞いたのでメモ。
一つ一つ簡単に掘り下げていきたい。
別記事を立てて、そちらに記載した。
関数について調べる方法
以下の4つが使用できる。
- doc
- source
- find-doc
- apropos
doc
(doc mod)
; -------------------------
; clojure.core/mod
; ([num div])
; Modulus of num and div. Truncates toward negative infinity.
source
(source mod)
; (defn mod
; "Modulus of num and div. Truncates toward negative infinity."
; {:added "1.0"
; :static true}
; [num div]
; (let [m (rem num div)]
; (if (or (zero? m) (= (pos? num) (pos? div)))
; m
; (+ m div))))
find-doc
(find-doc "mod")
;; 表示多いため、一部のみ
; -------------------------
; clojure.test/set-test
; ([name & body])
; Macro
; Experimental.
; Sets :test metadata of the named var to a fn with the given body.
; The var must already exist. Does not modify the value of the var.
;
; When *load-tests* is false, set-test is ignored.
; -------------------------
; cider.nrepl.middleware.debug/skip-breaks!
; ([mode] [mode coor code force?])
; Set the value of *skip-breaks* for the top-level breakpoint.
; Additional arguments depend on mode, and should be:
; - empty for :all or :trace
; - coordinates, code, and force for :deeper or :before
; See `skip-breaks?`.
; -------------------------
; cider.nrepl.middleware.debug/skip-breaks?
apropos
(apropos "selection")
(hello-clojure.core/selection-sort hello-clojure.core/selection-sort-test)
参考
clojure.spec
こちらの本で紹介されていて気になったので、調査。
そもそもどういうものか
Clojureは動的言語なので、型の情報なしで開発を進めていくのが基本です。
動的な言語と静的な言語のどちらが優れているか、という議論は
以前からありますが、あなたはどうお考えでしょうか。
この問題にはさまざまな意見があるかと思いますが、Clojureは動的型付け
を選択した言語です。
しかし、specというライブラリーを使うと、Clojureでも型の恩恵を
受けることができます。
動的型付けであるClojureで型が使えるものということだろうか。
使用してみる
;; import
(require '[clojure.spec.alpha :as s])
;; nameが文字列であることを定義
;; 型でいうとstring相当
(s/def ::name string?)
;; 正のintであることを定義
;; 型でいうとuint相当
(s/def ::age pos-int?)
;; データを定義
(def alice {:name "Alice" :age 30})
(s/valid? ::name (:name alice))
;; true
(s/valid? ::age (:age alice))
;; true
;; データを定義
(def bob {:name "Bob" :age -5})
(s/valid? ::name (:name bob))
;; true
(s/valid? ::age (:age bob))
;; false
参考にした文献
設定変更
Calvaのフォーマット設定が自分と合わなかったので設定変更する。
何をしたいか
Tabキー押したときのフォーマットを無効化したい。
Tabキー押したときにフォーマットされると、自分の想定していないところにカーソルが行くので戸惑っていますので。
公式ドキュメント
フォーマットに関してはこちらの公式ページがあるので、こちらを参照して変えていく。
実施に変えてみる
VSCodeの設定変更
まず、VSCodeのファイル保存時のオートフォーマットを有効化する。
Tabキー押下時のフォーマットは防ぎたいが、フォーマットそのものはほしいので、ファイル保存時のものを有効化する。
Also: If you have Format on Save enabled in VS Code, it will be Calva doing the formatting for Clojure files.
Calvaのキーバインドを無効
キーバインドそのものを無効化する。
特にキーバインドを使用していなかったので、一旦この対応で問題ないと思う。
問題発生
デフォルトキーバインド使ってないと思っていたら、コードの実行で使用していた(Control + Enter
)。
なので、なんとかしてデフォルトのTabキーのみを設定変更する必要がある。
キーバインドの変更
まずVSCodeのキーボードショートカットの設定を開く。
Macを使用しているので、Cmd + S
で開く。
検索欄にcalva
と入力し、Tabキーを探す。
あとは適当なキーに変更したらOK。
これでTabキーのフォーマットを無効化し、その他のキーバインドは使える状態になった。
記事化
今後キーマップ変更したいときがあるとおもうので、記事として残すことにした。
スレッディングマクロ
スレッディングマクロと言う記法を使えば、メソッドチェーンのようなことができるらしい。
(def text " hello world ")
;; threading macroを使う場合
(-> text
clojure.string/trim
clojure.string/upper-case
(str "!!!"))
;; threading macroを使わない場合
(str (clojure.string/upper-case (clojure.string/trim text)) "!!!")
別の例
(def person {:name "Alice" :age 28 :city "New York"})
(str "Age in 10 years: " (+ 10 (:age person)))
;; "Age in 10 years: 38"
(-> person
(:age)
(+ 10)
(str "Age in 10 years: "))
;; "38Age in 10 years: "
;; どうやら各処理の結果は第一引数に渡されるようだ
;; 第二引数に渡すようにするコードは以下になる
(->> person
(:age)
(+ 10)
(str "Age in 10 years: "))
Todoアプリのバックエンドを置き換えてみる
以前にGolangで作ったTodoアプリのバックエンドを置き換えて、Clojureをキャッチアップする試み。
以前作ったTodoアプリ
TodoアプリのAPIドキュメント
まずは準備
まずは必要そうなものを挙げて準備していこう。
参考になると思われる資料
ライブラリやフレームワークの選定
とりあえず候補を挙げてみる。
フレームワーク
Webフレームワーク
Ring
compojure
reitit
Pedestal
フルスタックフレームワーク
Luminus
ライブラリ
JSONエンコード/デコード
Cheshire
データバリデーション
RingとCompojureの動きを確かめる
こちらのページに作業ログを切り出した。
コミュニティライブラリのリポジトリ
コミュニティライブラリのリポジトリサイト見つけた。
cheshrieのparse-stringについて
cheshireのparse-stringでハマったところがあったので、備忘として。
以下のようなテストコードを書いていたが、出力が期待したものとちがっていた。
(:require [cheshire.core :as json])
(deftest test-handler-json
(testing "GET /json"
(let [response (app (mock/request :get "/json"))
body (json/parse-string (bs/to-string (:body response)) true)]
(is (= 200 (:status response)))
(is (= "application/json;charset=UTF-8" (get-in response [:headers "Content-Type"])))
(is (= {:message "これはJSONレスポンスです" :status "success"} body)))))
キーが文字列として扱われている。
; ; expected:
; {:message "これはJSONレスポンスです", :status "success"}
;
; ; actual:
; {"message" "これはJSONレスポンスです", "status" "success"}
これは、cheshire/core/parse-stringにtrueを指定すると回避できる。
(def json-string "{\"message\": \"これはJSONレスポンスです\", \"status\": \"success\"}")
;; trueを指定せずパースする
(def parsed-without-true (json/parse-string json-string))
;; trueを指定してパースする
(def parsed-true (json/parse-string json-string true))
;; 出力を確認
(prn parsed-without-true)
;; => {"message" "これはJSONレスポンスです", "status" "success"}
(prn parsed-true)
;; => {:message "これはJSONレスポンスです", :status "success"}
trueを指定しないと、キーがJSONで扱っている文字列そのままになるっぽい。
trueを指定すると、Clojureのmap構造に変換してくれる、ということのようだ。
記事化
備忘用に記事化した。
Atcoderについて
簡単にAtcoderについて調べたら、どうやらClojureではメモリをくうからあまりできないという話を見かけた。
しかし、Babashkaを使えば早くできる可能性があるらしい。
ただ、どうやら公式で対応しているらしいので杞憂かも。(2024年11月現在)
Babashka
Babashkaそのものも興味深いツールのようなので、いつか触ってみたい。
Babashkaそのものの参考記事。
ソースコードリーディング
なにか一つソースコードをリーディングしてみたい。
ライセンスや著作権の問題があるので、ローカルでやると思うけど。
候補
deferのexclude
defer
で:exclude
オプションを付けると、clojureの標準で使っている名前でも定義が可能らしい。
なお、標準のものを別名にしたい場合は:rename
を使うことで回避が可能とのこと。
ClojureScript本
ClojureScript本あるじゃんね。
日本語の書籍ないと誤解してたからたすかる。
リロード用のミドルウェアがあるらしい
毎回ファイルをロードし直すの面倒だなー、と思い始めたころだったけど、どうやらオートリロードするミドルウェアがRingにあるらしい。
次回、このあたりから触っていきたい。
フィボナッチ関数を書いてみる
再帰の定番である、フィボナッチ関数を書く。
まず単純にフィボナッチを返す関数から。
(defn fibonacci [n]
(cond (= n 0) 0
(= n 1) 1
:else (+ (fibonacci (- n 2)) (fibonacci (- n 1)))))
(deftest fibonacci-helper-test
(testing "フィボナッチ関数のテスト"
(is (= (fibonacci 0) 0))
(is (= (fibonacci 1) 1))
(is (= (fibonacci 2) 1))
(is (= (fibonacci 3) 2))
(is (= (fibonacci 4) 3))
(is (= (fibonacci 5) 5))
(is (= (fibonacci 6) 8))
(is (= (fibonacci 7) 13))
(is (= (fibonacci 8) 21))))
次に、フィボナッチのリストを返す関数。
(defn fibonacci-seq [n]
(letfn [(fib-helper [a b count]
(if (zero? count)
[]
(cons a (fib-helper b (+ a b) (dec count)))))]
(fib-helper 0 1 n)))
(deftest fibonacci-seq-test
(testing "リストを返すフィボナッチ関数のテスト"
(is (= (fibonacci-seq 0) []))
(is (= (fibonacci-seq 1) [0]))
(is (= (fibonacci-seq 2) [0 1]))
(is (= (fibonacci-seq 3) [0 1 1]))
(is (= (fibonacci-seq 4) [0 1 1 2]))
(is (= (fibonacci-seq 5) [0 1 1 2 3]))
(is (= (fibonacci-seq 6) [0 1 1 2 3 5]))
(is (= (fibonacci-seq 7) [0 1 1 2 3 5 8]))
(is (= (fibonacci-seq 8) [0 1 1 2 3 5 8 13]))
(is (= (fibonacci-seq 9) [0 1 1 2 3 5 8 13 21]))
(is (= (fibonacci-seq 10) [0 1 1 2 3 5 8 13 21 34]))))
TOC版。
(defn fibonacci-toc [n]
(letfn [(fib-helper [a b count]
(if (zero? count)
a
(recur b (+ a b) (dec count))))]
(fib-helper 0 1 n)))
(deftest fibonacci-toc-test
(testing "フィボナッチ関数(TOC)のテスト"
(is (= (fibonacci-toc 0) 0))
(is (= (fibonacci-toc 1) 1))
(is (= (fibonacci-toc 2) 1))
(is (= (fibonacci-toc 3) 2))
(is (= (fibonacci-toc 4) 3))
(is (= (fibonacci-toc 5) 5))
(is (= (fibonacci-toc 6) 8))
(is (= (fibonacci-toc 7) 13))
(is (= (fibonacci-toc 8) 21))))
コールグラフ生成ツール
コールグラフ生成ツールがほしい。
多分、コードリーディングの際にも役立つと思う。
chatGPTにきいてみた
Clojureで使えるコールグラフツールの候補を聞いてみた。
clj-kondoでやる方法
まずCLIツールとしてのclj-kondoをインストールする。
macなのでhomebrewでインストールした。
brew install borkdude/brew/clj-kondo
dot形式にする方法をAIに聞いたもの。
(require '[clojure.edn :as edn]
'[clojure.java.io :as io])
;; EDNファイルを読み込む
(defn load-edn [file]
(with-open [r (io/reader file)]
(edn/read (java.io.PushbackReader. r))))
;; 関数呼び出し関係をDOT形式に変換する
(defn generate-dot [analysis]
(let [usages (:var-usages analysis)
edges (map (fn [{:keys [name to]}] [name to]) usages)]
(str "digraph G {\n"
(apply str (map (fn [[from to]]
(str " \"" from "\" -> \"" to "\";\n"))
edges))
"}")))
;; EDNファイルからDOTファイルを生成
(defn edn-to-dot [edn-file dot-file]
(let [analysis (load-edn edn-file)
dot-data (generate-dot analysis)]
(spit dot-file dot-data)))
(edn-to-dot "analysis.edn" "graph.dot")
- 使えそうなツールを調査する
階乗を計算するプログラム
再帰の練習として、階乗を計算するプログラムを書いてみた。
(defn factorial [n]
(if (= n 0)
1
(* n (factorial (dec n)))))
(factorial 5)
;; 120
末尾呼び出し最適化
末尾呼び出し最適化を行ってみる。
(defn factorial-tco [n]
(letfn [(fact-helper [n sum]
(if (zero? n)
sum
(recur (dec n) (* sum n))))]
(fact-helper n 1)))
(factorial-tco 5)
;; 120
ライブラリの作成方法
Clojureでライブラリを作りたくなったので、作成方法を記事化してみた。
GPGキーを使ってデプロイする方法がうまくいかなかったため、今後の課題。
それ用の参考資料をおいておく。
Clojureのポッドキャスト
探していたらClojureのポッドキャストがあった。
ぼちぼち聴いていこうかな。
Rich hickeyによる設計判断
RichによるClojureの設計判断についての記事があった。
ちょっとずつ読んでいこうと思う。
別スクラップに切り出したので、こちらにメモを残す。
この記事を読んでのメモ
少ない抽象データ構造と、たくさんの関数
Clojureでは、オブジェクト指向言語と異なり、関数とデータはおおよそ完全に分離しています。
一方、データと関数が分離している場合は、関数を特定のクラスやオブジェクトに結びつける意味はないですし、逆に、ある関数のために専用のクラスを作る意味もない。極端に考えれば、すべての関数が共通のデータ構造を処理するようにした方が、多様な関数を柔軟に活用できるようになる。
このような構造はJavaのインタフェースのようなものがあれば、他でも実現できると思うだろうけども(実際、Associativeと Seqは、Javaのインタフェースとして定義されています)、それは「注意深く設計すればそのライブラリ内ではそうできる」という話であって、言語として標準の抽象データを定義して、すべてをそこに集約させよう、という世界では、他の言語機能が、すべて、このような仕組みを支援するように作られています。言語としての前提であるからです。たとえば、JavaですべてのデータをMapで作っても、苦しいだけで利点などないでしょう。そういう前提で言語が作られていないからです。
このような抽象データ構造があるからこそ、関数とデータを分離できるわけです。関数は共通の抽象データ構造しか見ていないからこそ、実際のクラスとは結びつく必要がないのです。
アドホックな多態
一方、Clojureにとって多態とは、関数ディスパッチの話と捉えられています。関数はデータ(オブジェクト)とダイレクトには結びついてないので、多態の説明として、オブジェクトやクラスと結びつけて語ることはできません。ある関数を呼んだ時に、実際に実行される関数実装はどれなのか?というディスパッチの問題に過ぎないのです。
つまり、コードを書いている時に、ある関数を多態にするかどうかというのは後から考えても大丈夫な仕組みなわけです。もちろん、作ってる段階で、設計として「ここの関数は外から拡張できるようにしたいから、マルチメソッドにしておこう」とか「ここはユーザー利便性を考えてプロトコルを定義しておくか」ということはありますが、そうでないところを後からマルチメソッドやプロトコル関数化することも、結構簡単に行えるのです。
だからAd-hoc(場当たり的な)ポリモフィズムと呼ぶわけです。もちろん注意すべきことはあって、対象の処理が関数に分離されてなければ、後で多態関数化することもできませんから、できれば関数は細かく分けておいた方が、後から対処しやすいと思います。そのような注意点さえクリアしておけば、柔軟に後からコード変更可能だ、というのも、Clojureの利点の一つです。
Web開発について
この記事かなりまとまっててとてもいい。
RIngのこととかも書かれていて、雑多に集めていた知識が一つにまとまった感ある。
個別に勉強し終わったら読み直したい。
Calvaでファイル単位に読み込む方法
コマンドパレットからCalva: Load/Evaluate Current File and its Requires/Dependencies
を選択してロードする。
これはショートカットキーでも設定ができるので、ショートカットキー設定を開いて(mac環境ならCmd+K
を押したあとにCmd+S
)、自分の使いやすいキーにバインドすれば快適に使える。
とりあえずOpt+Enter
にキーを設定した。
Duct
WebフレームワークであるDcutというのがあるらしい。
Ringの方を手に掛けようとしているから、あとで使うための資料をまとめておく。
あとなんかドキュメントが少ないとかいう恐ろしい話を見かけたんだけど、ホント...?
defrecordについて気になる記事
気になる記事見つけたのでメモ
userjarについて
Uberって何というと、ドイツ語で「従事・同時性」みたいな意味があってそこから来ているそうです。
高速化関係の記事
型ヒント
JVM
マクロの書き方についての資料
他の作業があって、まだマクロに入門するのは難しいので資料だけ集めておく。
本
Clojureのリテラル表現
bigintとかbigdecが出てきてよくわからなかったので、資料をメモ。
STM関係
Javaを含めてSTM全般の話
forとletの関係について
:when
オプションなどを一旦考えなければ、以下のコードのようにlet
でfor
の動作は再現可能。
(for [x [1 2 3]]
x)
(let [x (doall (map identity [1 2 3]))]
x)
つまり、for
では遅延リストではなく、評価したリストを返している模様。
:when
の再現もfilter
を使えば可能。
(for [x [1 2 3 4 5 6]
:when (even? x)]
x)
(let [x (doall (map identity (filter even? [1 2 3 4 5 6])))]
x)
:let
の再現はどうやら難しいっぽい。
(for [x [1 2 3]
:let [y (* x x)]]
(+ x y))
;; (2 6 12)
(let [x (doall (map (fn [x]
(let [y (* x x)]
(+ x y))) [1 2 3]))]
x)
;; (2 6 12)
REPL駆動開発で気になった点
この記事から。
REPL を起動するとプロジェクト中の Clojure のコードは REPL 上に読み込まれ、REPL 上から利用可能となります。
REPL起動で全てが読み込まれるとのことだが、今のところ、自力で読み込まない限り読んでくれない。
なにか起動時のやり方があるのだろうか?
あるいは設定?
Calvaでやってるのが問題?
よくわかっていないが、一旦備忘としてメモしておく。