見出し画像

Retro-gaming and so on

メソッドへ至る道

こないだ書いた記事に訂正がある。

プログラムの単純化の為、ドラゴンクエストのキャラと敵キャラの直接攻撃の式を全く同じにしてしまったが、実は味方のキャラのダメージを与える式と敵キャラのダメージを与える式は違うらしい。

このページによると、敵の攻撃はもっと複雑な計算式を使ってる模様で、Pythonで書くと、

def attack2member(enemy, member):
 x = enemy.THAC0 - member.AC / 2
 if enemy.THAC0 / 4
  t = x * randint(54, 197) / 256
  return round(t - t / 4)
 elif enemy.THAC0 / 8 < x or x <= enemy.THAC0 / 4:<br>  if randint(0, 3) == 0:
   return 0
  else:
   y = enemy.THAC0 / 4 * randint(54, 197) / 256
   return round(y - y / 4)
 else:
   if randint(0, 1) == 0:
    return 0
   else:
    y = enemy.THAC0 / 4 * randint(54, 197) / 256
    return round(y - y / 4)

のように、ちょっと複雑な条件分けになってる模様だ。
従って、関数head_on_fightは次のように修正されないとならない。

def head_on_fight(member, enemy):
 msgs = {"self":"{0}のこうげき!\n{1}に{2}ポイントのダメージをあたえた!",
     "enemy": "{0}のこうげき!\n{1}は{2}ポイントのダメージをうけた!",
     "win": \
     "{0}をやっつけた!\nけいけんち{1}ポイントかくとく\
\n{2}ゴールドをてにいれた。"}
 mHP = member.HP
 eHP = enemy.HP
 print("{}があらわれた!".format(enemy.name))
 while True:
  e_damage = attack2enemy(member, enemy)
  print(msgs["self"].format(member.name, enemy.name, e_damage))
  eHP -= e_damage
  if eHP
   print(msgs["win"].format(enemy.name, enemy.EX, enemy.gold))
   member.HP = mHP
   member.EX += enemy.EX
   break
  m_damage = attack2member(enemy, member) # ここを訂正
  print(msgs["enemy"].format(enemy.name, member.name, m_damage))
  mHP -= m_damage
  if mHP
   print("{}はしんでしまった!".format(member.name))
   break

・・・・・・ふむ。
今、戦闘に関する関数が二つに増えた。
味方側が攻撃するか、敵方が攻撃するか、で使う関数を選ばなアカン。
・・・メンド臭くね?

出来れば同じ関数名で違う内容が呼び出されて欲しいのだが・・・・・・。
そうは問屋が卸さないんだな。
通常、関数は大域的に定義されるわけだが。当然関数名が重複する事は許されていない。だからこそattack2enemyattack2memberと二つの関数名になっている。
出来れば、例えばattackと言う名前でこの二つの関数の内容を適用するデータの「型」によって振り分けたいのだ。
要するにこれは名前空間の問題であり、記述の手間を減らしたい、と言った願望である。

ここから書くのはまずはLispで培われたテクニックである。

通常、上のような命題を与えられた場合、「条件式で振り分けて・・・」等と考えるだろう。
だが、それでさえメンド臭いのだ。
条件節で分ける、となると何らかの関数の「追加」があった場合、節を書き換えなければならない。
そうではなくって、何らかの関数の「追加」はあくまで簡単に「追加」として成し遂げられなければならない。
そう考えた時に、便利なデータ型がある。
ハッシュテーブルだ。
Pythonでは辞書型として知られている。

例えばここで次のような辞書を大域変数として定義してみる。

attack_table = {Brave: attack2enemy, Monster: attack2member}

そして次のように関数を定義する。

def attack(obj0, obj1):
 return attack_table[type(obj0)](obj0, obj1)

そうすれば引数のobj0の型がBrave型だろうとMonster型であろうと、attack_tableに登録された「値」が返ってくる。
そしてその値は今、関数なので、引数(obj0, obj1)に適用される。

上のattack関数は「名前空間」の問題を解決し、obj0の型判定を利用して「自動で」適する関数を返してくれるのだ。
しかも、仮に新しい型が出てきて、関数を追加したい、と言った場合、新しく関数を定義して、対象となる「型」をキーとして辞書型に登録するだけで同じ「attack」と言う名前で使用する事が出来る。

;; ガチンコ勝負の関数
def head_on_fight(member, enemy):
 msgs = {"self":"{0}のこうげき!\n{1}に{2}ポイントのダメージをあたえた!",
     "enemy": "{0}のこうげき!\n{1}は{2}ポイントのダメージをうけた!",
     "win": \
     "{0}をやっつけた!\nけいけんち{1}ポイントかくとく\
\n{2}ゴールドをてにいれた。"}
 mHP = member.HP
 eHP = enemy.HP
 print("{}があらわれた!".format(enemy.name))
 while True:
  e_damage = attack(member, enemy) # ここが書き換わる
  print(msgs["self"].format(member.name, enemy.name, e_damage))
  eHP -= e_damage
  if eHP
   print(msgs["win"].format(enemy.name, enemy.EX, enemy.gold))
   member.HP = mHP
   member.EX += enemy.EX
   break
  m_damage = attack(enemy, member) # ここが書き換わる
  print(msgs["enemy"].format(enemy.name, member.name, m_damage))
  mHP -= m_damage
  if mHP
   print("{}はしんでしまった!".format(member.name))
   break

こういう「型による関数の振り分け」を用いたプログラミング法を「データ駆動型プログラミング」と呼び、上で見せたattack関数のような関数を総称関数と呼ぶ。
実はこれだけで、オブジェクト指向にまつわる「メソッド」の問題、と言うのは基本的には解決されてしまうのだ。
名うてのLispハッカー、ポール・グレアムがこの文書で指し示した事はこの方式の事を指している。

ハッシュテーブルにクロージャを詰め込む とか、弱い言語だったらオブジェクト指向テクニックが必要だっただろうと 思えることはたくさんやってきたけれど、CLOSを使う必要に迫られたことは無かった。

そう、上で書いてるattack関数とはまさに「ハッシュテーブルにクロージャを詰め込む 」手法である。

上の総称関数はまさしくオブジェクト指向のメソッドとして働いている。
こう書くと、やっぱり、

「いやいや、メソッドってのはクラスに属してるモノでしょ?」

と言うあわてんぼうさんが現れるだろう。
しかしそれは実は正しくないのだ。
オブジェクト指向と言う「厳密に定義がない」ものだと、実際、クラスにメソッドが属すべきか属さないべきか、と言うのはハッキリ言うとそのプログラミング言語の実装者の好みになる。
そして、「クラスにメソッドが属してる」と言うのはC++で広まった方法であるが、別に「そうせなアカン」と言う性質の問題ではない。
上に書いた通り、問題の本質は、「名前空間」の問題である。同名の関数を複数定義しても問題がない「システム」が欲しいだけ、なのだ。
それで言うと、総称関数はその目的には符合している。

実は上のような総称関数をオブジェクト指向のメソッドとして採用しているのが、ANSI Common LispのCLOS(Common Lisp Object System)である。
例えば、CLOSで定義した勇者クラス、モンスタークラスは次のようになるだろう。

(defclass Brave ()
 ((strength :initarg :strength :accessor strength)
 (dexerity :initarg :dexerity :accessor dexeriry)
 (maxHP :initarg :maxHP :accessor maxHP)
 (maxMP :initarg :maxMP :accessor maxMP)
 (THAC0 :initarg :THAC0 :accessor THAC0)
 (AC :initarg :AC :accessor AC)
 (HP :initarg :HP :accessor HP)      ; CLOSでは自身は参照不可
 (MP :initarg :MP :accessor MP)      ; CLOSでは自身は参照不可
 (equip :initarg :equip :initform '() :accessor equip)
 (EX :initarg :EX :initform 0 :accessor EX)
 (name :initarg :name :initform "" :accessor name)
 (LV :initarg :LV :initform 1 :accessor LV)))

(defclass Monster ()
 ((HP :initarg :HP :accessor HP)
 (MP :initarg :MP :accessor MP)
 (THAC0 :initarg :THAC0 :accessor THAC0)
 (AC :initarg :AC :accessor AC)
 (dexerity :initarg :dexerity :accessor dexerity)
 (EX :initarg :EX :accessor EX)
 (gold :initarg :gold :accessor gold)
 (name :initarg :name :accessor name)))

これでクラスが定義されるのだが、メソッドはこれと別に定義する。

;; 勇者のパーティメンバーからのモンスターへの攻撃
(defmethod attack ((member Brave) (enemy Monster))
 (round (* (- (THAC0 member) (/ (AC enemy) 2)) (/ (+ 54 (random 143)) 256))))

;; モンスターからの勇者のパーティメンバーへの攻撃
(defmethod attack ((enemy Monster) (member Brave))
 (let ((x (- (THAC0 enemy) (/ (AC member) 2))))
   (cond ((< (/ (THAC0 enemy) 4) x)
      (let ((t (* x (/ (+ 54 (random 143)) 256))))
        (round (- t (/ t 4)))))
      ((<= (1+ (/ (THAC0 enemy) 8)) x (/ (THAC0 enemy) 8))
        (if (zerop (random 4))
          0
        (let ((y (* (/ (THAC0 enemy) 4) (/ (+ 54 (random 143)) 256))))
          (round (- y (/ y 4))))))
      (t (if (zerop (random 2))
         0
        (let ((y (* (/ (THAC0 enemy) 4) (/ (+ 54 (random 143)) 256))))
          (round (- y (/ y 4)))))))))

随分とカッコが多くてさすがにツラいが気づいただろうか?
同名のattackと言うメソッドが二種類定義されてるが、全くエラーが出ない。

実はこのdefmethodと言うのもマクロで、上で見たような総称関数の仕組みを自動的に定義してくれる。気持ち悪いくらい暗黙に色々とやってくれるのがCLOSで定義されているマクロ群だ。

例えば(THAC0 member)とか(AC member)とか、Pythonだとmember.THAC0とかmember.ACとか書きそうなトコが随分と気味悪い記述になってるが、これは実際、THAC0ACと言う名前の「関数」になってて、それらはdefclass内のメンバ変数(CLOSではスロットと呼ぶ)定義で:accessorを記述した時点で「自動生成」される。

同様に、defmethodは使った時点で大域的に関数参照の為の仕組みを作り出し、自動でそこに「関数本体」を含めてしまうので、名前がぶつかる事がないのだ。

いずれにせよ、ANSI Common Lispの策定者達は「クラスにメソッドを含める必要はない」と判断した。別にメソッドをクラスに含めないでも名前の衝突は避けられる、と言う技術的判断があった、と言う事だな。
そして、データ駆動型プログラミング、あるいは総称関数の仕組みは、特にオブジェクト指向じゃないプログラミング言語でも、結果「使える」と言う事だ。
C言語なんかでも・・・・・・あまり考えたくねぇけど(笑)、構造体と最低でも配列、あとは関数ポインタでも駆使すれば似たような事は可能だろう。
・・・・・・やりたくねぇけど(笑)。

取り敢えず総称関数が汎用的だ、と言う事は分かっただろう。メソッド定義はこの方式でも可能だし、適した型、ないしはクラスが存在しない場合はそもそも総称関数は適用出来ない。
一方、C++でポピュラーになった方式が、「クラスにメソッドを含む」方法論である。
これは「プログラミング言語がそういう設計になってないと」使えない方式で、オブジェクト指向がどんな定義であれ、オブジェクト指向言語、と名乗るには必要だと(誤解であれ)思われている。

いずれにせよ、これも真の目的は、実は関数の名前の衝突を避ける事である。

PythonもC++と同様の方式を採っている為、勇者クラスにもモンスタークラスにもメソッドとして同名のattackを定義出来る。

class Brave(object):
 def __init__(self, strength, dexerity, maxHP, maxMP, THAC0 = 0, AC = 0, equip =[], EX = 0, name = "", LV = 1):
  self.strength = strength
  self.dexerity = dexerity
  self.maxHP = maxHP
  self.maxMP = maxMP
  self.THAC0 = THAC0
  self.AC = AC
  self.HP = maxHP
  self.MP = maxMP
  self.equip = []
  self.EX = EX
  self.name = name
  self.LV = LV

 def attack(self, enemy):
  return round((self.THAC0 - enemy.AC / 2) * randint(54, 197) / 256)


class Monster(object):
 def __init__(self, HP, MP, THAC0, AC, dexerity, EX, gold, name):
  self.HP = HP
  self.MP = MP
  self.THAC0 = THAC0
  self.AC = AC
  self.dexerity = dexerity
  self.EX = EX
  self.gold = gold
  self.name = name

 def attack(self, member):
  x = self.THAC0 - member.AC / 2
  if self.THAC0 / 4
   t = x * randint(54, 197) / 256
   return round(t - t / 4)
  elif self.THAC0 / 8 < x or x <= self.THAC0 / 4:<br>   if randint(0, 3) == 0:
    return 0
   else:
    y = self.THAC0 / 4 * randint(54, 197) / 256
    return round(y - y / 4)
  else:
   if randint(0, 1) == 0:
    return 0
   else:
    y = self.THAC0 / 4 * randint(54, 197) / 256
    return round(y - y / 4)

総称関数では引数にobj0obj1を取ってobj0の型によって動作を振り分けていた。
一方、C++方式のオブジェクト指向のクラスのメソッドは、クラスに属する為、特に型判定は必要ない。
その代わり、第一引数は、Pythonの場合、生成されたインスタンス自身になる。
そこでself、と書くのは実は文法的に決められてるわけではない。が、命名規約としてselfが使われるケースが多い(実はJavaのようにthisを使っても特に文法違反にはならない)。
従って、総称関数でobj0として扱われてた部分は全部selfに置き換わる事となる。

総称関数の場合、attackと言う名前で第一引数の型で実際2つの関数を振り分けていた。
一方、Pythonのメソッド(や他のいわゆる通常のオブジェクト指向のメソッド)は次のようにして使う。

member.attack(enemy) # パーティメンバーからモンスターへの攻撃
enemy.attack(member) # モンスターからパーティメンバーへの攻撃

確かにattackと言う名前は被ってるが、識別子的にインスタンス名にくっつくカタチで使用される。
個人的にはこれで「名前の衝突が起きない」と言うのはインチキっぽい、って言えばインチキっぽく思えるけどな(笑)。

いずれにせよ、メソッド、と言うのは基本的に関数と変わらない。
が、「同じような作業だから同じ名前をつけたい」と言う要求に応える為、考え出された方法だ、と言う事は押さえておいた方が良いと思う。
Javaのように「オブジェクト指向で書く事を強制する言語」ならいざ知らず、別に同名のメソッドを定義する必要がない場合は、フツーに関数を書いても一向に構わない。
むしろそうするべきで、「クラスを作ったから何が何でもメソッドを作って使わなアカン」ってモンでもねぇのだ。
要はこれは選択肢の問題であり、選択肢は多いに越したことがない、と言う話なのだ。

;; Python のメソッド使用でのガチンコ勝負の例
def head_on_fight(member, enemy):
 msgs = {"self":"{0}のこうげき!\n{1}に{2}ポイントのダメージをあたえた!",
     "enemy": "{0}のこうげき!\n{1}は{2}ポイントのダメージをうけた!",
"win": \
"{0}をやっつけた!\nけいけんち{1}ポイントかくとく\
\n{2}ゴールドをてにいれた。"}
 mHP = member.HP
 eHP = enemy.HP
 print("{}があらわれた!".format(enemy.name))
 while True:
  e_damage = member.attack(enemy) # ここが書き換わる
  print(msgs["self"].format(member.name, enemy.name, e_damage))
  eHP -= e_damage
  if eHP
   print(msgs["win"].format(enemy.name, enemy.EX, enemy.gold))
   member.HP = mHP
   member.EX += enemy.EX
   break
  m_damage = enemy.attack(member) # ここが書き換わる
  print(msgs["enemy"].format(enemy.name, member.name, m_damage))
  mHP -= m_damage
  if mHP
   print("{}はしんでしまった!".format(member.name))
   break

まとめ。

  • 総称関数、と言う方式はオブジェクト指向じゃないプログラミング言語でもオブジェクト指向っぽくプログラムが書ける汎用の方法論である。
  • 総称関数、及びC++スタイルのメンバ関数ないしメソッドは、どれも「似たような機能を持った関数」に「同じ名前をつけたい」場合の解決策である。
  • メソッドがクラスに属する場合、それは基本的にはプログラミング言語が提供する機能でなければならず、自分でエミュレートする事は出来ない。
  • メソッドがクラスに属するか属しないか、はそのプログラミング言語の開発者の好みであって、その方法論の選択自体はオブジェクト指向か否かを定義しない。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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