昨日の apply, unapply ネタの続き。 って言うか、ほんとは昨日の時点でこれも一緒に書くつもりだったのに、何かいろいろやりながら書いてたらすっぽり忘れてたという。
シーケンス型のオブジェクトに対しては、パターンマッチ時にシーケンスパターンというのが使える。 どういうものかと言うと、一言で言えば『長さが可変のパターン』かな。 パターンマッチを持っている言語なら、大抵はプリミティブなシーケンス型 (通常はリストだろう) に対して同様のものが使えて、例えば OCaml なら、
# match [1;2;3;4;5] with | x::y::z::rest -> Printf.printf "%d %d %d\n" x y z | _ -> ();; 1 2 3 - : unit = ()
こんな風に書けたりする。 ただ、普通こういう書き方は、あらかじめ言語側で用意されている型にしか使えなくて、自分で何か新しく型を作った場合には使えない。 unapplySeq はそれを可能にしてくれるものだ。
ちなみに、Scala で上記のものと同様のことをしたとすると、こんな感じ。
scala> List(1,2,3,4,5) match {
| case List(x,y,z,_*) => printf("{0} {1} {2}\n",x,y,z)
| }
1 2 3
unnamed0: Unit = ()
_* は変数名じゃなく、『残り全部』を表わす特別なパターン。 これを変数に束縛したい場合は name @ _* のように書く。 @ はパターンの塊に名前を付けるもので (OCaml で言えば as)、次のような例にも使える。
scala> (1,2) match {
| case tup @ (x,y) => {printf("{0}\n{1}\n{2}\n",tup,x,y)}
| }
(1,2)
1
2
unnamed1: Unit = ()
さて、それじゃあ unapplySeq の使い方だけど、基本的には unapply と何ら変わりは無い。 単に、シーケンス型の値を返せば良いだけだ。
scala> object MyList {
| class MyList[T](l:List[T]) { val value = l }
| def apply[T](xs:T*) = new MyList(xs.toList)
| def unapplySeq[T](x:MyList[T]) = Some(x.value)
| }
defined module MyList
scala> MyList(1,2,3,4,5) match {
| case MyList(x,y,z,rest @ _*) => printf("{0} {1} {2} {3}\n",x,y,z,rest)
| }
1 2 3 List(4, 5)
unnamed7: Unit = ()
ちなみに、unapply でも unapplySeq でも、Option 型を返すことになっているけど、これは『マッチしない』ことを表わすために None を返せるようにということ。
逆に言えば、上記の例のように Some の値だけを返す定義なら、MyList(...) というパターンを使ったときに、unapplySeq の引数である MyList 型の値が来れば、必ずマッチするということでもある。
(追記)微妙に嘘だな、これ。unapply/unapplySeq によって分解されたあとの値がマッチするかどうかも当然関係する。例えばパターンに定数を指定していれば、そこで一致しなきゃマッチしない。
マッチしないときに None を返すというのを利用すれば、複雑なガード条件を一つのパターンとしてまとめてしまうこともできる。 例えば、
scala> def f(x:int) = x match {
| case y if y > 0 && y % 2 == 0 => 'ok
| case _ => 'ng
| }
f: (int)Symbol
こういう関数があったとして (x が 0 より大きくて、2 で割り切れる場合 'ok を返す)、この 'ok を返すようなパターンを何度も利用したいなら、
scala> object PositiveEven {
| def unapply(x:int) = if (x > 0 && x % 2 == 0) Some(x) else None
| }
defined module PositiveEven
こんなのを定義しておくと、
scala> def g(x:int) = x match {
| case PositiveEven(_) => 'ok
| case _ => 'ng
| }
g: (int)Symbol
scala> g(10)
unnamed9: Symbol = 'ok
scala> g(-10)
unnamed10: Symbol = 'ng
scala> g(3)
unnamed11: Symbol = 'ng
こんな風に書けるようになる。
以前話題にした件について、どうなったかなーと思って調べてみたんだけど、やっぱり同じように無限ループする。
scala> val infs = Stream.from(0) infs: Stream[Int] = Stream(0, ?) scala> val filts = infs.filter(_ < 10) filts: Stream[Int] = Stream(0, ?) scala> val takes = filts.take(10) takes: Stream[Int] = Stream(0, ?) scala> takes.length Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
どうも、こういう境界バグは見つけてしまうと何とかせずにいられないというか、前回はソースを展開する前にスルーしたから良かった(?)んだけど、今回は concurrent パッケージを調べたせいで、すでにソースが展開されている状況で、ついついソース読んじゃったんだよ...orz
ちなみに Stream クラスは (というか、実際には Stream のインスタンスを作成する Stream.cons.apply 関数だが) 例のすぐに評価されないブロックを利用して遅延評価な仕組みを作っている。 型で書くと、こう↓
apply[A](hd: A, tl: => Stream[A]) : Stream[A]
で、tl のブロック (特別 { } で囲まなくても、式一つでも同じ扱いだけど) は、Stream クラスの tail メソッドが呼ばれたときに始めて評価される。
そんな感じで、ざっとソースを眺めていくと、結局のところ take メソッドがよろしくないっぽい。 take の実装は↓こんな。
% cat scala/Stream.scala ... 245 override def take(n: Int): Stream[A] = 246 if (n == 0) Stream.empty 247 else Stream.cons(head, tail.take(n-1)) ...
これだと、filter されたリストの最後の要素で、触らなくても良い tail を触ってしまう。 結果、その tail は (もう絶対にマッチしない条件だから) 無限ループなので、length で最後の要素まで実際に計算されると、無限ループになる。 ……うまく説明できないな。 単純化した例で書けば、
scala> val s = Stream.from(0).filter(_ == 0) s: Stream[Int] = Stream(0, ?)
まず、こういうリストがあるとしよう。 これは、0 から始まる無限リストを、要素が 0 のものだけフィルタリングしたものなので、構造としては一番頭の要素が 0 で、それ以降のリスト (tail) は値が無い (計算しようとすると無限ループする) リストになっている。 ただ、この時点では tail の部分の評価は遅延されているので、何も起こらない。 そして、これを take(1) で切り取ってみる。
scala> val t = s.take(1) t: Stream[Int] = Stream(0, ?)
これが現在の実装だと、どのような構造になっているか考えると、
Stream.cons(0, tail.take(0))
こうで、このリストに length を使えば、(empty が出るまで再帰するので) 当然 tail.take(0) を評価することになって、結果 (tail は無限ループするので) 無限ループになってしまうというわけ。
そんでまあ、結局この問題を解決するには take の実装を↓こんな風に変えれば良いと思う。
scala> def take[T](self:Stream[T])(n:int):Stream[T] = {
| n match {
| case 0 => Stream.empty
| case 1 => Stream.cons(self.head, Stream.empty)
| case _ => Stream.cons(self.head, take(self.tail)(n-1))
| }
| }
take: [T >: ? <: ?](Stream[T])(int)Stream[T]
scala> val s = Stream.from(0).filter(_ < 10)
s: Stream[Int] = Stream(0, ?)
scala> take(s)(10).length
unnamed0: Int = 10
実際はインタンスメソッドとして実装するわけだから、その辺はそのように直すにしても、ともあれこれで例の問題は解決する。 得意の一行パッチを書けば、こんな感じか。
--- Stream.scala.orig 2007-06-25 17:06:09.000000000 +0900
+++ Stream.scala 2007-06-25 17:06:53.000000000 +0900
@@ -244,6 +244,7 @@
*/
override def take(n: Int): Stream[A] =
if (n == 0) Stream.empty
+ else if (n == 1) Stream.cons(head, Stream.empty)
else Stream.cons(head, tail.take(n-1))
/** Returns the stream without its <code>n</code> first elements.
つーかねー、実のところ、これでもまだ不満があって、空リストに対して take を呼んだときの対処がされてないのが気持ち悪いんだよなー。
scala> val s = Stream.range(0,5).take(10)
s: Stream[Int] = Stream(0, ?)
scala> s.length
java.util.NoSuchElementException: head of empty stream
at scala.Stream$$anon$0.head(Stream.scala:29)
at scala.Stream$$anon$0.head(Stream.scala:27)
at scala.Stream$class.take(Stream.scala:247)
at scala.Stream$$anon$0.take(Stream.scala:27)
at scala.Stream$$anonfun$9.apply(Stream.scala:247)
at scala.Stream$$anonfun$9.apply(Stream.scala:247)
at scala.Stream$cons$$anon$1.tail(Stream...
そりゃねーよ、と。 ちなみに Haskell なら、こう↓ね。
Hugs> take 10 [0..4] [0,1,2,3,4] Hugs> length (take 10 [0..4]) 5
いや、例えばこの NoSuchElementException とやらを for 文が配慮してくれるなら良いよ?
scala> for (x <- Stream.range(0,5).take(10)) println(x)
0
1
2
3
4
java.util.NoSuchElementException: head of empty stream
at scala.Stream$$anon$0.head(Stream.scala:29)
at scala.Stream$$anon$0.head(Stream.scala:27)
at scala.Stream$class.take(Stream.scala:247)
at scala.Stream$$anon$0.take(Stream.scala:27)
at scala.Stream$$anonfun$9.apply(Stream.scala:247)
at scala.Stream$$anonfun$9.apply(Stream.scala:247)
at scala.Stream$cons$$anon$1.tail(Stream...
してくれないじゃない。 こんなんじゃ、安心して使えないよね。 どうしたって遅延リストと言ったら Haskell のものをイメージしちゃうわけだし、同じように使えないのはツライ。
しかし、この現在の状態って単に手抜きなのか、何かしらポリシーがあるのか、どっちなんだろうなあ。 空リスト云々なんて、単に空リストなら空リストを返すっていうパターンを入れれば良いだけなんだから、手を付けてないってことは、敢えてそうしてるって気がしなくもないんだけど、そうだとして、そうする根拠って何だろう。 うーん、わからん。
向井さんとこ読んで、「待ってくれー!!」と思ったので書いとく。 それ Spotlight ならできるヨ。
一部のディレクトリはインデックスにつっこんでほしくない、
/.Spotlight/_rules.plist を編集しませう。以下例。
...(略)
<dict>
<key>EXCLUDE</key>
<array>
<string>/opt/local/var/db</string>
</array>
...(略)
</dict>
...(略)
うちの過去記事も併せてどぞ。
パスといえば、特定のパスの下だけ検索したいケースというのもあると思うけど、
『場所:/hoge fuga』とか。 あるいは Finder で目当てのフォルダを開いて、Finder ウインドウの検索欄を使用 (もしかしてデフォだと出てなかったかも知れないけど、その場合は速攻で『ツールバーをカスタマイズ...』推奨)。 『場所』とか打つのダセー (or ダルー) と思う場合は、『場所』の代わりに『where』でも可 (もしかすると、Unlocalize.mdimporterが必要だったかも)。 コマンドラインで mdfind 使うなら -onlyin オプションが便利。(see 'man mdfind')
via del.icio.us/otsune.
英語モードにすると快適だよ…という話。 実は、前々からなんとなくそんな気はしてたんだけど、実際に試したことはなかった。 やってみようかな。
あーなるほど、 Finder の検索ウィンドウが使えるのですね。それは知りませんでした。<br>あと、設定ファイルの類は覚えるのが正直めんどうくさくなってまして……と思ってみたら、システム環境設定から、これは設定できますねえ。失礼しました。<br># Google Desktop は Spotlightの設定を流用するという設定になっているので、ようするにどちらもできると。うーむ気付いていなかった
Unlocalize はそこには影響していないと思います。あとインデックスから外すのはシステム環境設定 > Spotlight > プライバシーでも変更できたはず。