LoginSignup
4
4

More than 5 years have passed since last update.

オブジェクト指向ジャンケンをSqueak/Pharo Smalltalkで書いてみる

Last updated at Posted at 2016-08-13

はじめに

Twitter の TL でジャンケンを“オブジェクト指向”でというネタを見かけて楽しそうだったので、私も Smalltalk でチャレンジしてみました。

この場合の“オブジェクト指向”は、アラン・ケイの「オブジェクトがメッセージを送り合って」というタイプのオブジェクト指向ではなく、ビャーネ・ストラウストラップの「抽象データ型をクラスを使って実現する」タイプのオブジェクト指向のことを指しているのだと思います。したがって、何をどんなふうにクラスで表現するかがミソになるのではないかと想像します。

まずは、きっかけとなったこちらの二つの Java と C# で書かれたコードについて

恥ずかしながらこれらの動きがいまひとつわからなかったので、ぞれぞれをまず Smalltalk で書き直してみました。もちろん Smalltalk は Java や C# 等と比べてたいへんシンプルな言語で、これらの言語のような豊富な機能はないため、やむを得ず意訳のようになった部分は多々ありますがどうぞあしからず。

なお、お示ししたコードは、いちおう、日本語表示可能な設定を終えた Squeak もしくは Pharo という Smalltalk環境向けに書いてはいますが(日本語表示設定は、たとえば Pharo ならこちらが参考になるかと)、慣習で用いられる「クラス名 >> メソッド定義」という簡約記法で記述しているためそのままでは動作しません。実際に動かすには、クラスやメソッド定義をシステムブラウザなどを用いて個別にコンパイルする必要があります。

日本語の表示の設定はともかく、コードの入力などにはあまり手間をかけずにとりあえず動かしてみたい…という向きには、併記した .mcz ファイルのリンクを用意しましたのでそれをダウンロードして、Squeak もしくは Pharo のデスクトップ画面にドロップイン後、ポップアップメニューから Load version することでインストールし動作の確認(Obj7 example や Rule example を do it )が可能です。

lrf141さん版 をそのまま移植した版

Janken-lrf141.st
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さん版 をそのまま移植した版

Janken-sitou1986.cs.st

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 ブロック(無名関数。いわゆるクロージャー)として独立させています。

ジャンケンの手をあえてクラスにするなら自分ならこう書く版

そんなわけで、あえて、特にジャンケンの手をクラスとして表現するとき、自分ならどう書くかを考えてみたのが次のコードです。

Janken-sumim.st
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 ととらえている私のような人間には、クラスを新しく定義しなくとも、メッセージングを介したオブジェクト群の協働として表現されたこのジャンケンもまた、立派な“オブジェクト指向”のジャンケンだと思えるので、クラス設計の善し悪しと「オブジェクト指向か否か」というのはまた別の話のような気はしました。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4