コレクションクロージャメソッド

bliki_ja で、いつになく Smalltalk への言及が多い(…わりに、おいしいところはぜんぶ Ruby に持っていかれている orz)ので、あらためて Squeak システムの Smalltalk で実際に評価可能な例を添えて書き直してみました。

#do: (Ruby の each)

employees do: [:employee | employee doSomething]
例:
World findATranscript: nil.
(1 to: 10) do: [:each | Transcript cr; show: each printString]

#select: , #reject:

managers := employees select: [:employee | employee isManager]
例:
($a to: $z) select: [:each | each isVowel]   " => #($a $e $i $o $u) "
'squeak' reject: [:each | each isVowel]      " => 'sqk' "

#collect:

offices := employees collect: [:employee | employee office ]
例:
(1 to: 10) collect: [:each | each romanString]
=> #('I' 'II' 'III' 'IV' 'V' 'VI' 'VII' 'VIII' 'IX' 'X')

#allSatisfy: , #anySatisfy:(Ruby の all? , any? ), #noneSatisfy:

allManagers := employees allSatisfy: [:employee | employee isManager]
noManagers := (employees anySatisfy: [:employee | employee isManager]) not
noManagers := employees noneSatisfy: [:employee | employee isManager]
例:
'smalltalk' allSatisfy: [:each | each isLowercase]   " => true "
'smalltalk' anySatisfy: [:each | each isVowel]       " => true "
'smalltalk' anySatisfy: [:each | each isDigit]       " => false "
'Smalltalk-80' anySatisfy: [:each | each isDigit]    " => true "
'smalltalk' noneSatisfy: [:each | each isDigit]      " => true "
'Smalltalk-80' noneSatisfy: [:each | each isDigit]   " => false "

#detect:ifNone: , #detect:

volunteer := employees 
                detect: [:employee | employee hasSteppedForward]
                ifNone: [employees atRandom]
例:
'smalltalk' detect: [:each | each isVowel]                  " => $a "
'smalltalk' detect: [:each | each isDigit] ifNone: [$0]     " => $0 "
'Smalltalk-76' detect: [:each | each isDigit] ifNone: [$0]  " => $7 "

#sort:

sortedEmployees := employees sort: [:a :b | a lastname <= b lastname]

SqueakSmalltalk には sort_by に相当するメソッドはない。

例:
'squeak' sort: [:a :b | a <= b]     " => 'aekqsu' "
'squeak' sort                       " => 'aekqsu' "

Ruby 同様、#sort を起動しても同じ(デフォルトの比較ブロックは [:a :b | a <= b ] なので)。ただし、sort と sort: [...] はそれぞれ別のメソッドを起動するメッセージとして区別され、引数を省略(あるいは追加)した記述…というわけではないので注意が必要。さいごの補足の項も参照されたい。

'squeak' sort: [:a :b | a > b]      " => 'usqkea' "

#doWithIndex: (Ruby の each_with_index), withIndexDo: , keysAndValuesDo:

例:
'squeak' doWithIndex: [:each :index | Transcript cr; show: (index -> each) printString]
'squeak' withIndexDo: [:index :each | Transcript cr; show: (index -> each) printString]
'squeak' keysAndValuesDo:  [:key :value | Transcript cr; show: (key -> value) printString]
=> 1->$s
   2->$q
   3->$u
   4->$e
   5->$a
   6->$k

#doWithIndex: と #withIndexDo: の違いはブロック変数の順序だけ。また、#keysAndValuesDo: はもともと a Dictionary 用のメソッド。a SequenceableCollection ではインデックスをキーに見立てて機能する(結果、#withIndexDo: と振る舞いは同じ)。

| dictionary |
World findATranscript: nil.
dictionary := {#apple -> Color red. #lemon -> Color yellow. #melon -> Color green} as: Dictionary.
dictionary keysAndValuesDo: [:key :value | Transcript cr; show: (key -> value) printString]
=> #apple->Color red
   #lemon->Color yellow
   #melon->Color green

#inject:into:

total := employees inject: 0 into: [:result :employee | result + employee salary]
例:
(1 to: 10) inject: 0 into: [:sum :each | sum + each]  " => 55 "
'smalltalk' inject: Set new into: [:set :each | set add: each; yourself]
=> a Set($l $m $a $s $t $k)

二番目は #inject:into: の動きを利用した応用例。なお、#yourself は何もせず、たんにレシーバを返すだけのメソッド。Smalltalk には、Collection >> #add: など、コレクションに対して要素の追加や削除を行なうメソッドはその返値に、コレクション(レシーバ)ではなく、追加や削除した要素を返す…という暗黙の了解がある。#inject:into: では、ブロックの評価値が次のブロック評価の第一ブロック変数の値として利用されるので [:set :each | set add: each] だけだとうまくいかない。そこで、カスケード(;)を用いた yourself の送信で改めてレシーバ(この場合、a Set)をブロックの評価結果として返させている。[:set :each | set add: each. set] としても同じ。

負け惜しみ

SqueakSmalltalk には、Ruby の partition と同じことをするメソッドは用意されていない。が、#groupBy:having: を使用すれば似たようなことは可能。(でも、こんなことは普通しない。w)

managersAndPlebs := employees groupBy: [:employee | employee isManager] having: [:dummy | true].
managers := managersAndPlebs at: true ifAbsent: [#()].
plebs := managersAndPlebs at: false ifAbsent: [#()]
例:
'squeak' groupBy: [:each | each isVowel] having: [:dummy | true]
=> a PluggableDictionary(false->an OrderedCollection($s $q $k) true->an OrderedCollection($u $e $a) )

なお、SqueakSmalltalk は、その前身である Apple Smalltalk 時代に追加された多重代入をのちに改めて(…というのも Smalltalk-72 のごく初期のバージョンでもいっとき多値が扱えたらしいのでw)廃止したので partition の例で示されているようなことは今はできない。(2.7 より前のバージョンでは {a. b} := {b. a}のように変数の入れ換えなども可能だった)

補足

ファウラーは do、collect、select などと“:”を省いてしまっているが、Smalltalk 的には、たとえば #select と #select: とは明確に区別される(つまり、“:”まで含めてメソッド名である)ので、こうした呼称は推奨されない。なお、# は付けても付けなくてもいい(付けない人のほうが多いけど、私はメソッドとメッセージを区別するのに便利なので付けることにしている 例:#sort はメソッド。sort は #sort の起動を期待して送るメッセージ。じっさいに起動してくれるかどうかは、レシーバしだい(^_^;))。

追記

原文がじゃっかん書き直されたようです。重複、不一致についてはご容赦あれ、かし。 余談ですが、今回追加された次のような言及は、Smalltalk 変態文法信奉者には我が意を得たりってかんじでとても嬉しいですね。

Ruby's syntax is nice for methods that take a single block as an argument, but I find it rather more clunky for multiple blocks. Smalltalk (according to my questionable memory) would look like this.

volunteer := employees 
               detect: [:each| each hasSteppedForward]
               ifNone: [self pickVictim]

As usual Smalltalk's keyword parameters make it much easier to read a multi-argument method.

ちなみに detect: [...] ifNone: [...] は、これでひとつのメッセージで、#detect:ifNone: という名前のメソッドを起動します。detect: や ifNone: をキーワードと呼ぶこともありますが、これはあくまで便宜的なもので、LISP(や、将来の Ruby )のように“キーワード”という(たとえば、ifNone: といった引数オプションのようなものが)エンティティとして存在するわけではありません。参考までに、#detect:ifNone: を起動する式を(Ruby 風味の)普通の文法で直訳気味に書くと次のようになります。

employees.detect:ifNone:(lambda{|e| e.steppedForward?}, lambda{self pickVictim})

なお、Smalltalk では、文脈によってメソッド名のことを“セレクタ”と呼ぶこともあります。