概要
初めてClojureに触れる人の手助け、また自分の振り返りとして記事をまとめています。
Clojure新米であるため、誤りがあれば都度ご連絡いただけると幸いです。
概要から始まり、基礎 -> 関数型プログラミング -> WEBアプリという順番でまとめていく想定です。
※ 基本的には、Clojure公式ドキュメントを閲覧することをお勧めします。
※ 注意: 著者はLispや関数型言語を取得しているわけではございません。過不足ございましたらご指摘お願いいたします。
Clojure
Clojureは、Javaプラットフォーム上で動作するLisp方言の一つで、2007年にRich Hickeyによって開発されました。
Clojureは、Lispの影響を受けた関数型プログラミング言語であり、Javaとのシームレスな統合を可能にするために、Java仮想マシン(JVM)上で実行されます。
Clojureの特徴として、以下のようなものが挙げられます。
- 関数型プログラミングのサポート:Clojureは、高階関数、無名関数、再帰関数、マクロなどの機能を提供しています。
- 不変性データ構造:Clojureは、永続的な不変性データ構造をサポートしています。これにより、副作用のない関数を記述しやすくなり、安全で並行処理に適したプログラムを開発することができます。
- マクロ:Clojureは、マクロ機能を提供しており、コードの自動生成や再利用を簡単にすることができます。
- Javaとの統合:Clojureは、Javaとの相互運用性を備えており、Javaクラスやライブラリを直接利用することができます。
- 高速性:Clojureは、JVM上で動作するため、高速で効率的なプログラムを開発することができます。
※ ClojurescriptというClojureのJavascriptコンパイラもあり、Web開発におけるフロント側の実装を行うこともできます。
Clojure基礎
データ型
数値
整数、浮動小数点数、比較演算子等が利用可能です。
42 ; integer
3.14 ; float point
22/7 ; ratio
文字
テキストを表現するためのデータ型
"Hello Clojure" ; string
\e ; character
#"[0-9]+" ; ragular expression
シンボル
Clojureにおいて重要な役割を果たすデータ型。
識別子として、変数や関数、名前空間、マクロを定義する際に利用されます。
map
my-fuction
キーワード
シンボルの先頭にコロンを付与したもの。
Mapのキーとして利用されます。 ※1 (keyword
関数を利用して文字列をKeyにすることも可能)
:name
:age
対象のキーワードがMapに存在しない場合はnil
を返却する。
(def person {:name "Alice" :age 30})
(:name person)
; => "Alice"
(:age person)
; => 30
(:address person)
; => nil
キーワードは名前空間を持つことができます。
2つの::
を先頭につけることで表現できます。※ClojureのFramework-duct
ではよく見られます。
(ns my.namespace) ; javaでは packageと表現されている
(def person {::name "Alice", ::age 30})
; => {:my.namespace/name "Alice", :my.namespace/age 30}
Collection
リスト、ベクター、マップ、セット(集合)の4つがあります。
(1 2 3) ; list
[1 2 3] ; vector
{:name "Alice" :age 30} ; map
#{1 2 3} ; set
上記、Collectionは、データ構造を抽象化したインターフェースであるISeq
(シーケンス)を実装のもとなっています。Collectionを操作する関数first
, reset
, cons
, nth
をデータ型を意識することなく、利用することが可能です。
※ listは正格評価,vectorは遅延評価やパフォーマンスで違いがあります。
Atom
管理された参照型のひとつで、変更可能な状態を持つオブジェクトを表現します。
スレッドセーフな方法で値を更新することができ、内部の値が不変なデータ構造であることを前提にしています。
※ ただし、アトムは適切に使用することが重要であり、状態管理の必要がない場合や不変なデータ構造で問題を解決できる場合は、アトムを使わずに済ますことが推奨されます。
(def my-atom (atom 0)) ; 作成方法
@my-atom ; 参照方法
; => 0
(defref my-atom) ; 参照方法
(swap! my-atom inc) ; atom型を操作する関数として`swap!`を利用しています。
; => 1 ; incは数値を1上げます。
変数
Clojureには変数の概念があります。
Clojureでは変数を定義すると、その変数は不変(再代入を許さない)となります。
変数の値を変更するためには、新しい変数を定義する必要があります。
Clojureでの変数の定義には、letやdefなどの特別な形式があります。
letはローカルな変数を定義し、defはグローバルな変数を定義します。
例えば、次のように変数を定義することができます。
(let [x 1 y 2] ; (let [変数名 値 ...] 処理)
(+ x y)) ; 前置法という記述方法です。
上記の例では、xとyという変数を定義しています。また、2つの変数を足し合わせています。
(def x "Hello Clojure") ; (def 変数名 値)
上記の例では、xという変数を定義しています。
関数
Clojureでは、関数が非常に重要な役割を担っています。
関数は値を返すことができ、他の関数に渡すことができます。
defn
マクロをつかって関数を定義します。
下記は引数で受け取った数値を加算した結果を返す関数です。
(defn add [x y] ; 定義
(+ x y))
(add 3 5) ; 利用方法
; => 8
また、Clojureでは無名関数を定義することもできます。
無名関数は、#
,fn
という形式で表現されます。
(#(+ %1 %2) 3 5) ; #を使用した無名関数
; => 8
((fn [a b]
(+ a b) 3 5) ; fnを使用した無名関数
; => 8
高階関数
高階関数は、関数を引数に受け取ったり、関数を戻り値として返すこのできる関数です。
コードが簡素になり、かつ再利用性可能なものになるため、関数型プログラミングの特性である柔軟性とモジュール性が向上します。
代表例を紹介します。
(map inc [1 2 3]) ; mapは与えられた関数をコレクションの各要素に適用し、
; => [2 3 4] ; 新しいコレクションを返却します。
(filter even? [1 2 3 4]) ; filterは与えられた述語関数(真偽値を返却する関数)が真となる要素だけで
; => [2 4] ; 新しいコレクションを返却します。
(reduce + 0 [1 2 3 4]) ; reduceはコレクションの要素を左から右に走査し、
; => 10 ; 与えられた関数を適用して結果を累積する関数です。
- 名前空間
Clojureでは、名前空間という概念があります。名前空間を使うことで、異なる名前空間で同じ名前の変数や関数を定義することができます。
名前空間を使うことで、コードの重複を避けたり、モジュール化したりすることができます。
名前空間は、nsという特別な形式を使って定義します。
例えば、次のように名前空間を定義することができます。
(ns my-namespace
(:require [clojure.string :as str]))
上記の例では、my-namespaceという名前の名前空間を定義しています。また、:requireを使って、clojure.stringという名前空間をインポートしています。
- マクロ
Clojureには、マクロという概念があります。マクロを使うことで、プログラムの実行前にコードを変換することができます。これによって、コードの簡略化や最適化などを行うことができます。
マクロは、defmacroという特別な形式を使って定義します。マクロは、関数と同様に引数を受け取りますが、関数と異なり、マクロは式を返すのではなく、式を生成します。
例えば、次のようにマクロを定義することができます。
(defmacro my-if [cond then else]
`(if ~cond ~then ~else))
上記の例では、my-ifというマクロを定義しています。このマクロは、3つの引数を受け取り、if式を生成して返します。マクロの本体は、バッククォートと呼ばれる特殊な記号を使って式を記述しています。バッククォートの中で、~を使って変数を展開しています。
- プロトコル
Clojureには、プロトコルという機能があります。
プロトコルは、特定のデータ型に対して、インターフェイスを定義するために使用されます。これにより、異なるデータ型に対して同じ操作を実行することができるようになります。
例えば、clojure.core/seqableというプロトコルは、シーケンスのように振る舞うことができるすべてのデータ型に対して、seq関数を提供します。
これにより、異なるデータ型に対して同じ操作を実行することができます。
※ JavaのInterfaceに近いイメージです。
(defprotocol Flyable
(fly [obj]))
(defrecord Bird [name]
Flyable
(fly [obj]
(println (str (:name obj) " is flying."))))
(defrecord Airplane [model]
Flyable
(fly [obj]
(println (str (:model obj) " is flying."))))
(fly (Bird. "Eagle")) ; "Eagle is flying."
(fly (Airplane. "Boeing 747")) ; "Boeing 747 is flying."
- Javaのインターフェース
Clojureは、Javaとのインターフェイスが可能です。これによって、Javaで書かれたライブラリやフレームワークをClojureで使うことができます。
Javaのクラスやメソッドにアクセスするには、Javaのパッケージ名を指定した上で、Javaのクラスやメソッドを呼び出す形式を使います。
例えば、次のようにJavaのクラスをインポートして、Javaのメソッドを呼び出すことができます。
(import 'java.util.Date)
(defn print-current-date []
(let [date (Date.)]
(println date)))
上記の例では、java.util.DateというJavaのクラスをインポートして、Dateクラスのインスタンスを作成し、現在の日付を表示しています。
※ Date.は、JavaのDateクラスのコンストラクタを呼び出すためのClojureの書き方です。
JavaのクラスをClojureで使う場合、クラス名の後に.をつけて、インスタンス化するためのコンストラクタを指定します。
練習問題
※ 解答例はあくまで解答例ですので、参考程度に参照ください。
関数型プログラミング
関数型プログラミングは、プログラミングスタイルの1つで、問題を解決するために関数を使用することに焦点を当てています。
オブジェクト指向プログラミングとは異なり、プログラムの状態や変数の変更よりも、関数の合成や変換を重視します。
関数型プログラミングでは、関数が純粋であることが重要です。つまり、入力値に対して常に同じ出力値を返し、副作用を起こさないことが求められます。これにより、プログラムの動作が予測可能で、テストやデバッグが容易になります。
また関数型プログラミングでは、再帰的な関数呼び出しを使用することが一般的であり、高階関数やラムダ式の使用が一般的です。
関数型プログラミングの主なメリットは、副作用が少ないことで、プログラムの安定性が高まることです。また、複雑な問題をシンプルに解決することができ、並行処理や分散処理にも適しています。
※ 副作用という言葉の実例を記述いたします。
- 純粋な関数
- 引数 x と y を受け取り、それらの和を計算して返します。この関数は純粋であり、副作用がありません。同じ引数を渡すと、常に同じ結果が返されます
;; 二つの数値の和を返す純粋な関数
(defn add-numbers [x y]
(+ x y))
- 副作用のある関数
- この関数は、アトムを使用して管理された可変のカウンターをインクリメントします。swap!関数は、アトムに格納されている値を変更する副作用があります。この関数を実行するたびに、カウンターの値が変化します。副作用があるため、同じ引数を渡しても、実行するたびに結果が変わる可能性があります。
;; 変数を書き換える副作用がある関数
(def counter (atom 0)) ; アトムを使用して可変のカウンターを定義
(defn increment-counter []
(swap! counter inc))
WEB アプリケーション
下記を参考にするといいかと思います。
※ 私の方でもまとまり次第、記載していきます。