はじめに
業務でClojureを触ることになったのでキャッチアップを始めた。
あまりに宇宙語すぎてソースを見ても何が何だかわからなかったので基本から学習を始めたので、備忘録を兼ねてclojureについて紹介します。
ClojureはLisp系の関数型プログラミング言語で、主にJVM上で動作し、Java資産を活用できる
最大の特徴は「コード=データ」という性質と、すべてのデータ構造がイミュータブルである点
これにより副作用を抑えた安全なプログラム設計や並行処理がしやすい
また、mapやfilterなどの高階関数を用いてデータを変換していくスタイルが基本になる
オブジェクト指向のように状態を持つのではなく、データを関数で加工していく点が大きな特徴
1. リテラル(基本データ型)
数値型
(type 1) ; -> java.lang.Long
(+ 1 1) ; -> 2
(type 1.5) ; -> java.lang.Double
(* 1.5 1.5) ; -> 2.25
(type 22/7) ; -> clojure.lang.Ratio
(+ 22/7 44/5) ; -> 418/35
(type 1234N) ; -> clojure.lang.BigInt
(type 1.5M) ; -> java.math.BigDecimal
(* 1.1M 1.1M) ; -> 1.21M
(* 1.1 1.1) ; -> 1.2100000000000002
通常の整数、少数に関してはjavaクラスのLong,Doubleが使用されている
分数に関してはRatio,数値の後にNをつけることでBigInt(任意精度整数),MをつけるとBigDecimal(任意精度小数)となる
文字列・文字・正規表現
(type "Hello") ; -> java.lang.String
(type \e) ; -> java.lang.Character
(type \newline) ; -> java.lang.Character
(type #"[0-9]+") ; -> java.util.regex.Pattern
文字列は "..." で表現され、内部的には java.lang.String として扱われる。
一方、\e のような単一文字は java.lang.Character となり、改行やタブも \newline や \tab といった専用リテラルで表現できる。
また、#"[0-9]+" のように #"" で囲むと正規表現リテラルとなり、java.util.regex.Pattern として事前にコンパイルされた状態で扱われる。
さらに、文字列内の \n はエスケープシーケンスとして解釈されるのに対し、\newline は独立した文字リテラルであり、用途によって使い分ける必要がある。
(println "\"\n\"")
; "
; "
(println \" \n \")
; " n "
(println "\"\newline\"")
; "
; ewline"
(println \" \newline \")
; "
; "
2. コレクション(超重要)
List
Listはリンクリスト構造を持つシーケンスで、先頭要素へのアクセスや追加が高速なのが特徴
firstやrestで先頭中心の操作を行い、conjやconsは要素を先頭に追加する
一方でインデックスアクセスnthはコストが高く、ランダムアクセスには向いていない
(let [list '(1 2 3 4)]
(println (type list)) ; -> clojure.lang.PersistentList
(println (first list)) ; -> 1
(println (rest list)) ; -> (2 3 4)
(println (nth list 2)) ; -> 3
(println (conj list 0)) ; (0 1 2 3 4)
(println (cons -1 list)) ; (-1 1 2 3 4)
)
Vector
Vectorは配列に近い構造を持つ永続データ構造で、インデックスによるランダムアクセスや更新が高速に行える
nthやassocによる位置指定の操作が得意
conjは末尾に要素を追加するため、Listとは挙動が異なる点に注意が必要
(let [array [1 2 3 4]]
(println (type array)) ; -> clojure.lang.PersistentVector
(println (first array)) ; -> 1
(println (rest array)) ; -> (2 3 4)
(println (nth array 2)) ; -> 3
(println (conj array 5)) ; [1 2 3 4 5]
(println (assoc array 1 100)) ; [1 100 3 4]
)
Set
Setは重複を許さないコレクションで、要素の存在チェックを高速に行えるのが特徴
順序は保証されず、firstやrestの結果は不定となる
contains?での存在確認、conjによる追加、disjによる削除ができる
また、intersectionやunion、differenceといった集合演算が標準で用意されており、データの比較やフィルタリングが可能
(let [my-set (set [1 2 2 3 4])]
(println (type my-set)) ; -> clojure.lang.PersistentHashSet
(println (first my-set)) ; -> 1
(println (rest my-set)) ; -> (4 3 2)
(println (contains? my-set 2)) ; -> true
(println (conj my-set 5)) ; -> #{1 4 3 2 5}
(println (disj my-set 3)) ; -> #{1 4 2}
(println (set/intersection #{1 2 3} #{2 3 4})) ; -> #{3 2}
(println (set/union #{1 2 3} #{3 4 5})) ; -> #{1 4 3 2 5}
(println (set/difference #{1 2 3 4 5} #{2 4})) ; -> #{1 3 5}
)
Map
Mapはキーと値のペアで構成される辞書型のコレクションで、キーワードを使った直感的なアクセスが可能
(:key map)のようにキーワード自体を関数として使える点が特徴
assocやupdateによって元のMapを変更せずに新しいMapを生成するイミュータブルな設計になっている
keysやvalsで構造を扱うこともできる
(let [testMap {:name "test1", :age 21}]
(println (type testMap)) ; -> clojure.lang.PersistentArrayMap
(println (:name testMap)) ; -> test1
(println (:comment testMap)) ; -> nil
(println (get testMap :comment "hey!")) ; -> hey!
(println (assoc testMap :comment "hey!")) ; -> {:name test1, :age 21, :comment hey!}
(println (update testMap :age inc)) ; -> {:name test1, :age 22}
(println (dissoc testMap :age)) ; -> {:name test1}
(println (keys testMap)) ; -> (:name :age)
(println (vals testMap)) ; -> (test1 21)
)
関数
関数の使い方
Clojureではdefnを使って関数を定義し、関数名の後に引数ベクターと処理内容を記述する
関数呼び出しは常に「(関数名 引数…)」という前置記法で統一されており、演算子も関数として扱われる点が特徴
戻り値は最後に評価された式となり、明示的なreturnは不要
シンプルな構文で関数を定義・呼び出しできるため、データ変換処理を組み立てやすい
(defn greet [name] (str "Hello," name "!"))
(greet "World") ; -> "Hello,World!"
無名関数
無名関数は名前を持たない関数で、一時的な処理をその場で定義したいときに使用する
fnを使う方法と、より簡潔な#()記法(リーダーマクロ)の2種類があり、%は引数を表す
例えば#(* % %)は引数を2乗する関数を意味する
高階関数と組み合わせて使うことが多く、短い処理を簡潔に記述できる点が大きなメリット
((fn [i] (* i i)) 5) ; -> 25
(#(* % %) 5) ;-> 25
高階関数
高階関数は「関数を引数として受け取る、または関数を返す関数」のこと
Clojureではmap、filter、reduceが代表的で、コレクションに対する処理を宣言的に記述できる
さらにcompによる関数合成や、partialによる部分適用によって関数を組み合わせて再利用できる
これにより、処理を小さな関数として分割し、それらをつなげてデータ変換パイプラインを構築するスタイルが実現できる
(map inc [1 2 3]) ; -> (2 3 4)
(filter (fn [i] (= (mod i 2) 1)) [1 2 3 4 5]) ; -> (1 3 5)
(reduce (fn [x y] (+ x y)) [1 2 3 4]) ; -> 10
(def f (comp inc #(* % 2)))
(f 2) ; -> 5
(def greeting (partial str "Hello, "))
(greeting "world!") ; -> "Hello, world!"
最後に
地道にClojureと仲良くなっていきたいです