Learn you a Clojure for Great Groovy!

最近話題のタイトルは、たのしく読んでいる途中(9章に入った)。
その話ではなく、このエントリは Clojure で Ninety-Nine Prolog Problems をやっているときに思いついたもの。


Ninety-Nine Prolog Problems も Lists のところだけ、現在 Groovy, Haskell, Scala, Prolog, Clojure で解いた。*1
Lists の前半のクライマックスは encode-direct 辺りだが Groovy に較べて Clojure は簡潔に書けてしまう。

;; 1.13
(defn encode-direct [s]
  (for [[n c] (map (juxt count first) (partition-by identity s))] (if (= 1 n) c [n c]) ))
(assert (= [[4 \a] \b [2 \c] [2 \a] \d [4 \e]] (encode-direct "aaaabccaadeeee") ))


これは Haskell の group 関数にあたるものが Clojure に存在するためだ。

Prelude> :m + Data.List
Prelude Data.List> group "aaaabccaadeeee"
["aaaa","b","cc","aa","d","eeee"]


つまり 1.09 の pack が Clojure では

;; 1.09
(defn pack [s] (partition-by identity s))
(assert (= [[\a \a \a \a] [\b] [\c \c] [\a \a] [\d] [\e \e \e \e]] (pack "aaaabccaadeeee") ))


で、Groovy では *2

// 1.09
def pack(list) {
  if (!list) return []
  list = list.reverse()
  list.tail().inject([[list.head()]]){ acc, v -> acc.head()[0] == v ? [[v,*acc.head()],*acc.tail()] : [[v],*acc] }
}

assert ["aaaa","b","cc","aa","d","eeee"] == pack("aaaabccaadeeee".toList())*.join()


Groovy にも Collection#split という同じインターフェイスのメソッドが存在するが振る舞いが違う。
Groovy の split は、引数のクロージャで true と false に分類しているのに対し
Clojure の partition-by は、引数のクロージャで変換した結果が1つ前のグループと同じかどうかで分類している。


これは便利だ。いつもなら partition-by を実装するところだが Clojure はオープンなので貸してもらうことにする。*3

@Grab(group='org.clojure', module='clojure', version='1.4.0')
@Grab(group='org.apache.commons', module='commons-lang3', version='3.1')
import clojure.lang.Compiler
import clojure.lang.Symbol
import clojure.lang.RT
import static org.apache.commons.lang3.StringUtils.removeEnd
import static org.apache.commons.lang3.StringUtils.splitByCharacterTypeCamelCase


def methodMissing(String name, args) {
  def names = splitByCharacterTypeCamelCase(name)*.toLowerCase()
  if (names[0] == "is") {
    names = names.tail()
    names[-1] = names[-1] + "?"
  }
  return Compiler.eval(Symbol.create(names.join("-"))).invoke(*args)
}

def propertyMissing(String name) {
  // Why?
  // Caused by: java.lang.RuntimeException: Unable to resolve symbol: out in this context
  if (name == "out") return System.out
  return Compiler.eval(Symbol.create(name))
}

def pack(s) {
  partitionBy(identity, s)
}

def encode1(s) {
  map(juxt(count, first), pack(s))
}

def encode2(list) {
  pack(list).collect{ [it.size(),it.head()] }
}

assert ["aaaa", "b", "cc", "aa", "d", "eeee"] == pack("aaaabccaadeeee")*.join()
assert [[4,'a'], [1,'b'], [2,'c'], [2,'a'], [1,'d'], [4,'e']] == encode1("aaaabccaadeeee")
assert [[4,'a'], [1,'b'], [2,'c'], [2,'a'], [1,'d'], [4,'e']] == encode2("aaaabccaadeeee")
assert isZero(0)
// Why?
// groovy.lang.MissingPropertyException: No such property: lang for class: clojure
// assert 'a' == clojure.lang.RT.first("abc")
assert 'a' == RT.first("abc")

encode1 は Groovy でも encode2 のように書けるのでメリットがあるわけではないが他の関数も借りられますよということで書いてみた。
propertyMissing の振る舞いがよく分かっていないので Why? が2箇所発生している。
clojure.lang.RT のメソッドなら直接呼び出すことも可能なようだ。
Symbol を毎回生成して eval しているがこれはキャッシュに保持してもいいかもしれない。
改善点はあると思うが Clojure の関数は Groovy から簡単に呼び出せたよ、ということが言いたかった。

*1:Prolog では 28 がまだだが

*2:Scala も探したけど見当たらない。Scala では foldRight + パターンマッチで切り抜けた

*3:Groovy もオープンだ