最長重複文字列問題 in Groovy


WEB+DB PRESS Vol.67 の最長重複文字列問題をやってみた。
もう少し前に実装していたのだけど takeWhile 待ちしている間に目的が変わってしまった。
ClojureScala では既に実装されている方がおられる。

なので参考にしながら Groovy で書いたものを ClojureScala に書き換えていった。
Java から Groovy への敷居が低いのは、文法が似ているからだ。
同じことが、関数プログラミングにも言えて、Groovy で関数プログラミングのスタイルで書ければ、他の関数プログラミングスタイルの言語も書けてしまう。
ClojureScala を最初から詳しく知る必要はなく、まずは Groovy と行き来できる範囲で使えれば後は自然に覚えられるはず。

問題を少し変えてみる

考えてみるとこのアルゴリズムは文字列である必要がないので数列など他の型にも対応することにした。
そもそも元ネタの Haskell では Char を a に置き換えて Ord a => を追加するだけで対応できてしまう。
他の言語もかくありたい。

Groovy

もう待てないので takeWhile 欲しさに Groovy Version: 2.0.0-beta-3 にバージョンアップした。
一応 takeWhile も実装したので、takeWhile のないバージョンの場合は maxDupStr を実行する際にカテゴリで囲めば動作する。

tails     = { xs, acc=[] -> xs.empty ? acc << xs : trampoline(xs.tail(), acc << xs) }.trampoline()
listAsc   = { xs, ys -> [xs, ys].transpose().findResult{ it[0] <=> it[1] ?: null } ?: xs.size() <=> ys.size() }
sort      = { it.sort(listAsc) }
makePair  = { it ? it.collate(2, 1, false) : [] }
calcLen   = { it.collect{ xs, ys -> lenstr(xs, ys) } }
lenstr    = { xs, ys -> [comlen(xs, ys), xs] }
comlen    = { xs, ys -> [xs, ys].transpose().takeWhile{ it[0] == it[1] }.size() }
chooseMax = { it.max{ it.first() } }
extract   = { it ? it[1].take(it[0]) : null }
maxDupStr = extract << chooseMax << calcLen << makePair << sort << tails

assert maxDupStr("mississippi".toList()).join() == "issi"
assert maxDupStr([8,10,8,10,12,8,10,8,10,12,8]) == [8,10,8,10,12,8]

異なる型でも動作させるので引数はリストを想定した。
しかし Groovy にはダックタイピングがあるので文字列をリストに変換する必要はないかもしれない。

文字列渡し

class CharSequenceOps {
  static def head(CharSequence cs) { cs[0] }
  static def tail(CharSequence cs) { cs[1..<cs.size()] }
  static def transpose(List css)   { GroovyCollections.transpose(css*.asType(List)) }
  static def takeWhile(List list, Closure pred) {
    list.findIndexOf{ !pred(it) }.with{ i -> i < 0 ? list : list.take(i) }
  }
}

use (CharSequenceOps) {
  assert maxDupStr("mississippi") == "issi"
  assert maxDupStr("a") == ""
  assert maxDupStr("") == null
}


惜しいところでだめだった。
head と tail は getAt でも OK なのだが transpose が動作してくれない。

groovy:000> "abc" as List
===> [a, b, c]
groovy:000> (List) "abc"
ERROR org.codehaus.groovy.runtime.typehandling.GroovyCastException:

Groovy には Java 風のキャストと as によるキャストがあるのだが transpose は Java 風のキャストを使っていたからだ。


Groovy では、文字列も配列もリストも size() で長さが取得できるが、これは分かりやすさだけでなくダックタイピングには重要なことなのだ。
CharSequence にも head や tail があれば、Java の String は List ではないが List のように扱えてしまうから。
扱えなかったので、CharSequenceOps に head と tail を実装し、タプルの意味なら getAt でアクセスするようにした。

マルチメソッド

def maxDupStr(List xs)  { extract << chooseMax << calcLen << makePair << sort << tails << xs }
// def maxDupStr(List xs)  { return { [xs] } >> tails >> sort >> makePair >> calcLen >> chooseMax >> extract << [] }
def maxDupStr(String s) { maxDupStr(s.toList())?.join() }
maxDupStr = this.&maxDupStr

assert maxDupStr("mississippi") == "issi"
assert maxDupStr([8,10,8,10,12,8,10,8,10,12,8]) == [8,10,8,10,12,8]

これは Clojure から参考にした対応。
Groovy でクロージャではマルチメソッド (動的なオーバーロード) を定義する方法がない(知らないだけ?)が、メソッドで定義してからクロージャにすればマルチメソッドは有効みたいだ。

Scala からのフィードバック

// Comparator でデフォルト引数を取れることに気づいた
listAsc = { xs, ys, i=0 -> xs.size() < i && ys.size() < i ? 0 : xs[i] <=> ys[i] ?: call(xs, ys, i+1) }

// trampoline を知った
listAsc = { xs, ys, i=0 -> xs.size() < i && ys.size() < i ? 0 : xs[i] <=> ys[i] ?: trampoline(xs, ys, i+1) }.trampoline()

// Scala で実装されていた List Comparator を Groovy で実装してみた
listAsc = { xs, ys -> [xs, ys].transpose().find{ it[0] <=> it[1] }?.with{ it[0] <=> it[1] } ?: xs.size() <=> ys.size() }

// findResult を使うようにした
listAsc = { xs, ys -> [xs, ys].transpose().findResult{ it[0] <=> it[1] ?: null } ?: xs.size() <=> ys.size() }

上の2つが以前から使っていたもの。その下が今回知ったもの。
他の言語で実装すると違う実装法を知るチャンスも増える。
なぜ List の Comparator を言語で提供してくれないのかはなぞなのだが。


あとなぜデフォルト引数が取れるか裏もとった。

    public static <T> List<T> sort(Collection<T> self, boolean mutate, Closure closure) {
        List<T> list = mutate ? asList(self) : toList(self);
        // use a comparator of one item or two
        int params = closure.getMaximumNumberOfParameters();
        if (params == 1) {
            Collections.sort(list, new OrderBy<T>(closure));
        } else {
            Collections.sort(list, new ClosureComparator<T>(closure));
        }
        return list;
    }

one or two と書いてあるがこの実装なら three も OK だ。
MaximumNumberOfParameters の存在は以前から知っていたが、なぜ Maximum かは最近知った。

コンパイルエラー

英語で思い出した。
以前、困ってそうな外国人が話しかけてきたのだが、ネイティブすぎて全然聞き取れない。
ネイティブでなくても多分聞き取れないのだが、ネイティブなのでジェスチャーで意味が分かった。
どうやら何かが動かなくなって困っているようだ。
壊れたの?って言おうと思って習った英語を組み立てた。

I understand. Are you broken?

NOOOOOOOO!

English のコンパイルエラーは激しかった。
ClojureScalaコンパイルエラーは英語ほどではないので安心してチャレンジできる。