S-JIS[2011-07-03] 変更履歴

Scala 異なるものを検索する実験

Scalaコレクション内から異なっている値を検索する方法を考えてみる。


お題

Twitterで、異なる文字を探す遊びがたまに見られる。

difference_Dbot 【間違い探し(上級)】脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳腦脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳脳 できたらRT

人間が目で探すからゲームになるんだけど、こういう作業をコンピューターにやらせたいと思うのがプログラマー(笑)


以降では、変数sにこの文字列が入っているものとする。

val s = "脳脳〜〜脳脳"

なお、WindowsのREPLでは、-Xnojlineを指定してscalaを起動しないと漢字が入れられないので注意。


重複排除

一番簡単にコーディングできるのは、重複排除。

scala> s.distinct
res1: String = 脳腦

どの場所が違うかは分からないけれど、違っている文字は一目瞭然w


違っている文字の探索(仮定版)

こういう間違い探しの場合、1文字目が違っていることはまず無いので、1文字目はたくさんある文字だと仮定できる。
すると、コレクション内から違うものを探すのはfilterNotメソッドなので(別にfilterでもいいけど)、

scala> val C = s(0)
C: Char = 脳

scala> val D = s.filterNot(_ == C)
D: String = 腦

scala> val D = s.filter(_ != C)
D: String = 腦

s(0)」は、文字列sの先頭1文字を取ってきている。
これは「s.apply(0)」を省略した記法だが、中身としては「s.charAt(0)」と同じ。


ちなみに、「_ == C」の部分は、「C ==」と書くことも出来る。

scala> val D = s.filterNot(C ==)
D: String = 腦

これは、"abc".foreach(println)と同じ構造。
foreachにprintln関数を渡しているのと同様に、filterNotに「C==」という関数を渡している。
対比させてみれば一目瞭然。

"abc".foreach{ c => println(c) } s.filterNot{ c => c==C } foreachやfilterNotから引数cを受け取り、それを使って処理する。
s.filterNot{ c => C==c } ==の場合、左右の項を入れ替えても同じ結果になる。
"abc".foreach(println(_)) s.filterNot(C == _)
s.filterNot(C.==(_))
引数が一回しか使われない場合、プレースホルダー「_」に置き換えることが出来る。
また、関数呼び出しのピリオドと丸括弧は省略できる
"abc".foreach(println)
"abc".foreach(Predef.println)
s.filterNot(C==)
s.filterNot(C.==)
そして、引数を省略して、関数名そのものを渡すことが出来る。
このとき、関数の属しているオブジェクトも一緒に指定できる。
(printlnの場合は、普通はPredefを省略している。暗黙にインポートされているから)

違っている文字の探索(素直版)

1文字目がたくさんある文字だという仮定はあくまで仮定なので、それに頼らない方法を考えてみる。

文字ごとに出現数を数え、最も出現数が少ない文字を出す。

文字ごとにグルーピングする。 s.groupBy(c=>c) groupByで、コレクション内の要素毎にMapが作られる。
出現数でソートする。 s.groupBy(c=>c).values.toArray.sortBy(_.size) valuesで同一文字群を取り出し、文字数(出現数)でソートする。
valuesは(Iterableだから)sortByを持っていないので、toArrayで配列に変換している。
Charに変換しておく。 val D = s.groupBy(c=>c).values.toArray.sortBy(_.size).head.charAt(0) headでソート後の先頭の文字列(出現数が一番少ないもの)を取り出し、
charAtでCharに変換している。

ところで、groupByでちょっとハマったので、メモ。

scala> "aabaa".groupBy(c => c)
res21: scala.collection.immutable.Map[Char,String] = Map(a -> aaaa, b -> b)

groupByに渡している「c => c」は、引数cを一度しか使っていないからプレースホルダー「_」に置き換えられると思った。

scala> "aabaa".groupBy(_)
<console>:9: error: missing parameter type for expanded function ((x$1) => "aabaa".groupBy(x$1))
       "aabaa".groupBy(_)
                       ^

ところがエラーになって、最初はエラー位置だけ見て「なんで?」と思ったけれど、
nouvelleluneさんに教わって改めてエラーメッセージを見てみると、関数リテラルが表現されている。

「x$1」がプレースホルダー部分の変数を表しており、これを変数cに置き換えて見れば、
(c) => "aabaa".groupBy(c)」という解釈をしようとして、その変数の型が推論できないからエラーになっているようだ。

groupByの引数が演算の形になっていれば こちらの思っている通りに解釈されるらしい。

scala> "aabaa".groupBy(_.toChar)
res23: scala.collection.immutable.Map[Char,String] = Map(a -> aaaa, b -> b)

あと、pomu0325さんによると、「c => c」のように値が変わらない関数が欲しい場合は(Predefで定義されている)identity関数を使って書けるそうだ。

scala> "aabaa".groupBy(identity)
res24: scala.collection.immutable.Map[Char,String] = Map(a -> aaaa, b -> b)

違っている場所の出力

最後に、どこが違っていたのかを出力する。

indexOfやindexWhereを使えば位置(インデックス)の数値は出る。

scala> s.indexOf(D)
res31: Int = 76

scala> s.indexWhere(_ == D)
res32: Int = 76

が、これでは実際の場所は認識できないので(苦笑)、異なる文字以外を他の文字に置き換えることにする。
(Cは沢山ある文字、Dは異なっている文字)

s.map{ c => if (c == D) c else '□' }
s.map{ c => if (c != C) c else '□' }
s.map{ case c if c == D => c; case _ => '□' }
s.map{ case D => D;           case _ => '□' }
s.map{ case C => '□';        case c => c }
s.replace(C, '□')

replaceを使う方法は、pomu0325さんに指摘されるまで気付かなかった^^;
新しく知ったこと(Scalaのコレクション・map関数)を使うことばっかり考えて他の方法を忘れてしまう、初心者にありがちなミス(苦笑)


Scala実験へ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま