見出し画像

Retro-gaming and so on

ババ抜きを作ろう 補足

補足として、洒落でPython版も作ってみた。
人によってはやっぱりLispより「フツーのプログラミング言語」で記述した方がREPLと言うモデルの「明解さ」が伝わるだろう、からだ。
これは基本的にはRacketの丸ごとのPythonへの移植である(が端末でプレイする事を想定している)。



Python原作と見比べてみて欲しい。
多分オブジェクト指向で書く、と言うのとREPLと言うモデルで書く、と言うモノの違いがハッキリと分かるだろう。
と、同時に「C言語脳」で書いたPythonのコードと「Lisp脳」で書いたPythonのコードの違いも分かる筈である(笑)。Lisp脳だとすぐreturnして、そしてそのreturnされるブツが異様に長い、とか思うだろう(改行入りまくりだし!・笑)。
ただし、マジな話、Pythonが本来想定してるのは「Lisp脳で書かれるような」コードなのである。少なくともそれを許容するような機能を持つ言語、としてデザインされているのだ。

さて、これは基本的にはRacket版のベタ移植なのだが、当然それだけでは済まないトコは済まない。
よって「工夫が必要」なトコを含めたポイントを箇条書きにしていこう。

1. Pythonには構造体がない -> 代わりにclassを使え

Pythonには構造体がないので代わりにclassを使おう。
ただし、メソッドを実装しなければclassはまんま構造体と変わらん、と言う話はいつか書いた通りである。
ちなみに、Python原作では元々getCardStr関数を作った時点でインスタンスの表示を作成する、としてたが、本来ならクラス設計時に__repr__と言う特殊メソッドを作る事によって表示がコントロール出来る。

class Card(object):
 '''トランプのオブジェクト'''
 def __init__(self, suit, number):
  self.suit = suit
  self.number = number

 # この特殊メソッドでインスタンスを「どう表示するか」決める事が出来る
 def __repr__(self):
  return f"[{self.suit}:{self.number}]"

なお、Python原作ではクラス変数も使われてるが、これは個人的にはどーにも気持ち悪いので使わなかった(※1)。

2. Pythonは多値としてタプルを返す

Pythonでも多値関数のような挙動をエミュレートする事が可能だ。

>>> def foo():
   return 1, 2

>>> foo()
(1, 2)
>>>

実際は多値ではないが、タプル、として値を返してくる。
ただし、Pythonにはアンパックと言う機能があって、扱いやすさから言うとLispより扱いやすいかもしれない。

>>> x, y = foo()
>>> x
1
>>> y
2
>>>

3. Pythonではmap関数やfilter関数の代わりにリスト内包表記を使う

これも何度も指摘している。そして、リスト内包表記を使わないようなPythonプログラムはクリープを入れないコーヒーのようなものだ。もっとも俺はコーヒーにクリープを入れた事はねぇけどな(爆
冗談はさておき、さぁ、気づくだろ。このPythonプログラムには一回もfor文は出てこない(※2)。以前書いたけど、プログラムの繰り返し処理の80%以上はリスト内包表記があれば何とかなるって言った通りになってんだろ?それ以外に出てくる繰り返し構文はwhileのみ、だ(こっちは末尾再帰の代替、だ)。
Pythonでの関数型プログラミングスタイル、ってのはこんなカンジに落ち着くのである。
分かったろ?リスト内包表記を使いまくれ。もっと言うとリスト内包表記が使えない、と言う事はワタシはリスト内包表記を使うだけの脳みそを持ってません、って言ってるのと同じなのである。
言い過ぎだと思う?違う、プログラミング言語が抽象性が高いある機能を持たせてる理由はそれを使った方がラクだから、なのだ。それ以外に特にエラソーな理由は存在しない(殆ど唯一の例外で、「なんでこんな機能がある」のかパッと見意味不明なのは、Schemeの継続、くらいなモンである)。
つまり、ラクになる筈の機能が使えない、ってのは単に人間として間違ってるわけだ。
お分かり?
いずれにせよ、Lisp使いは「とにかくmapしまくれ、そしてreduceしまくれ」を合言葉のようにしている(※3)。
Pythonも「とにかくリスト内包表記しまくれ、そしてreduceしまくれ」なのだ。
習うより慣れろ、である。食わず嫌いをせずに使いまくって練習しまくるのだ。

4. Pythonのitertools.groupbyにはちとクセがある

Racketのgroup-byにあたるのがPythonのitertools.groupbyである。
この2つはほぼ同じ機能なのだが、Python版にはちとクセがある。

  1. Racket版はそのまま使えるが、Python版は受け取るイテラブルはソート済みである必要がある。
  2. Racket版はグルーピングされたリストをただ返すだけだが、Python版はグルーピングの際に「何の指標でグルーピングされたのか」を示すキーを先頭に付け足した上、実際のグループもまたイテラブルになっている。
従って、グルーピングした結果を上手い具合取り出すにはちと工夫が必要になる。
具体的にはリスト内包表記をする際に、取り出す値をキーとイテラブル、としてアンパックしながら取り出しキーを無視する。そして要素として得られるイテラブルを改めてリスト内包表記を用いてリスト化しないとならない。
なんでこんな設計にしてるのか分からん、ってのが正直なトコだ。

5. Pythonでの集合演算は集合(set)を使う

Lispでは集合演算の時、リストをそのまま集合としてみなすが、一方、Pythonではリストとはまた別に集合型(あるいはSet型)と呼ばれる型が独立して存在してる。
従って、Pythonで集合演算をする場合、一旦リストをSet型に変換し、何らかの集合演算をした後にリスト型に戻す、と言うひと手間が必要だ。

6. itertools.zip_longestについて

Python組み込みのzip関数では、長さの違う2つのリストがあった場合、イテラブルは短い方の長さに合わせて生成される。

>>> list(zip([1, 2, 3], [4, 5, 6, 7]))
[(1, 4), (2, 5), (3, 6)]

一方、itertools.zip_longestは、逆に長い方に合わせてイテラブルを生成するのだ。

>>> list(zip_longest([1, 2, 3], [4, 5, 6, 7]))
[(1, 4), (2, 5), (3, 6), (None, 7)]
>>>

カードのディーリングに於いて、Racket版はカードデッキに余ったカードの枚数に従って、末尾再帰しつつplayersリストからplayerを引っ張ってこなければならなかったが、Python版はitertools.zip_longestがあるお陰で「余りの枚数(これは常にプレイヤー数より少なくなる)」を気にしなくても良くなってる。
結果、Noneじゃない限り、新たなカードと各player(つまりカードのリスト)を簡単に結合する事が出来る。
それにより、Python版の方がシンプルになってる、と言えるだろう。

7. fold関数の代わりにfunctools.reduceを使え

Schemeのfold、Racketのfoldlの代わりに使えるのがPythonのfunctools.reduceだ。
これもリスト内包表記程出番があるわけではないが、重宝する。
リスト内包表記じゃ上手くいかなさそうな時、真っ先に思い浮かべなければならない関数である。
ババ抜きプログラムではここで使われている。

def shuffle_list(lst, n):
 '''カードをシャッフルする'''
 return reduce(lambda x, y: sample(x, len(lst)), range(n), lst)

これも、forでループを書きたくない、から使われてるのである(笑)。

「無理せんで素直にforでループを書いたら?」

と思う向きもあるだろう。意地でも書かない、ように見えるんだろうか(笑)。
いや違うんだ。書かない、んじゃなくって書きたくない、のである。
この「書きたくない」ってのはぶっちゃけ、プログラミング初心者の「forループを書きたくない」って思う気持ちと根っこは同じなんだよ。
言い換えると「考えるだけでメンド臭い」って事なんだ。あと長いし。
関数型言語ユーザーは、要するに

forでループを書かなきゃならない時は、よっぽど低レベルな操作が必要になる時」

だと思ってるんだ。で、なるたけそういう低レベル操作はしたくないんだよ。間違える可能性も高いしね(笑)。「ウキーッ」ってなっちゃう。なるんだって(※4)。
一方、高階関数を使えばセグフォ的な結果を見る事はない。仮にルーピングが失敗したにせよ「あ、これは俺の責任だ」と割り切れる。
いずれにせよ、リスト内包表記やら高階関数を愛用する最大の理由は「それらを理解してる俺ってアタマがイイだろ?」じゃねぇんだ。単に怠惰だから使ってる、ってのが本当のトコだ。
関数型言語ユーザーってのは「怠け者」なんだ。分かった(笑)?
そして怠け者になるため、便利機能を使えるように勤勉に努力し、死力を尽くす、のである(笑)。
なお、Python原作ではshuffleと言う関数が使われていたが、こっちは破壊的変更を伴うので、関数的であるsampleを用いている。こだわり、と言うのならこっちの方を敢えてこだわっている。

8. Pythonには連想リストがないのでハッシュテーブル(辞書型)を使え

これはもう表題のまんま、である。
「メッセージ分離方式」を行う以上、プログラム上にある種の「データベース」を持たせなければならないが、Racket版では古典的な連想リストを使う部分でPythonはハッシュテーブルを使う。
なお、本当だったらRacket版でもハッシュテーブルを使って構わないケースではあった。
ただし、連想リストと違い、ハッシュテーブルは(今回はやらなかったが)データの変更をする際は基本「破壊的変更」を要する。
連想リストの場合、破壊的変更も出来るが、通常、破壊的変更をしないデータ型であり、その辺がハッシュテーブルと(検索の遅さと相まって)違うトコロ、なのである。
Lispでは一般に、連想リストでデータテーブルを設計しておいて、プログラム完成時にボトルネックになってた際にのみハッシュテーブルへとすげ替える。
一方、Pythonでは関数型プログラミング「っぽい」プログラムは書けるが、原則「破壊的変更welcome」な言語である。
よって、最初っからハッシュテーブルを使って構わない。そもそもわざわざ連想リストの検索関数を作るのもメンド臭いしね。

9. なかなか便利なenumerate関数

Racket版では、プレイヤー番号を出力する際に、playersリストの長さを調べて番号用のリストを生成する必要があった。
一方、Python版ではそのテの作業をする必要はない。enumerate関数がそのテの作業を一手に引き受けてくれるから、である。

>>> list(enumerate(["foo", "bar", "baz"]))
[(0, 'foo'), (1, 'bar'), (2, 'baz')]
>>>

ご覧の通り、enumerateはリストの要素番号と組み合わせたイテラブルを返してくれる(だから中身を見る為には一旦リスト化しないとならない)。

余談だが、リストはそもそもC言語なんかの配列を扱う方法と違って要素自体が「自らを何番目か知る」のは非常に難しくなっている。
と言うより、C言語なんかではそもそも「要素番号を使うプログラミング」が前提な為、配列を舐める際にも「要素番号」はいつでも明確になる、明確にしなければならない、と言った特徴がある。
一方、Lispや、あるいは昨今流行りのイテレータなんかでは「取り出される要素が重要なんであって格納番号は重要じゃない」と言う設計になっている。Pythonのforなんかもそういう思想を反映した設計となってるわけだ。
そういう思想は大方正しいのだが、こう言った「要素番号を知りたい」と言った場合、逆に面倒臭い状態に陥るのだ。C言語の配列操作以上にリストの要素は「自らの格納位置を知る」手段が存在しない。
Pythonのenumerate関数はそういう「リストの弱み」を上手くフォロー出来るような関数となっている(もちろん自作しようと思えばRacketでも自作可能だ)。

> (define (enumerate lst)
  (map (lambda (x y)
     (cons x y))
    (range(length lst)) lst))
> (enumerate '(foo bar baz))
'((0 . foo) (1 . bar) (2 . baz))
>



ちなみに、ANSI Common Lispにはリストの格納位置を知るpositionと言う関数が用意されていて、ホント、痒いトコに手が届くようなライブラリになっている。

CL-USER> (position 'bar '(foo bar baz))
1
CL-USER>

ANSI Common Lispには盲点がない。ホント何でも用意されているのだ。

10. リストの分割にはスライスを徹底して使え

Racketではsplit-atを多用してリストを任意の場所で分割していた。
しかし、正直言うと、Pythonではもっとラクなカタチでリストを分割出来る。スライスだ。
Lispのリストは強力で、アタマから全体を「舐める」際にはこれ程優れたシステムは存在しない、と思われる。
ただし、それは「先頭から順次見ていく場合」に限る話であって、一方、「任意の位置で分割したい」等という、ちと配列染みた扱いをせなアカンくなると、元々の設計思想から言ってその辺は得意ではない、と言う印象を受ける。そもそも記述法がスマートにならない、のだ。
一方、Pythonのリストは一般的な配列型の記述法を採用している上に、構文上のショートカットがたくさんある。
ハッキリ言って、プログラミング言語として、どっちが学習上負担にならない「シンプルな記述法を採用してるのか」と言うと間違いなくLispなのだが、反面、ショートカット的な記述がたくさんあって、覚える量がありつつも書いたプログラム自体がシンプルになるのはPythonの方なのだ。
以下、知ってる人にとっては当たり前だが、Lispのリスト操作とPythonのリスト操作の対比を記述しておく。なお、リストをlstとしている。

  • (car  lst) -> lst[0]
  • (cdr lst) -> lst[1:]
  • (take lst i) -> lst[:i]
  • (drop lst i) -> lst[i:]
  • (split-at lst i) -> lst[:i], lst[i:]
  • (cons x lst) -> [x] + lst
  • (append lst0 lst1) -> lst0 + lst1
割にこう、Pythonの方が記述的には一本槍になってるのが分かるだろう。

11. Pythonには循環リストがない

Lispのリストはあらゆる意味で万能である。コンスセルで構成されたシンプルなデータ構造であるが故に、循環リスト等と言うワケが分からない(ハッキリ言って危険な)モノまで例外なく扱う事が可能となっている。
一方、実のことを言うとPythonのリストはconsではない。どっちかと言うと、可変長ではあるけれども、構造的にはSchemeで言うvectorに近い、と言うのがPythonのリストなんだ。
従って、Pythonのリストではconsを使ったような自在なマジックは存在しない。結果、循環リストのようなヘンなリストは作れないし、ドットペアも作れないわけだ。
と言う事は、Racket版で行ったような循環リストを利用したプレイヤーの循環は書けない事になる。
しょーがないんで、次のようなトリックを使う。
例えば、循環リストではないけど、clist = [0, 1, 2, 3]と定義したとしよう。
そうすると、これを参照対象として「循環する要素」を引っ張ってくる場合には次のように記述する。

clist[i % len(clist)]

これを利用すると、循環リストと同じような計算結果を見る事が出来る。

>>> clist[0 % len(clist)]
0
>>> clist[1 % len(clist)]
1
>>> clist[2 % len(clist)]
2
>>> clist[3 % len(clist)]
3
>>> clist[4 % len(clist)]
0
>>> clist[5 % len(clist)]
1
>>> clist[6 % len(clist)]
2
>>> clist[7 % len(clist)]
3
>>> clist[8 % len(clist)]
0
>>>

こういうハックを施しておくと、Racketの循環リストを用いたトリックを循環リストを使用せずに実現する事が可能だ(条件分岐を書くよりはマシだろう)。

とまぁ、これらがRacket版をPythonに移植する際のポイント、及び注意点だ。

最後に、もう一つ、洒落でオブジェクト指向版もちと書いてみた。
今までは、まるでオブジェクト指向とREPLが対立概念のように扱ってきた。
しかし、本当の事を言うと、それは正しくない。オブジェクト指向でもREPLを組み立てる事は可能、なのである。
逆に言うと、関数型言語でもREPLと言うモデルを採用しないとコードがワヤクチャになる、って事は充分ありえるわけだ。
そして、別に「オブジェクト指向にした」からと言って、その採用が「プログラムを組む上で見通しが良くなる」と言う事を保証するわけではない、のである(「オブジェクト指向だとコードの見通しが良くなる」って言いたがる人が多いが、それは「ウソだ」と言うのがここまで来れば分かるだろう)。
繰り返す。選択した「言語の指向」と「プログラミングの見通しの良し悪し」は関係ない。重要なのは「どう言ったプログラム構築モデルで書くか」と言う事なんだ。

さて、とは言ってもREPLと言うモデルでRead、Eval、Printの部分をクラスで書くのはあまり賢くない(※5)。「Pythonはオブジェクト指向を強要しない」前提だと、その辺はフツーに関数を書くべきである。Javaが広めたような

「何でもかんでもクラスにしてしまう」

ってのは事実上、あまりアタマの良いやり方ではないのだ(※6)。
またもやポール・グレアムの言葉を借りよう。

私自身の感覚としては、オブジェクト指向プログラミングは時には 有用なテクニックであるが、書かれる全てのプログラムがそれでなくちゃいけない なんていうものじゃない。新しい型を自分で定義できることは必要だけれど、 全てのプログラムを新しい型の定義として書かなくちゃならないってことはないだろう。 

これは全く正しいと思われる。
以前にも書いたが、C言語やPascalのエキスパートがC++やObject Pascal(あるいはDelphi)であまり悩まないのは、彼らにとってはオブジェクト指向のクラス、と言うのは単なる構造体、及びレコード型の便利な拡張に過ぎないから、である。だから彼らがオブジェクト指向を「使う」時は非常に抑制の効いた使い方をして別に全面的にオブジェクト指向にするわけでもない。
言い換えるとC++やObject Pascalはオブジェクト指向使える言語であって、別にオブジェクト指向言語ではないのだ(カッコイイ言い方をすると「マルチパラダイム」言語である)。
このオブジェクト指向版ババ抜きはそっちの方針で組み上げている。つまり、基本的に、構造体代わりに使っていたclassに真っ当なオブジェクト指向を用いるとどうなるのか、と言う実験代わりに書いた、と言う事だ。
従って、関数で済むトコロをわざわざオブジェクト指向で塗りつぶしたりはしていない。

Pythonによる関数型プログラミング版「ババ抜き」では次の2つを構造体代わりのclassで作っていた。

  • トランプのカード
  • 環境情報World構造体
このうち、トランプのカードはこれ以上特に何もする必要がないのでこのまま、としてる。
一方、World構造体はそもそも操作関数自体が多かった。故にそれらの「操作用関数」をWorld構造体ならぬWorldクラスのメソッドにすればどうだ、と言うのが一つの指針となる。
あとは次の2つをクラス化している。

・メッセージ用の文字列作成機構
・Playerクラスの作成

ぶっちゃけ、メッセージ文字列作成部分は関数だろうとクラスだろうとどっちでも良かった。重要度的には大して高くないわけだが、幸い初期化(__init__)メソッド(コンストラクタ、等と呼んだりするが)に必要な部分もあるし(メッセージ分離方式上作ったハッシュテーブルだ)、まとめる「形式」にするには都合が良かったのでまとめてしまった。
Messageクラスの各メソッドは基本、関数プログラミング版の文字列整形関数の流用ではあるが、次のPlayerクラスで使うが為に若干改造してある。また、print関数もMessageクラスとの整合性を取る為に改造が必要ではあるが、デバッグをかけながら改造すればそれ自体は大した手間もかからないだろう。
問題はPlayerクラスだ。
関数型プログラミングにおけるplayerは単なるカードのリストだった。これをPlayerと言うクラスに変更する、と言うのは割に悪くはないアイディアに思われる。
ただし、だ。メソッドをどうするのか、と言う問題がある。
カードを引く、カードを捨てる、と言う「動作」をメソッド化するのはそんなに悪くないだろう。しかしながら、もうちょっと考えなければならない部分がある。
それは「手札を見せる」「捨てたカードを見せる」のはPlayerクラスに属すべきか否か、と言う辺りだ。
これがオブジェクト指向のクラス設計の悩ましいトコロなのだ。
色んなトコで「クラス設計は自由で良い」と書いてある。しかしこの場合の「自由」は「わーい、自由だ、万歳!」と言う自由ではない。単にセオリーがない、とかノウハウがありません、って言ってる事と同義なのだ。
また、「現実はオブジェクトに溢れてるので現実をエミュレートするのがオブジェクト指向なので簡単になる」と言う事もアッチコッチで良く書かれている。
そうか?
例えば人間と言うクラスと車と言うクラスを考えよう。現実世界では人間が車に乗って走らせる。さて、プログラム上では「人が」走らせるのか、「車が」走ってるのか(笑)。こんなの一体どうすればいいのか分からんし、下手すれば人間には「アクセルを踏む」メソッドがあり、車には「アクセルが踏まれる」メソッドを実装しなきゃならなくなるだろう(笑)。そんな肥大化したプログラムを書きたいか?
そうなのだ。現実世界はむしろややこしいのだ。必要なのは「抽象化する」機能であって、「現実世界をエミュレートする」事ではないのだ。少なくとも一般に流通する「オブジェクト指向に関するアレコレ」にはこの辺の解決策は全く書かれていない。
他にもオブジェクト指向にはツッコみどころがあるが、取り敢えずそれらは置いておいて、今回のPlayerクラスは次のようにしてみた。

class Player(object):
 '''プレイヤーオブジェクト'''
 def __init__(self, cards):
  self.cards = cards
  self.card = False
  self.discarded = []
  self.message = Message()

 def drawCard(self, player2, pos):
  '''カードを引く'''
  self.card = player2.cards[pos]
  self.cards += [self.card]
  player2.cards = player2.cards[:pos] + player2.cards[pos + 1:]
  return player2

 def discardPair(self):
  '''カードを捨てる'''
  p = [j[0] for j in [list(i) for key, i in \
         groupby(sorted(self.cards, key = lambda x: x.number), \
             key = lambda x: x.number)] if len(j) != 2]
  self.discarded = list(set(self.cards) - set(p))
  self.cards = p
  return self

 def showCards(self, id):
  return self.message.showCards(id, self.cards)

 def showDiscardCards(self, id):
  return self.message.showDiscardCards(id, self.discarded)

まず「手札を見せる」「捨てたカードを見せる」のはPlayerクラスに属する事とした。それこそ現実世界でのプレイヤーの行動として考えれば辻褄は合うだろう。
ただし、そのために、メッセージクラスのインスタンスをPlayerクラスのインスタンス変数(self.message = Message())として利用する、事としている。メッセージクラス自体を継承する手も考えられるが、いくら何でもやり過ぎだろう、と言うんでやらんかった(そもそも、「プレイヤー」の性質から考えても本質的ではない)。
また、その関係上、元々World構造体にあった「手札を意味する」card変数と「捨てたカードを意味する」discardedはPlayerクラスに引っ越ししている。それらも合わせてPlayerクラスに属させた方がシンプルになる、と言う判断である。
ただし、そのせいで関数型プログラミングじゃ考えられない事も起きてくる。そう、破壊的変更だ。
元々、オブジェクト指向と非破壊的な関数型プログラミングは相性が悪い(※7)。そもそも、「ユーザー定義で作ったデータ型をデータ型自らが弄る」モデルだと、破壊的変更から逃れるのは困難になるのだ(※8)。
しかし、オブジェクト指向で書こう、と決めた以上、「郷に入りては郷に従え」ってな事でこの版では敢えて破壊的変更を行っている。
例えばPlayerクラスがcarddiscardと言うインスタンス変数を持った為、メソッドになったdrawCarddiscardPairは多値を返す必要がなくなった。クラスに属するcardcardsdiscardと言ったインスタンス変数を破壊的変更すれば済むようになったから、である。
そしてdrawCardはカードを引かれる相手、player2のカード(インスタンス変数cards)を破壊的変更し、変更されたplayer2を返り値とする。
奇しくも、ポール・グレアムが「多値が存在しないプログラミング言語と副作用の関係に対して」言ったような結果になっている。
さぁ、多値を返す関数型プログラミングと破壊的変更を多用するオブジェクト指向、どっちが皆さんの好みに合うだろうか(※9)。

もう一つは、クラス化したWorldである。ちとコードを見てみよう。

class World(object):
 '''環境'''
 def __init__(self, clist, pair, id1, id2, players):
  self.clist = clist
  self.pair = pair
  self.id1 = id1
  self.id2 = id2
  self.players = players

 def insert(self, i, j, player1, player2):
  self.players[i] = player1
  self.players[j] = player2
  return self

 def next_id(self):
  self.pair = [self.id1, self.id2]
  self.id1 = next_id(self.clist, self.id1 + 1, self.players)
  self.id2 = next_id(self.clist, self.id1 + 1, self.players)
  return self

def next_id(clist, i, players):
 while True:
  pos = clist[i % len(clist)]
  cards = players[pos].cards
  if cards != []:
   return pos
  i += 1

まずはメソッドinsertから。
関数型プログラミング版ではわざわざリストをスライスして新しくリストを構築していた。
Pythonを知ってる層には「直接リストの要素番号を指定して代入したらイイんじゃない?」と歯がゆい思いをしただろう。しかし、それはリストを直接破壊的変更するのを避ける為にそうしていた、のである。
しかし、クラス化したWorldにはそのテの気遣いは無用だろう。と言うわけで、直接要素番号を指定して代入する、言わば「フツーのプログラミング方法」に変更になっている。
メソッドnext_idはクラス外部のnext_id関数を利用して一気にself.id1self.id2破壊的変更している。このように、何らかの作業が複数にわたる場合、キモとなる「操作」はクラス外で関数として設計して利用する方が見通しが良い。クラス内で基本操作メソッドとして設計すると、ワヤクチャになる可能性が高いから、だ。
また、見たら分かるが、メソッド名と関数名が被っていても気にしないで良い。何故ならメソッド作成の一番の理由は、名前空間を汚さないトコにあるから、である。
メソッドnext_idの内訳は

  1. 今回のself.id1self.id2self.pairと言うインスタンス変数を破壊的変更しながら退避させる。
  2. self.id1next_id関数を用いて破壊的変更で更新する。
  3. self.id2next_id関数を用いて破壊的変更で更新する。
となってて、関数型言語版のeval(world_go)の仕事のいくつか、を肩代わりしてる。
そして、もう一つポイントがある。ここでworld_goの返り値を見てみよう。

return w.insert(id1, id2, player1.discardPair(), player2).next_id()

ちとPythonじゃ見慣れない書き方になっている。
このまま読むと

「インスタンスwinsertしてnext_idしたものを返せ」

になりそうだ。正解である。つまり、1個しかないインスタンスwメソッドが2回適用されているのだ。
これはPythonでは珍しい書き方になるが、プログラミング言語Rubyでは割に目にする書き方である。インスタンスに適用するメソッドが連鎖するのでまんまメソッドチェーン等と呼称する。
秘訣は、Rubyの場合はANSI Common Lispと同様、「必ず値を返す」設計になっていて、メソッドの場合、基本的にはインスタンスである自分自身を返すようになっている。
Pythonだと明示的に何らかをreturnしなければならないが、結局Rubyでは、Pythonのメソッドで言う第一引数、つまり、self、言い換えるとインスタンス自身を常に返すように設計されている。
自分自身であるインスタンスが常に返る、と言う事はその返り値にまたメソッドを適用する事が可能だ、と言う事だ。こうやってメソッドチェーンは実現されているのである。
Worldクラスには破壊的変更、つまり副作用目的であるinsertnext_idと言う2つのメソッドしかない、のだが、副作用目的と言っても、別に返り値があって悪い、ってこたぁねぇわけだ。
結果、両者ともreturn selfしてて、これでメソッドチェーンが実現するようになっている。そしてそのお陰でコードがシンプルになり、また、world_go内でwを再代入する必要もなく、そのまま返す事が出来たわけだ(何故ならreturn selfにより返り値はwと言うインスタンス自身になってる、ってのは自明だからだ)。
return selfそしてメソッドチェーンにより、world_goは、削除された部分があるのも事実だが、関数型言語版よりはかなりシンプルに仕上げる事が出来た。
同様に、PlayerクラスのdiscardPairreturn selfしている。と言う事はplayer1.discardPair()の返り値もplayer1になる。もうちょっと丁寧に言うと、player1.discardPair()の返り値は「ペアとなったカードを捨て去った」player1だ。だからそのままの記述でworld_goinsertメソッドに突っ込めた、のである。
この辺のトリックはオブジェクト指向で「記述を簡易化する」には効果的なテクニックだろう。取り敢えず、副作用目的のメソッドでもreturn selfしとく癖を付けておけば役に立つ事があるかもしれない、と言う話。ないかもしれんが(笑)。

取り敢えず、こんなトコ、かな。

※1: クラス変数は使いどころさえ間違えなければ強力な機能だが、一方、無自覚に使用すると「どのインスタンスからも参照可能な変数」の為、極めて危険な変数へと変容する。
以前書いたが、インスタンス変数でさえクラス内に於いての大域変数のように振る舞うのに、クラス変数だともっと強力な大域変数、に成りかねない。
関数型言語でクロージャに慣れてる層から見ると、非常に気持ち悪い変数ではあるのだ。
もっとも、Python原作ではそういった危険な使い方はされていなく、あくまで「参照先とするのみ」としてて、抑制が効いた使い方が成されている。

※2: そもそもRacket版にもいわゆるループ構文にあたる「末尾再帰」は殆ど出てこない。Lispでも慣れてくると、再帰の使用比率より高階関数の使用率の方が上がっていき、結果mapだらけ、になるのだ。

※3: 以前にも書いたが、GoogleのMapReduceの名前の元ネタはこれ、である。
Googleは巨大なPythonユーザーとして有名だが、と同時に、関数型言語を操れるハッカーが多い、と言う事である。

※4: この辺ピンと来ない人に説明すると、仮にC言語なんかでforを使おうとした際に「goto使えばイイじゃん?」って言われて感じるモノが関数型言語ユーザーが感じる事と同じ事なのだ。
一般に、「抽象度が高い機能」を使う事が「当たり前」となってると「抽象度が低い機能」はまさしく「低レベルな動作を書かなきゃいけない時の非常時用」にしか見えない、と言う事である。そして仮にそれしか使ってないコードを見ると「無駄な事やってんな」とまで思うだろう。
この比喩を聞いて「大げさだろ」と思う人もいるかもしれないが、実は高級言語のforなんかの「gotoよりも抽象度が高い機能」が現れた時も人は抵抗を覚えたモンなのである。そしてそれが「受け入れられる」までそれなりに時間がかかってるのだ(forなんか使ってる「高級言語」を使うよりもアセンブリでgotoを書くべき、って論調が60年代後半〜70年代にはあった)。
要するに、歴史は繰り返すし、言語デザインと言う「枠組み」も進歩するんだ、ってだけの話で、もっと言っちゃうと「革新についていける人」と「ついていけない人」が出てくる、ってだけの話である。

※5: 実験でやるのは構わない。が、思ったより「意味が無く」、単に複雑性だけ増す、と言った結果にしかならないだろう。

※6: Javaが広めたのは事実ではあるが、とは言ってもJavaが初めて、と言うわけではない。
オブジェクト指向の雄、Smalltalkとは違う、完全なクラスベースのプログラミング言語、と言うモノがJava以前に、その可能性に付いて長い間大学等の研究機関で研究し倒されてきた。
恐らくそのテの言語での一番の性交成功例は1985年に登場したEiffelで、現在でも「オブジェクト指向を極めたい」と思ってる人には学習/研究用として人気の言語だ。この言語を用いた「オブジェクト指向入門」と言う本が二巻に渡って日本でも販売されており、オブジェクト指向のバイブル、として扱われている(僕は未読)。
Eiffelの処理系はオンラインで試してみる事が可能で、簡単な入門はここにあるので、興味がある人は触ってみたら良いだろう。
また、GNU版フランス国立情報学自動制御研究所(INRIA)から提供されている(なお、INRIAがOCamlやMATLABのクローンであるScilabの提供元である)。

※7: フランスで開発されたOCamlは「オブジェクト指向」と「関数型プログラミング」のマリアージュを目指して開発されたもので、日本でもそこそこ人気がある言語となっている。そしてマイクロソフトのF#の基となった。
LispでもGOOと言う方言やISLISPと言う方言があり、これらも「オブジェクト指向」と「関数型プログラミング」の融合を目指している。ただ、全般的にはLispユーザーには支持されてはいない。

※8: 一方、関数型プログラミングは「データを外部から与えられ」、極論それを「フィルタリング」する事「だけ」で結果を得ようとするモデルである。だから元データ自体を弄らなくて済むのである。

※9: もっとも、場合によっては破壊的変更を多用するプログラムの方がコードがシンプルになる可能性がある。・・・ただし、デバッグが大変になるのは間違いない。
一般に、ソースコード上の記述量の少なさと、デバッグの「し安さ」は必ずしも比例関係にはない。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事