LoginSignup
2
1

More than 3 years have passed since last update.

Clojureにおけるパターンマッチング~defunマクロ~

Posted at

Clojureにおけるパターンマッチング

この記事では、Clojureにおけるパターンマッチングを解説しています。特に、

defun

というcore.matchを使った関数定義を可能にするマクロの使用法を詳しく解説しています。

準備

依存関係に以下を追加:

[defun "0.3.1"]

ネームスペースで:

(ns match.core
  (:require [defun.core :refer [defun fun]]))

defunについて

Clojureに元から備わっているdefnと違い、defunでは引数リストの部分でcore.matchによるパターンマッチングを行うことができます。

(defun greet
  (["JoJo"] "誇りの道を往く者に太陽の輝きを")
  (["Dio"] "野望の果てを目指すものに生贄を")
  ([x]  (format "こんにちは,%sさん" x)))

(greet "JoJo")
;; => "誇りの道を往く者に太陽の輝きを"
(greet "Dio")
;; => "野望の果てを目指すものに生贄を"
(greet "Tom")
;; => "こんにちは,Tomさん"

このように、defunでは、通常の関数定義マクロdefnでの「引数オーバーロード」と同様の形式でcore.matchによるパターンマッチングを行えます。

実際に、greetは一つの引数しかとらない関数ですが、"JoJo"と"Dio"という引数にのみ特別な値を返しています。

他の例として、「フィボナッチ数列」を返す関数をdefunを使って作成してみましょう。

(defun fib
  ([0N] 0N)
  ([1N] 1N)
  ([x] (+ (fib (- x 1)) (fib (- x 2)))))

テスト

(doseq [x (range 10)] (println (format "(fib %s) = %s" x (fib x))))

;; result

(fib 0) = 0
(fib 1) = 1
(fib 2) = 1
(fib 3) = 2
(fib 4) = 3
(fib 5) = 5
(fib 6) = 8
(fib 7) = 13
(fib 8) = 21
(fib 9) = 34

同じ関数を通常のdefnでも書けますが、その場合は、cond等を使って場合分けが必要になります。

(defn fib-with-defn
  [x]
  (cond
    (= 0N x) 0N
    (= 1N x) 1N
    :else  (+ (fib-with-defn (- x 1)) (fib-with-defn (- x 2)))))

defunでは、引数オーバーロードをしておけば、引数リストの部分でパターンマッチングが行われるので、記述が簡潔になっているように思われます。

他にも再帰を使った関数の例を見てみましょう。以下の関数は、n以下の数字を順番にプリントします。

(defun count-down
  ([0] (println "Reached 0."))
  ([n] (do (println n) (recur (dec n)))))

(count-down 10)

;; result

10
9
8
7
6
5
4
3
2
1
Reached 0.

guard パターン

ここまでは、特定の値との比較によるパターンマッチングを見てきました。これに加え、core.matchでは述語関数によるパターンマッチングもサポートされています:

(x :guard pred)

の形で、(pred x)がnon nilとなるかどうかを基準としたマッチングになります。

以下はおなじみのFizzBuzz問題を述語関数によるパターンマッチングを使って解いたもの。

(defn divisible-by? [div n] (zero? (mod n  div)))

(defun fizz-buzz 
  ([(n :guard #(divisible-by? 15 %))] :fizz-buzz)
  ([(n :guard #(divisible-by? 5 %))] :buzz)
  ([(n :guard #(divisible-by? 3 %))] :fizz)
  ([n] n))

テスト


(map fizz-buzz (range 1 20))
;; => (1
;;     2
;;     :fizz
;;     4
;;     :buzz
;;     :fizz
;;     7
;;     8
;;     :fizz
;;     :buzz
;;     11
;;     :fizz
;;     13
;;     14
;;     :fizz-buzz
;;     16
;;     17
;;     :fizz
;;     19)

reduce -> defun

Clojureはデータの扱いを得意とする言語です。データとして何らかのコレクション(集合)を扱う場合、

  1. 集合ー>値
  2. 集合ー>集合

という変換が考えられます。このうち、1の変換の例として

「数の集合からその和を求める」

というものを挙げることができます。このように、集合から値への変換を行うとき、通常のClojureではreduceを使うのが一般的かと思います:

(defn sum-by-reduce
  "数の集合collの和を返す。"
  [coll]
  (reduce + 0 coll))

(sum-by-reduce [1 2 3])
;; => 6

reduceについて少し深掘りすると、reduceは、

(reduce f(関数) init(初期値) coll(集合))

という文法を有します。

ここで、fとして(集積値、次の値)という2つの引数をとるものが要求されます。上の関数で言えば、+がこのような関数になっています。reduceは、初め初期値と集合の最初の要素をfに渡し、その結果(集積値)と集合の第二の要素をfに渡し、その結果でまた同様のことをする、というプロセスによって集合を消費し、最後の値を返すわけです。

同じことをパターンマッチングで行うことができます。

(defun sum-by-defun
  ([([] :seq) ret] ret)
  ([([x & r] :seq) ret] (recur r (+ x ret)))
  ([coll] (recur coll 0)))

(sum-by-defun [1 2 3])
;; => 6

この場合は、reduceで書いた同じ関数よりわかりにくくなってしまいました。やはり引数オーバーロードを使っていますが、引数2個の部分では

(残りの集合 集積値)

を処理しています。特に、(残りの集合)が空になった時点で集積値を返していることに注意して下さい。

応用例:集合の分割

最後に、これまでの知識を使って少しは実用性のある関数を作ってみました。

問題

述語関数によって集合を分割する関数split-byを作りたい。split-byは

(split-by pred coll)

のように、述語関数と集合を引数にとり、predに対して真となる集合の要素によって集合を分割する。例えば、

(split-by keyword? [1 2 :a 4 3 :f :g 6 8 9 :x])
;; => ((1 2) (4 3) (6 8 9))

の様に動作するようにしたい。

コード

しばらく思案した後、以下のような方法を思いつきました。他にもやり方はあるはず。

(defn split-by [pred coll]
  (let [impl (fun
              ([([] :seq) ret n]
               ret)
              ([([(x :guard pred) & r] :seq) ret n]
               (recur r ret (inc n)))
              ([([x & r] :seq) ret n]
               (recur r (update ret n #(conj % x)) n))
              ([coll]
               (recur coll {} 0)))]
    (->> coll
         impl
         vals
         (map reverse))))

まとめ

以下、defunのgithubからの引用になります。

A macro to define clojure functions with pattern matching just as erlang or elixir.

このように、他言語の強みをマクロを使って積極的に取り入れていくことは素晴らしいことですね。これからもClojureという言語の発展に期待しています。

参考

defun

core.match

なぜ僕の中でElixirが一番であり続けるのか

2
1
0

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
2
1