正規表現はお好きでしょうか?この記事では、プログラミング言語Clojureで正規表現を通常より簡単に作成する方法を紹介しています。
モチベーション
正規表現は複雑なものになると、かなりのタイピングを要し、ミスを犯しやすくなりがちです。
例えば、以下の正規表現
(?:(\w+)(?:\p{Space},\p{Space})(\w+)\p{Space}+(\d+)(?:\p{Space},\p{Space})(\d+))
は
Friday, December 27, 2019
のように表記された日付をパースするものですが、これを手動で作成するのはかなり苦痛です。
Emacsのrxというライブラリーをご存じでしょうか。このライブラリーで提供されているrxというマクロを使うと上記のような正規表現をEmacsLispのコードから作成できるため、非常に便利だと感じました。
これと同様のことをClojureでもやりたいと思い、以下のような実装を試みました。
実装
準備
ネームスペースで
(ns rx.core
(:require [clojure.string :as str])
(:refer-clojure :exclude [and or + * set repeat]))
(alias 'c 'clojure.core)
Clojureの正規表現はJavaクラスjava.util.regex.Patternやjava.util.regex.Matcherを使用しています。(以下Pattern, Matcherなどと表記)
Patternに対してstrを適用すると、正規表現を文字列として返します。
(str #"\w")
;; => "\\w"
このことを利用し、正規表現を生成する関数を作成していきます。
定数を定義
(def word "\\w")
(def whitespace "\\s")
(def digit "\\d")
(def alnum "\\p{Alnum}")
(def space "\\p{Space}")
(def begnning-of-line "^")
(def end-of-line "$")
(def word-boundary "\\b")
(def non-word-boundary "\\B")
(def anything ".")
;; etc ...
論理オペレーター
(defn non-capturing-group-s [s] (format "(?:%s)" s))
(defn and [& args]
(-> (map str args) str/join non-capturing-group-s re-pattern))
(defn or [& args]
(->> (map str args) (str/join "|") non-capturing-group-s re-pattern))
上記のand,orでは、任意個数の文字列あるいはPatternオブジェクトを受け取り、対応する正規表現を返します。
(def re(or (and alpha digit) (and digit alpha)))
re
;; => #"(?:(?:\p{Alpha}\d)|(?:\d\p{Alpha}))"
(re-matches re "1a")
;; => "1a"
(re-matches re "a1")
;; => "a1"
(re-matches re "aa")
;; => nil
以下、同様のやり方で正規表現の文法に対応する関数を作っていきます。
quantifier,set,groupなど
(defn + [re] (re-pattern (str re "+" )))
(defn * [re] (re-pattern (str re "*")))
(defn ? [re] (re-pattern (str re "?")))
(defn set [& args] (re-pattern (format "[%s]" (str/join (map str args)))))
(defn except [& args] (re-pattern (format "[^%s]" (str/join (map str args)))))
(defn group [re] (re-pattern (format "(%s)" re)) )
(defn repeat
([n re] (re-pattern(format "%s{%s}" re n)))
([n m re](re-pattern (format "%s{%s,%s}" re n m))))
(defn at-least [n re] (re-pattern (format "%s{%s,}" re n)))
簡単な使用例
これまで定義した関数を使って、冒頭で触れた日付をパースする正規表現を作ってみます。
(def sep (and (* space) "," (* space)))
(def date
(and
(group (+ word))
sep
(group (+ word))
(+ space)
(group (+ digit))
sep
(group (+ digit))))
(re-matches date "Friday, December 27, 2019")
;; => ["Friday, December 27, 2019" "Friday" "December" "27" "2019"]
この例では、groupでグループ化を行い、「曜日」、「月」、「日」、「年」に相当する部分を取り出しています。
まとめ
EmacsLispとClojureはともにLispに属する言語なので、やはり互換性が高いと言えるでしょう。一般に、多くの言語に触れ、良いと思った機能を他言語で実装できることは素晴らしいと思います。とくにClojure含むLisp系の言語はマクロがあるため、DSL(Domain Specific Language)開発の可能性が開かれています。今回はマクロは使いませんでしたが、個人的にClojureマクロによるDSLの可能性に大きな魅力を感じています。