はじめに
Twitter の TL でジャンケンを“オブジェクト指向”でというネタを見かけて楽しそうだったので、私も Smalltalk でチャレンジしてみました。
この場合の“オブジェクト指向”は、アラン・ケイの「オブジェクトがメッセージを送り合って」というタイプのオブジェクト指向ではなく、ビャーネ・ストラウストラップの「抽象データ型をクラスを使って実現する」タイプのオブジェクト指向のことを指しているのだと思います。したがって、何をどんなふうにクラスで表現するかがミソになるのではないかと想像します。
まずは、きっかけとなったこちらの二つの Java と C# で書かれたコードについて
- Javaでじゃんけん[オブジェクト指向](以下 lrf141さん版)
- オブジェクト指向ジャンケン: または石やハサミや紙を手クラスから派生させる是非について(同 sitou1986さん版)
恥ずかしながらこれらの動きがいまひとつわからなかったので、ぞれぞれをまず Smalltalk で書き直してみました。もちろん Smalltalk は Java や C# 等と比べてたいへんシンプルな言語で、これらの言語のような豊富な機能はないため、やむを得ず意訳のようになった部分は多々ありますがどうぞあしからず。
なお、お示ししたコードは、いちおう、日本語表示可能な設定を終えた Squeak もしくは Pharo という Smalltalk環境向けに書いてはいますが(日本語表示設定は、たとえば Pharo ならこちらが参考になるかと)、慣習で用いられる「クラス名 >> メソッド定義」という簡約記法で記述しているためそのままでは動作しません。実際に動かすには、クラスやメソッド定義をシステムブラウザなどを用いて個別にコンパイルする必要があります。
日本語の表示の設定はともかく、コードの入力などにはあまり手間をかけずにとりあえず動かしてみたい…という向きには、併記した .mcz ファイルのリンクを用意しましたのでそれをダウンロードして、Squeak もしくは Pharo のデスクトップ画面にドロップイン後、ポップアップメニューから Load version することでインストールし動作の確認(Obj7 example や Rule example を do it )が可能です。
lrf141さん版 をそのまま移植した版
Object subclass: #Obj7
instanceVariableNames: 'gu choki pa rnd play1 play2'
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-pobj'
Obj7 >> initialize
gu := 1.
choki := 2.
pa := 3.
rnd := Random new
Obj7 >> play1
^play1
Obj7 >> play2
^play2
Obj7 >> player1
| what |
what := rnd nextInt: 3.
what caseOf: {
[1] -> [play1 := gu].
[2] -> [play1 := choki].
[3] -> [play1 := pa].
}
Obj7 >> player2
| what |
what := rnd nextInt: 3.
what caseOf: {
[1] -> [play2 := gu].
[2] -> [play2 := choki].
[3] -> [play2 := pa].
}
Obj7 class >> example
"Obj7 example"
| obj play1win play2win |
obj := self new.
play1win := 0.
play2win := 0.
Transcript open.
1 to: 3 do: [:i |
obj player1.
obj player2.
Transcript cr; show: '------', i printString, '試合目-----'.
Transcript cr; show: 'play1 = ', obj play1 printString.
Transcript cr; show: 'play2 = ', obj play2 printString.
Transcript cr; show: '-----------------'.
(obj play1 = 1 and: [obj play2 = 2])
ifTrue: [play1win := play1win + 1]
ifFalse: [(obj play1 = 2 and: [obj play2 = 3])
ifTrue: [play1win := play1win + 1]
ifFalse: [(obj play1 = 3 and: [obj play2 = 1])
ifTrue: [play1win := play1win + 1]
ifFalse: [obj play1 = obj play2
ifFalse: [play2win := play2win + 1]
]
]
]
].
play1win > play2win
ifTrue: [Transcript cr; show: 'winner player1']
ifFalse: [play1win < play2win
ifTrue: [Transcript cr; show: 'winner player2']
ifFalse: [Transcript cr; show: 'draw']
]
各々のプレイヤーの手を生成して保持するのに Obj7 を使っていて面白いですね。ただ、個人的には出す手を生成するだけならここまで複雑にする(プレイヤーごとに同じ内容のメソッドを生やす、とか、そもそも出す手を保持しておく)必要はないように思ったので、次のようにに書き換えてみました。
lrf141さん版 のおおよその動きをクラスを使わずに再現した版
| shapes player1win player2win player1shape player2shape |
shapes := #(グー チョキ パー).
player1win := 0.
player2win := 0.
Transcript open.
1 to: 3 do: [:i |
Transcript cr; show: '------', i printString, '試合目-----'.
Transcript cr; show: 'プレイヤー1 = ', (player1shape := shapes atRandom).
Transcript cr; show: 'プレイヤー2 = ', (player2shape := shapes atRandom).
Transcript cr; show: '-----------------'.
(player1shape = #グー and: [player2shape = #チョキ])
ifTrue: [player1win := player1win + 1]
ifFalse: [(player1shape = #チョキ and: [player2shape = #パー])
ifTrue: [player1win := player1win + 1]
ifFalse: [(player1shape = #パー and: [player2shape = #グー])
ifTrue: [player1win := player1win + 1]
ifFalse: [player1shape = player2shape
ifFalse: [player2win := player2win + 1]
]
]
]
].
player1win > player2win
ifTrue: [Transcript cr; show: 'プレイヤー1の勝ち']
ifFalse: [player1win < player2win
ifTrue: [Transcript cr; show: 'プレイヤー2の勝ち']
ifFalse: [Transcript cr; show: '引き分け']
]
※このコードは Workspace (Squeak の場合。Pharo ではPlayground)へコピペしてあらためて全選択 → 右クリックメニューから do it で動作します。
グー、チョキ、パーはシンボルで表現して配列 shapes に保持、出す手は shapes atRandom で必要なときに選ぶしくみに変えて、出した手は playerNshape に代入し、後の勝敗判定に参照できるようにしてあります。
sitou1986さん版 をそのまま移植した版
Object subclass: #Hand
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Hand'
Hand >> printOn: stream
self subclassResponsibility
Hand subclass: #Choki
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Hand'
Choki >> printOn: stream
stream nextPutAll: 'チョキ'
Hand subclass: #Gu
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Hand'
Gu >> printOn: stream
stream nextPutAll: 'グー'
Hand subclass: #Pa
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Hand'
Pa >> printOn: stream
stream nextPutAll: 'パー'
Object subclass: #Comparer
instanceVariableNames: 'from to result'
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Comparer'
Comparer >> from
^from
Comparer >> result
^result
Comparer >> to
^to
Comparer subclass: #Wins
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Comparer'
Wins >> from
^from
Wins >> result
^result
Wins >> to
^to
Wins >> initialize
result := true
Wins >> setFrom: handClass1 to: handClass2
from := handClass1.
to := handClass2
Wins class >> from: handClass1 to: handClass2
^self new setFrom: handClass1 to: handClass2; yourself
Comparer subclass: #Reverse
instanceVariableNames: 'source'
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Comparer'
Reverse >> initialize: hand
source := hand.
from := source to.
to := source from.
result := source result not
Reverse class >> source: source
^self new initialize: source; yourself
Object subclass: #Rule
instanceVariableNames: 'matches availableHands'
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sitou1986-Rule'
Rule >> createHand
^availableHands atRandom new
Rule >> find: triplet
| hand1 hand2 expectedResult |
hand1 := triplet first.
hand2 := triplet second.
expectedResult := triplet last.
^ (matches at: (self generateMatchKey: {hand1 class. hand2 class}) ifAbsent: [])
ifNotNil: [:rule | rule result = expectedResult ifTrue: [rule]]
Rule >> generateMatchKey: hands
^'{1}=>{2}' format: hands
Rule >> setRules: comparers
matches := ((comparers gather: [:rule | {rule. Reverse source: rule}])
groupBy: [:rule | self generateMatchKey: {rule from. rule to}]
having: #notEmpty) associations
inject: Dictionary new into: [:dic :group |
dic at: group key put: group value last; yourself].
availableHands := (comparers gather: [:rule | {rule from. rule to}]) asSet asArray
Rule >> win: pair
^((self find: {pair first. pair second. true})
ifNil: [self find: {pair second. pair first. false}]) notNil
Rule >> play: hands
| winners |
winners := hands select: [:hand |
hands allSatisfy: [:target | target == hand or: [self win: {hand. target}]]].
^winners size = 1 ifTrue: [winners first]
Rule class >> new: rules
^self new setRules: rules; yourself
Rule class >> rules: rules
^self new setRules: rules; yourself
Rule class >> example
"Rule example"
| numOfPlayers rule |
Transcript open.
numOfPlayers := (UIManager default request: 'プレーヤー人数を入力してください:') asInteger.
numOfPlayers ifNil: [^self].
rule := Rule new: {Wins from: Gu to: Choki. Wins from: Choki to: Pa. Wins from: Pa to: Gu}.
[ | hands winner |
hands := Array streamContents: [:ss |
(1 to: numOfPlayers) do: [:i |
| hand |
hand := rule createHand.
Transcript cr; show: ('プレーヤー{1}: {2}' format: {i. hand}).
ss nextPut: hand
]
].
winner := rule play: hands.
winner
ifNotNil: [
| index |
index := hands indexOf: winner.
Transcript cr; show: ('プレーヤー{1}の勝ちです' format: {index})]
ifNil: [Transcript cr; show: '引き分けです'].
Transcript cr; endEntry.
(UIManager default confirm: '終了しますか?') ifTrue: [^self]
] repeat
こちらは型の機能を使った複雑なつくりになっていて、前者にくらべて動きを把握するのにかなり苦労しました。ジェネリックスでジャンケンの手の組み合わせとどちらが強いかをパラメーターとして与えているのが興味深いです。最小限のルールだけ与えて、あとの逆の組み合わせは Reverse 型で自動的に生成する遊び心も楽しげですね。
ただ個人的にはやはりクラスでやる理由がいまひとつ弱く感じました。Wins は手の強弱の情報を持たせられれば組み込みデータ型で代替可能ですし、加えて Reverse でわざわざ辞書に逆の組み合わせを登録する必要もよく考えるとなさそうです。せっかく Hand から派生したジャンケンの手も、ラベル程度の意味にしか使われていないのも残念です。結局、手の組み合わせとそのときの勝者がどちらになるかを辞書で管理できれば、それで済んでしまうのではないかな…と試しに書いたのが次のコードになります。
sitou1986さん版 のおおよその動きをクラスを使わずに再現した版
| numOfPlayers superior availableHands winnerOf |
numOfPlayers := (UIManager default request: 'プレーヤー人数を入力してください:') asInteger.
numOfPlayers ifNil: [^self].
superior := #((グー チョキ) (チョキ パー) (パー グー))
inject: Dictionary new
into: [:dic :pair | dic at: pair asSet put: pair first; yourself].
availableHands := superior values.
winnerOf := [:hands |
| handsSet winner |
handsSet := hands asSet.
handsSet size = 2 ifTrue: [
winner := superior at: handsSet.
"(hands occurrencesOf: winner) = 1 ifTrue: [winner]"
]
].
Transcript open.
[ | hands |
hands := (1 to: numOfPlayers) collect: [:idx | availableHands atRandom].
hands doWithIndex: [:hand :idx |
Transcript cr; show: ('プレーヤー{1}: {2}' format: {idx. hand}).
].
(winnerOf value: hands)
ifNotNil: [:winner |
| winnerIndics |
Transcript cr; show: 'プレーヤー'.
winnerIndics := (1 to: hands size) select: [:idx | (hands at: idx) = winner].
winnerIndics
do: [:idx | Transcript show: idx]
separatedBy: [Transcript show: ', '].
Transcript show: 'の', winner, 'の勝ちです'
]
ifNil: [Transcript cr; show: '引き分けです'].
Transcript cr; endEntry.
(UIManager default confirm: '続けますか?') ifFalse: [^self]
] repeat
※このコードは Workspace (Squeak の場合。Pharo ではPlayground)へコピペしてあらためて全選択 → 右クリックメニューから do it で動作します。
辞書の内容を少し変更して、勝敗を判定したい二つの手の組み合わせ(Set)をキーにして、そのときに勝つ手を値とすることで Comparer 以下のクラスや generateMatchKey: メソッドは不要に、ジャンケンの手もやはりシンボルで表現し Hand 以下もお役ご免となりました。
勝敗判定処理だけ残りましたが、それも Rule クラスの存在意義には弱いので、winnerOf ブロック(無名関数。いわゆるクロージャー)として独立させています。
ジャンケンの手をあえてクラスにするなら自分ならこう書く版
そんなわけで、あえて、特にジャンケンの手をクラスとして表現するとき、自分ならどう書くかを考えてみたのが次のコードです。
Magnitude subclass: #JankenHand
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sumim'
JankenHand >> name
self subclassResponsibility
JankenHand >> superior
self subclassResponsibility
JankenHand >> < other
^other class = self superior
JankenHand >> = other
^other class = self class
JankenHand >> hash
^self class name hash
JankenHand >> printOn: stream
stream nextPutAll: self name
JankenHand subclass: #JankenChoki
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sumim'
JankenChoki >> name
^#チョキ
JankenChoki >> superior
^JankenGu
JankenHand subclass: #JankenGu
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sumim'
JankenGu >> name
^#グー
JankenGu >> superior
^JankenPa
JankenHand subclass: #JankenPa
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Janken-sumim'
JankenPa >> name
^#パー
JankenPa >> superior
^JankenChoki
JankenHand class >> example
"JankenHand example"
| numOfPlayers availableHands |
Transcript open.
numOfPlayers := (UIManager default request: 'プレーヤー人数を入力してください:') asInteger.
numOfPlayers ifNil: [^self].
availableHands := JankenHand subclasses.
[ | hands |
hands := (1 to: numOfPlayers) collect: [:idx | availableHands atRandom new].
hands doWithIndex: [:hand :idx |
Transcript cr; show: ('プレーヤー{1}: {2}' format: {idx. hand}).
].
hands asSet size ~= 2
ifTrue: [Transcript cr; show: '引き分けです']
ifFalse: [
| winner winnerIndics |
winner := hands max.
Transcript cr; show: 'プレーヤー'.
winnerIndics := (1 to: hands size) select: [:idx | (hands at: idx) = winner].
winnerIndics
do: [:idx | Transcript show: idx]
separatedBy: [Transcript show: ', '].
Transcript show: 'の', winner printString, 'の勝ちです'
].
Transcript cr; endEntry.
(UIManager default confirm: '続けますか?') ifFalse: [^self]
] repeat
遊び心としては、ジャンケンの手の強弱を不等号による比較演算による大小で判定できるように、ジャンケンの手の抽象クラス JankenHand は Magunitude のサブクラスとしてみました。Magunitude は他の言語では Comparable という抽象クラス(あるいはインターフェイス)で表現される比較可能なオブジェクトを表わします。Smalltalk では < と = と hash をオーバーライドすることで Magunitude として振る舞うことができます。= は引数のクラスと比較、hash はオブジェクトの文字列表現のために用意した name メソッドの返値から算出することにします。
残りの < は、引数となるジャンケンの手との力関係で「負け」を true として返せればよいので、引数が属するクラスが、自分が負ける手のクラスを返す superior メソッドの返値と同じかどうかの結果を返す実装にしました。
これで「グー > チョキ」が true を返すようになると同時に、Magnitude に定義されている残りのメソッド群が使えるようになります。つまり単純に比較(による勝敗の判定)ができるようになったというだけではなく、たとえば二種類の手がコレクション(上のコードでは hands)に収められているとき、勝つ手はどちらかを「hands max」とシンプルに書くだけで判断が可能になることも意味します。
追記: このコードの C# 版を見よう見まねで書いてみました。
おまけ
そもそも今回のジャンケンだとか、よく取り上げられる FizzBuzz というのは、わざわざクラスを使うまでもなくシンプルに書くことができるテーマです。
| janken player1 player2 |
janken := #(グー チョキ パー).
player1 := janken atRandom.
player2 := janken atRandom.
^player1 = player2
ifTrue: ['両者', player1, 'で引き分け']
ifFalse: ['{1} vs {2} でプレイヤー{3}の勝ち' format:
{player1. player2. (player1 = (janken, janken after: player2)) asBit + 1}
]
"=> 'グー vs パー でプレイヤー2の勝ち' "
冒頭の前提はさておき、オブジェクト指向をケイのメッセージングの OOP ととらえている私のような人間には、クラスを新しく定義しなくとも、メッセージングを介したオブジェクト群の協働として表現されたこのジャンケンもまた、立派な“オブジェクト指向”のジャンケンだと思えるので、クラス設計の善し悪しと「オブジェクト指向か否か」というのはまた別の話のような気はしました。