オブジェクト指向関連の不満な点

また遅れてしまった…オブジェクト指向 Advent Calendar11日目。

最近いろいろオブジェクト指向について調べていて、やっぱりこの概念はいかがなものか、と思ったことを軽く書く。

継承

まずこれ。オブジェクト指向において「AがBを継承している」というのは、外延的には「任意のBのメソッドはAのメソッドである」であり、実装としては「AはBが持つ内部状態やメソッドの実装を利用できる」ことが多い。しかし、Twitterにも似たようなことを書いたが、「車クラスが乗り物クラスを継承している」みたいなゆるふわな使い方が目立つ。クラスインターフェイスの用語と、それらの継承の定義ははっきりさせたい。則のない関係に意味なし。

抽象クラス、抽象メソッド

実装のない「抽象メソッド」と実装のあるメソッドをごちゃ混ぜにできるような仕様は害があると考えている。インターフェイスがあるにもかかわらずこのような中途半端な仕組みを作るのは、不完全な抽象化と複雑化の原因になりうる。

多重継承

暗黙に何もかも引き継いでしまう仕様と、複数の概念から引き継ぐ仕様は本質的に両立しない。言語の簡単さを犠牲にして小難しいテクニックを導入するより、どこから引き継ぐのか明示したほうが楽に見える。

デザインパターン

デザインパターンは「道具」ではなく、「よくあるパターン」である。なぜそのパターンになったかを感覚的に理解せずに、デザインパターンを「使おう」としてもうまくいかないのに、なぜありがたがっている人が多いのかよくわからない。また、言語設計者はデザインパターンを不要にしていく方向への努力が必要だと考えている。

オブジェクト指向

すべてオブジェクトで扱うのが正義という風潮、これはよくない。オブジェクト指向は、内部状態を持った概念に対する操作の列(メインループでUpdateメソッドを繰り返し呼ぶなど)を抽象化するとき初めて必要になるものだと私は考えている。オブジェクト指向の表現力が高いのは確かだが、だからといって基本的な型や関数をおろそかにするのは本末転倒だ。


私が満足できるようなオブジェクト指向プログラミングができる言語はまだ存在しない。近いうちに理想的なオブジェクト指向プログラミング言語を作って、オブジェクト指向はどうあるべきか、また考えてみたい。

状態、手続き、OOP

これはオブジェクト指向 Advent Calendar 2014の7日目の記事です。

入力によって変化する状態をうまく扱うことはプログラミングにおいて重要である。この記事では、状態や手続きを扱うさまざまなアプローチを紹介、比較する。

手続き型プログラミング

命令の列である手続きを「定義」「呼び出し」する記述力を提供する。命令の結果として得られた値に依存して、次に行う手続きを決定するという性質が肝である。

コルーチン

手続きに区切りを設けることで、「途中まで実行された手続き」を値として扱う。手続きが持つ潜在的な状態を閉じ込めることができるため、柔軟性が高い。

第一級手続き型プログラミング

手続きをデータ構造の一つとして表現し、呼び出すだけではなく、手続きそのものに対する操作も可能にする。「手続きの結果として得られた値に依存して、次に行う手続きを決定する」性質に対応するものとしてモナドがある。モナドHaskellやPureScriptでは非常によく使われている。

構造体とフィールド

複数の値をまとめて一つの値として扱うための構造。フィールドを指定して、値を取得したり更新したりできる。

第一級フィールド

フィールドを値で表現することにより、より柔軟に構造に対してアクセスできるようにする。第一級フィールドの利点は、それが対象とする構造と強く結びつけられていないことであり、例えばRGBを格納する構造に対し、色相、彩度、明度のフィールドを定義することも可能である。lensはもっとも完成された第一級フィールドの実装であり、フィールド同士の合成や、データ型からの自動生成などができる。

オブジェクト指向

「オブジェクト」と「メソッド」の仕組みを新たに提供する。オブジェクトに対してメソッドを呼び出すと、メソッドに関連付けられた手続きが呼び出され、オブジェクトの内部状態が変化する。オブジェクトの内部状態を表現するのに構造体の仕組みがよく使われている。また、インターフェイスがあれば内部状態の持ち方が異なっても同じように扱えるため、複雑な状態を持つアプリケーションの記述に特に有用である。

第一級オブジェクト指向

オブジェクト指向の諸概念を言語に組み込むのではなく、あくまでデータ構造で表現することで、より自由にオブジェクトを扱う。第一級オブジェクト指向においては、オブジェクトは手続きを手続きに変換するオートマトンインターフェイスメソッドの集合であり、継承はメソッドの集合の包含関係で表される。オブジェクト自体が合成可能であるほか、メソッドモナドにすることで、メソッドを合成可能にもできる。従来のオブジェクト指向と異なり、副作用が常に表現できるわけではないため、ゲームのロジックなどを安全に記述するDSLに応用できる。第一級オブジェクト指向の実装としてobjectiveがある(作った)。

FRP(Functional Reactive Programming)

Behavior(時間によって連続的に変化する値)とEvent(飛び飛びの時間で発生する値)を扱う。BehaviorでEventを振り分けたり、Eventを畳み込んでBehaviorにしたりする関数を組み合わせてプログラムを記述する。入力と出力がはっきりしていれば、信号の流れを非常にきれいに表現できる。一方、手続き的な考え方との相性は良くないので注意する必要がある。FRPの実装として、netwireYampaelereasodiumreactive-bananaordreaなどがある。

まとめ

一般に、言語の組み込み機能として何かの機能を提供するより、型と値を用いて目的のものを実装するほうがより柔軟性が高い。目的が何であれ、型と値の記述力を重視するのが、最高のプログラミング体験への鍵であるとして、この記事を締め括ろう。

大洗に行ってきたよ

はるアイコン鯖 Advent Calendar 2014 - Adventar

※現実で。

※現実で。

※現実で。

海岸

f:id:fumiexcel:20141104133908j:plain

大洗磯前神社入口

f:id:fumiexcel:20141104141947j:plain

絵馬

f:id:fumiexcel:20141104141555j:plain

痛絵馬は西側に偏っている模様。

神社から見える水平線

f:id:fumiexcel:20141104141726j:plain

石が置かれまくっている像

f:id:fumiexcel:20141104142127j:plain

大洗駅内にて

f:id:fumiexcel:20141104145223j:plain

大洗を3つの言葉で表すとすれば、「海」「レンガ」「ガールズ&パンツァー」だ。

この経験を生かし、はるアイコン鯖の大洗にレンガの建物をもっと増やし、ついでに石を置かれまくっている像も作りたい。

はるアイコン鯖の鳥取に製鉄プラントを作る計画が進行中ですが、最近忙しくてインできていないです。すみません…

関数型プログラミングとオブジェクト指向の抜き差し可能な関係について整理して考える

Googleで適当に検索すると

f:id:fumiexcel:20140922142853p:plain

とズラリと出てくる。

オブジェクト指向 v.s. 関数型プログラミング

関数型とオブジェクト指向という一見相反するプログラミングパラダイムの併用について理解した

プログラマが知るべき97のこと/関数型プログラミングを学ぶことの重要性

新人プログラマに知っておいてもらいたい人類がオブジェクト指向を手に入れるまでの軌跡

関数型プログラミングとオブジェクト指向の抜き差しならない関係について整理して考える

とそれなりに参考になりそうな情報はあるものの、無駄に複雑化されたオブジェクト指向をストローマンにするような記事ばかり(それだけ今までのオブジェクト指向にみんなうんざりさせられているのだろう)で、そろそろきちんと自分自身「関数型プログラミングオブジェクト指向の切り離され方」についてはっきりさせておきたい、と考え、概念整理した結論を書きます。

まず端的な結論

結論を端的に言うと、

ストリーム変換器 + 手続きの自然変換*1オブジェクト指向

の関係になっている。

関数型プログラミングオブジェクト指向を臨機応変に併用する」

だとか、

関数型プログラミングオブジェクト指向のマルチパラダイムである」

とすると、今まで見えていなかった部分で概念の重複が起こり、いろいろややこしいことになる、理解においても実践においても混乱を招く、実際混乱を招いている、ということになります。

関数そのものから考える

茶番はここまで。ある純粋な関数fを考えてみよう。:の左は識別子名、右は型シグネチャである。

f : A → B

fは具体的な型Aの値を受け取り、Bを返す。fは純粋なので、いつ値を渡そうと、日本の首都が鳥取になろうと、同じ値を渡せば同じ値が返ってくる。

ところが実用上は、今までに受け取った値に応じて今後の動作を変えたい場面が多々ある。パーサーやWebアプリケーションなどがその例だ(Aに文字やリクエスト、Bにデータやレスポンスが入る)。それを表現するにはどうすればよいだろうか?Bだけではなく、「次のわたし」も一緒に返せばよいのだ。

data TransAB where
    TransAB : (A → (B, TransAB)) → TransAB

runTransAB : TransAB → A → (B, TransAB)

このように、入力によって出力と次の状態が決まる概念をミーリマシン(mealy machine)と呼ぶ。

「次のわたしがある」という制約によって、関数の表現力が増したのだ。

さて、HaskellやPureScriptなどの一部の言語では、手続きそのものをデータ構造として扱うことが一般的に行われている。ある程度の型の表現力があれば、手続きと、手続きの間の関数も定義できる。

もっとも簡単な手続きを定義してみよう。

data Yo x where
    Yo : Yo Result

見たとおり、Yoしかないので、Yoを超えることはできない。「Yoの結果に対して演算を行う」はできず、「Yoを2回」ももちろん無理。ましてや「前のYoが成功したらYoしない」など夢のまた夢だ。

これをなんとかするために、「モナド」という概念が作られた。下のようにYoの機能を増やすと、前のYoに依存したYoが可能になる。PureとBindの導入については、モナドとはモナドであるも参照されたい。

data Yo x where
    Pure : a → Yo a
    Bind : Yo a → (a → Yo b) → Yo b
    Yo : Yo Result

Yoを具体的な動作に変換する、Haskellに非常に近い言語による実装を示した。

runYo : ∀ x. Yo x → IO x
runYo Yo = do
  print "Yo"
  return Success
runYo (Pure a) = return a
runYo (Bind m k) = runYo m >>= runYo . k

手続きの変換を言葉で表すと「手続きの結果が何であっても、中身の構造を保つ関数」となる。最初に扱った純粋な関数と比べると、任意の結果に対応しなければならない分、より表現力が強い。圏論ではこういった変換を自然変換(natural transformation)と呼ぶ。ただ、runYoは純粋なので、いつYoを変換しても、具体的に行われる内容は変わらない。

ここまで紹介した、2種類の関数の発展を地図に描いてみよう。

f:id:fumiexcel:20140922162925p:plain

変換器を「手続きへの適用」方向に伸ばし、自然変換を「状態依存」方向に伸ばした先に宝物がある気がしてこないだろうか?


変換器の領域を緩やかに進んでいた私に、「取舵一杯」となにかが告げた。そして、突如としてそれは姿を現した(表記の統一のために一般化された代数的データ型(GADT)を用いたが、実装に必須ではない)。

data Treasure m n where
    Treasure : (∀ x. m x → n (x, Treasure m n)) → Treasure m n

runTreasure : Treasure m n → m x → n (x, Treasure m n)

ある手続きを受け取ると、その結果と次の状態を返す手続きを生み出す。初めて目にするが、とてもわかりやすい、不思議な感覚。

とりあえず、ミュータブルな変数を作ってみよう。

data Identity x where
    Identity : a → Identity a

data GetSet s x where
    Get : GetSet s s
    Set : s → GetSet s ()

variable : s → Treasure (GetSet s) Identity
variable s = Treasure (handle s)

handle :: s → GetSet s a → Identity (a, Treasure (GetSet s) Identity)
handle s Get = Identity (s, variable s)
handle s (Set s') = Identity ((), variable s')

状態を保持する能力を確認できた。次に、この概念が関数のような性質を持つか確かめてみた。まず、恒等関数相当。

echo : Functor m ⇒ Treasure m m
echo = Treasure (map (λx → (x, echo)))

(Haskellの場合、mapはfmapと読み替えてほしい)

続いて、合成。$はみんな大好き関数適用演算子(f x $ g y = f x (g y))。

(>>>) : Functor n ⇒ Treasure l m → Treasure m n → Treasure l n
Treasure m >>> Treasure n = Treasure $ λe → map
    (λ((x, m'), n') → (x, m' >>> n'))
    (n (m e))

確かに関数のようにふるまうようだ。

この構造は私に既視感を感じさせたが、その正体がわかった。OOPにおけるオブジェクトのようにふるまうのだ。

オブジェクトは内部状態を持ち、呼ばれたメソッドに応じて、必要があれば内部状態を更新し、副作用を引き起こす――ちょうど、手続きの変換とミーリマシンの両方の性質を持っている。

よくあるオブジェクト指向プログラミング言語における記述

result = obj.Foo(x, y)

をこのやり方で解釈すると、

(result, obj') ← runTreasure obj (Foo x y)

という表現になる。

共通する性質は、メソッドレベルの『手続き』を、実装レベルの『手続き』に変換」しつつ、「自分の状態を更新」するという点だ。

TreasureをObjectに改名して、典型的なOOPとの対応をまとめてみよう。

典型的OOP 関数型OOP
プログラム IO型の値
クラス Object M IO型の値の定義
インスタンス Object M IO型の参照*2
コンストラクタ Object M IOを生成する関数
インターフェース M
メソッド M型の値
フィールド get : M sset : s → M ()の存在
継承 Object M' Mとの合成
オーバーライド Object M' Mの実装の一例
菱形継承 メソッドへのパターンマッチ
ダックタイピング 存在量化
カプセル化 メソッド以外のアクセス手段なし
This 不要
不可能 IOを制限されたクラス(Object M Limited)
不可能 クラスの実装そのものへの操作((>>>t))

オブジェクト指向の仕組みが、たった一つの型の特殊な場合として表現できるのは、なかなか魅力的ではないだろうか。

応用

さっそく、ObjectのHaskellによる実装を作った。Object型に加え、Objectのインスタンスとして利用するためのユーティリティ関数も備えている。

現在、callというゲームエンジンへの応用を試みている。リンク先のコードのように、いかにもオブジェクト指向らしい記述を実際に可能にしている。

結論

オブジェクト指向は、関数型プログラミングと相容れない何かではなく、実装するものだ。

元ネタ


この記事はIIJ-IIのアルバイトのため、備忘録を兼ねて新しいプログラミングスタイルへのチュートリアルとして書いた。

*1:カジュアルな用法

*2:HaskellのIORefなど

Haskellでオブジェクト指向を再発明する

状態管理のモデル案: spawn/killモデルの実装を作ってみた。

worldsパッケージがそれだ(露骨な名前だが赦してほしい)。前の記事と違う点は、Worldモナド変換子として実装されている点だけである。

worlds-exampleは画面内のキャラクターを方向キーで操作する例。メインのプログラムは以下のようになっている:

import Include
import Types
import qualified Entity.Player as Player
import Assets

main = runGameDefault $ runWorldT $ do
    player <- spawn $ Player.new (V2 240 240)
    forever $ do
        whenM (lift $ keyPress KeyLeft)    $ player .! Player.Move L
        whenM (lift $ keyPress KeyRight)   $ player .! Player.Move R
        whenM (lift $ keyPress KeyDown)    $ player .! Player.Move D
        whenM (lift $ keyPress KeyUp)      $ player .! Player.Move U
        player .! update
        lift tick

見ての通り、Player内部の状態には直接関与しない。一方、Playerの実装では、mainから受け取ったメッセージを解釈し、実際の動作と状態遷移に変換する。

{-# LANGUAGE GADTs #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}

module Entity.Player where
import Include
import Types
import Assets

data States = States
  { _hp :: Float
  , _position :: Vec2
  , _direction :: Direction
  , _animation :: Int
  }
makeLenses ''States

data Actions x where
  GetCoord :: Actions Vec2
  Update :: Actions ()
  Move :: Direction -> Actions ()

instance Updatable Actions where
  update = Update

type Player s = Life (WorldT s Game) Actions ()

-- 命令を受け取る部分
handle :: (MonadState States m, FreeGame m) => Actions a -> m a
handle GetCoord = use position
handle Update = do
  d <- use direction
  n <- use animation
  p <- use position
  translate p $ bitmap $ playerBitmap (n `div` 5) d
handle (Move d) = do
  case d of
    L -> position -= V2 2 0
    R -> position += V2 2 0
    U -> position -= V2 0 2
    D -> position += V2 0 2
  direction .= d
  animation += 1

life :: States -> Player s
life s = Alive $ \e -> do
  (a, s') <- lift $ runStateT (handle e) s
  return (a, life s')

new :: Vec2 -> Player s
new p = life $ States
  { _hp = 8
  , _position = p
  , _direction = R
  , _animation = 0
  }

これはまさにカプセル化メッセージパッシングであり、オブジェクト指向の実装であると言える。従来のオブジェクト指向と違う点は、インスタンスメソッドなどの仕組み全てがファーストクラスであり、カスタマイズすることができることである。たとえば、メソッドモナドにするのは、Operationalモナドを使えば高々2行の変更で可能だろう。

ゲーム開発などにおいてこのアプローチがどう使えるか、これからも調べていきたい。

追記

ドッペルゲンガーを作ってみた。インスタンスは霊本体と操作するクローンに分かれている。

haunt :: (Monad m, FreeGame m) => Name s Player.Actions r -> Life (WorldT s m) Identity ()
haunt she = go R where
  go d = Alive $ \(Identity pass) -> do
    r <- lift $ randomness (0 :: Int, 59)
    she .! Player.Move d
    if r == 0 then do
      i <- lift $ randomness (0, 3)
      return (pass, go (directions !! i))
    else return (pass, go d)

main = runGameDefault $ runWorldT $ do
    player <- spawn $ Player.new (V2 240 240)
    playerClone <- spawn $ Player.new (V2 320 240)
    doppelganger <- spawn $ haunt playerClone
    forever $ do
        whenM (lift $ keyPress KeyLeft)    $ player .! Player.Move L
        whenM (lift $ keyPress KeyRight)   $ player .! Player.Move R
        whenM (lift $ keyPress KeyDown)    $ player .! Player.Move D
        whenM (lift $ keyPress KeyUp)      $ player .! Player.Move U
        player .! update
        playerClone .! update
        doppelganger .! return ()
        lift tick

状態管理のモデル案: spawn/killモデル

生と死のあるアクターを表現するためのモデルについて考えた(あくまで個人用メモ)。

以下の型変数sは世界の一意性、閉鎖性を表す。生命や名前を勝手に外に持ち出すことを防ぐために必要。

  • Life m a: 生命を表す型。aは生命の最終結果。
  • Name s a: 生命に与えられた一意な名前。
  • World s: 生命が住む世界を表すモナドで、以下の操作が可能。
spawn :: Life (World s) a -> World s (Name s a) -- 同じ世界に新しい生命を出現させる
kill :: Name s a -> a -> World s () -- 同じ世界の生命を殺害する
world :: (forall s. World s a) -> Life Identity a

worldは中で起きた生と死を管理し、その過程をある種の命として表す。

これだけでは文字通り何もできないので、生命の間でコミュニケーションする何かが必要。

時間の概念をどのようにして表現するかも考えなければいけない。

(追記)アプローチ

生命自身の動作を表す新たな型変数eを追加する。

spawn :: Life (World s) e r -> World s (Name s e r)
kill :: Name s e r -> r -> World s ()
contact :: Name s e r -> e a -> World s a

eはOperationalモナドやextensible effectsなどを使うことが想定される。

本日の料理: ヤングコーン、ピーマン、豚肉の炒め物

食材と処理

  • にんにく1かけをみじん切りにする。
  • ピーマンの実を適度な大きさに分割する。
  • ヤングコーン(水煮)を小穂に対し斜めに切断する。
  • 豚の細切れ肉を用意する。

調理

  1. フライパンを中火で熱する。
  2. フライパンにごま油を適量入れる。
  3. にんにくを入れて香りを引き出す。
  4. ピーマン、ヤングコーン、豚の細切れ肉の順に、間隔をおいて投入する。
  5. 食材に多少焦げ目がつくのを待つ。
  6. 中華スープの素を少量加える。
  7. 醤油を回しかける。
  8. 十数秒後、加熱を止めて完成。