LoginSignup
3
2

More than 5 years have passed since last update.

Clojureでオブジェクト指向を行うまとめ

Last updated at Posted at 2018-05-23

Clojureは関数型言語で本来オブジェクト指向によらないパラダイムを取っています。ですが、ここではそんなこともお構いなしにオブジェクト指向的な機構を使ってプログラムを構築する方法をまとめておきます。

クラスの作成

クラスの定義にはdefrecordを使用します。
いかにもな名前のgen-classはJava用のクラスを作るもののようなので、clojureで完結させる場合には不要なものだと思われます。

(defrecord Person[name age])

インスタンス(オブジェクト)の生成

複数の方法が存在し、new、->(※)、.マクロを使用する方法があります。
恐らく違いはどれもありません。

  • ※2018/5/24 追記:
    • コメントに頂きましたが内部で->名前な関数が生成されているらしいです。
(defrecord Person[name age])
(let [bob (new Person "bob" 25)]
    (println bob))
(let [bob (->Person "bob" 25)]
    (println bob))
(let [bob (Person. "bob" 25)]
    (println bob))

プロパティアクセス

プロパティ名の頭に:(mapのアクセス方法)か.(インスタンスメンバーのアクセス方法)でアクセスすることができます。
通常と同様にClorureでは定数しか扱えないため、書き換えが必要ならatomとreset!(またはswap!)を利用します。
(atomはvolatile!/vreset!/vswap!でも可。ただしスレッドローカルに限る)

(defrecord Person[name age])
(let [bob (Person. "bob" 25)]
    (println (:name bob)))
(let [bob (Person. (atom "bob") 25)]
    (println @(.name bob))
    (reset! (.name bob) "jane")
    (println @(.name bob)))

コンストラクターの定義

defrecordには他の言語のように初期化処理を記述できるコンストラクターは存在しません。
make-○○※という名前で引数をゴニョゴニョしてnewするコンストラクターを自作します。
※Lispでオブジェクトを作るのに一般的な名前。好きな名前でも構いません。

(defrecord Person[first-name second-name full-name])
(defn make-Person[first-name second-name]
    (Person. first-name second-name (str first-name "・" second-name)))

(let [bob (make-Person "bob" "太郎")]
    (println (:first-name bob))
    (println (:second-name bob))
    (println (:full-name bob)))

メソッドの定義(defprotocol)

defrecord単体でメソッドを定義することはできません。
defprotocolで定義したプロトコル(インターフェース)をdefrecordに継承してメソッドの定義を行います。
定義するメソッドにはdefnやfnといった表記は必要なくメソッド名から始まります。
また、メソッドの第一引数は自分自身のオブジェクト(thisとかMeとかselfとか)固定となります。

メソッドの呼び出しは
(メソッド名 インスタンス 引数...) または (.メソッド名 インスタンス 引数...)
で行います。
メソッドの定義方法によっては.が使用できないこともあるので.無しで呼ぶ方がよさそうな感じがします。

(defprotocol IPerson
    (write-profile[self]))
(defrecord Person[first-name second-name full-name age] IPerson
    (write-profile[self]
        (printf "FULLNAME > %s\nAGE      > %d\n" full-name age)))

(defn make-Person[first-name second-name age]
    (Person. first-name second-name (str first-name "・" second-name) age))

(let [bob (make-Person "bob" "太郎" 25)]
    (write-profile bob)
    (.write-profile bob))

メソッドの定義(defmulti & defmethod)

メソッドの追加方法としてはdefmultiとdefmethod(マルチメソッド)で行う方法もあります。
ただし、厳密にはインスタンスメンバーとならないためか.メソッド名が使用できません。
また、関数内でのプロパティアクセスでもselfが必要なため注意が必要です。

(defrecord Person[first-name second-name full-name age])
(defmulti write-profile class) 
(defmethod write-profile Person [self]
    (printf "FULLNAME > %s\nAGE      > %d\n" (:full-name self) (:age self))) 

(defn make-Person[first-name second-name age]
    (Person. first-name second-name (str first-name "・" second-name) age))

(let [bob (make-Person "bob" "太郎" 25)]
    (write-profile bob))
    ;(.write-profile bob)) Error

複数引数を受け付ける場合にはdefmultiの定義を捻る必要があります。

(defrecord Person[first-name second-name full-name age])
(defmulti write-profile (fn[& args] (mapv class args)))
(defmethod write-profile [Person String] [self hobby]
    (printf "FULLNAME > %s\nAGE      > %d\nHOBBY    > %s\n" (:full-name self) (:age self) hobby)) 

(defn make-Person[first-name second-name age]
    (Person. first-name second-name (str first-name "・" second-name) age))

(let [bob (make-Person "bob" "太郎" 25)]
    (write-profile bob "崖登り"))

クラスの継承

できません。不可能です。
しかし、代替策はあります。
extendを使用します。
extendはあるクラスに対して、インターフェースと関数マップを与えることで関数マップの関数をメソッドとして追加してくれます。
基底クラスとしたいクラスのメソッドをマップに記述し、これを継承したいクラスに対して与えることで複数のクラスに同一の実装を拡張することができます。
ただし、extendしたメソッドには次の点で注意が必要です。

  • 関数内でのプロパティアクセスでもselfが必要。
  • .メソッド名使用不可。

(extendしたメソッドはパフォーマンス的に劣るみたいな話も聞きますけど実際どうなんでしょうか)

(defprotocol IPerson
    (write-profile[self]))
(def MPerson{
    :write-profile (fn[self]
        (printf "FULLNAME > %s\nAGE      > %d\n" (:full-name self) @(:age self)))})

(defrecord Person[first-name second-name full-name age])
(extend Person IPerson MPerson)

(defn make-Person[first-name second-name age]
    (Person. first-name second-name (str first-name "・" second-name) (atom age)))

(defprotocol IPersonEx
    (add-age[self]))
(defrecord PersonEx[first-name second-name full-name age] IPersonEx
    (add-age[self]
        (swap! age inc)))
(extend PersonEx IPerson MPerson)

(defn make-PersonEx[first-name second-name age]
    (PersonEx. first-name second-name (str first-name "・" second-name) (atom age)))

(let [bob (make-Person "bob" "太郎" 25)]
    (write-profile bob))
    ;(.write-profile bob)) Error
(let [bob (make-PersonEx "bob" "太郎" 25)]
    (add-age bob)
    (add-age bob)
    (add-age bob)
    (add-age bob)
    (add-age age)
    (write-profile bob))

オーバーライド

extendやextend-type等で拡張したメソッドのみ可能です。

extendで拡張したメソッドをオーバーライド
(defprotocol ITest
    (out[self]))
(def MTest{
    :out (fn[self]
        (println "ExtendMap"))})
(defrecord Test[])
(extend Test
    ITest MTest
    ITest{
        :out (fn[self]
            (println "OverRidden"))})

(let [t (Test.)]
    (out t))
extend-typeで拡張したメソッドをオーバーライド
(defprotocol ITest
    (out[self]))
(def MTest{
    :out (fn[self]
        (println "ExtendMap"))})
(defrecord Test[])
(extend Test
    ITest MTest)
(extend-type Test ITest
    (out[self]
        (println "OverRidden")))

(let [t (Test.)]
    (out t))
defrecordで実装したメソッドをオーバーライド
(defprotocol ITest
    (out[self]))
(defrecord Test[] ITest
    (out[self]
        (println "DefRecord")))
(extend Test
    ITest{
        :out (fn[self]
            (println "OverRidden"))})

(let [t (Test.)]
    (out t))
;user.Test already directly implements user.ITest for protocol:#'user/ITest
;user.Testはすでにプロトコルのuser.ITestを直接実装しています:# 'user / ITest

あとがき

意外に十分なクラス操作ができると分かったので満足です。

3
2
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2