読者です 読者をやめる 読者になる 読者になる

Creatable a => a -> IO b

Haskellと数学とちょびっと音楽

Haskellでポーカーを作ろう〜第一回 リストのシャッフルとカードの定義〜

はいはいどーも、進捗ダメです。ちゅーんさんです。 基本的にコンスタントに何かを生み出し続けなくてはいけないのが、 ギークなアクティブニートの使命なわけですが、一日の半分以上は睡眠とドラクエに当てらています。 人生なんてそんなものです。

えーっと

そういえば、勉強会とかTLとかで、 「すごいH本読んで、基本的な事はわかったと思うけど、Haskellで具体的なプログラムを開発するイメージが出来ない」 なんていう話を聞くことがたまにあって、そんなに大きくなくても良いので実践的なやさしい文章が増えると良いなぁとか、 ずっと思っていたわけです。

で、そういう状況でドラクエのカジノのポーカーをプレイしたりしていると、 「あ、ポーカー作ろう」っていう気分になったりするじゃないですか。

なったんですよ。

んなわけで、今回からちょっとづつ、Haskellを使ってCUIでプレイできるポーカーゲームを作っていきます。
本記事を読んで、写経しながら動かしたりしているうちに、遊べるポーカーゲームが出来上がってました、 みたいな、そんな感じを目指していこうと思います。

おことわり

前提知識とか

基本的には、「モナドってなんだか良くわからないけど、do構文でなんとかIO処理とか書けるよ」 くらいの人を想定してに話を進めていきます。 型や型クラス、インスタンスの定義とかは普通に読み書きできるくらいのスキルは欲しいです。

開発の進め方について

基本的に「ある程度書いたら記事にまとめて次〜」みたいな感じで作っていくので、 過去に書いた部分を書きなおす〜みたいなフェーズが発生する可能性があります。

なるべく「実際に作っていく感じ」を記録していきたいので、 多少遠回りになるかもしれませんが、ご了承ください。

あと、Haskell得意な人で、「ここはこうした方がええんちゃう?」みたいのがあれば、 ツッコミ頂けると幸いです。

ゲームの仕様をざっくり決める

今回作るのは、ドラクエ等のようにポーカー・ハンド毎に配当が設定されているビデオポーカーではなく、 トーナメント方式で掛け金を奪い合うオーソドックスなドローポーカーです。 いうても良くわからないかもしれませんが、とりあえずCPU何人かとチップを賭けながら、 最終的に全員のチップを奪う事を目的とするゲームです。

つまり、CPUの思考ルーチンなんかも作る予定です(!!)

ちなみに、調べてみたら、ポーカーってかなーり種類がいっぱいあるんですね。 イマドキのナウいルールは、テキサス・ホールデムと呼ばれるフロップポーカーの一種で、 スマフォアプリでオンライン対戦とかやってみてるんですが、これすごい熱いですね、ちょーたのしい。 本当はこれを作りたいくらいなんですが、プログラムが複雑になりすぎる気がしたので今回は諦めます。

んで、本当はここに細々とゲームのルールを書いていっていたんですが、 やった事ある人はわかると思うんですけど、ポーカーってわりと用語とか多くて手順がややこしいので、 文章化するとひじょーに面倒だという事を思い知りましたorz

各回毎に理解しておく必要がある所を説明します。 実際に作りながら覚えて頂くか、適当なWebサイトで調べたり、実際にプレイしてみたりしてください。

カードのシャッフルについて考える

さて、とりあえず今回は下準備として、トランプカードを扱うための道具を作っていきましょう。

カードの枚数はたかだが52枚なので、パフォーマンス上の問題はあんまり意識しなくても良さそうですし、 山札や手札などのカードの束はリストで扱うとしましょう。

リストをシャッフルするという処理は、トランプ以外でも共通です。 こういう処理は標準モジュールになくても、探せばHackageというパッケージデータベースにあったりします。

https://hackage.haskell.org/package/random-shuffle

今回使うのは、このrandom-shuffleというパッケージです。 cabalを使って、このパッケージをインストールしてください。 尚、この記事ではcabalの使い方は説明しません、以下の記事を参考にすると良いでしょう。

http://bicycle1885.hatenablog.com/entry/2012/10/08/044516

多分、このくらいならHaskellのパッケージシステムの悩みの種である依存地獄に陥る心配はあんまりないのですが、 不安な人はcabal sandboxの使い方を調べると良いかと思います。

さて、肝心のrandom-shuffleパッケージの中身なのですが、System.Random.Shuffleモジュール以下に、 3つの関数が用意されています。 シャッフルしたいだけなのになんか仰々しい型ですね(´・ω・`)

shuffle :: [a] -> [Int] -> [a]
shuffle' :: RandomGen gen => [a] -> Int -> gen -> [a]
shuffleM :: MonadRandom m => [a] -> m [a]

単にランダムにリストをシャッフルしたい時は、shuffleM関数を使うと覚えておけば良いです。 乱数の生成は参照透過ではないため、このような少々ややこしい型になってしまっています。 IOモナド内では、どのような副作用も認められているので、IOMonadRandom型クラスのインスタンスになっています。 その事さえ理解していれば、この関数を使うのはとても簡単です。

main :: IO ()
main = do
  shuffledList <- shuffleM [1,2,3,4,5]
  print shuffledList

以下のプログラムを実行すると、1から5までの数がランダムに並んだリストを出力します。

というわけで、カードのシャッフルについてはこのshuffleM関数を使えばさくさくっと出来そうなので、 続いてトランプのカードの定義に入って行きましょう。

トランプカードを定義する

型定義

まず、Cards.hsというファイルを用意します。 トランプには、ハート/ダイアモンド/クラブ/スペードの4種類のマーク(スート)がありますので、 直和型を使って定義します。

data Suit = Hearts | Diamonds | Clubs | Spades
  deriving (Show, Read, Eq, Ord, Enum)

スートはカードの強さに関係ありませんが、 カードそのものがソート出来ると何かと便利なので、Ord型クラスのインスタンスにしておきましょう。 また、Enum型クラスのインスタンスにしておくことで、全てのスートを列挙するのが簡単になります。 んで、トランプカードは、番号とスートからなるので、次のようにCard型を定義します。

data Card = Card Int Suit
  deriving (Eq, Ord)

トランプの番号を表すInt型をSuitの前に書いたのは、deriving Ordした際に、手前の型から大小比較のキーとなるからです。 番号の大きなカードほど強いのでしたね。

後ほどもうちょっと詳しく説明しますが、好き勝手なカードを錬成(w)出来ても困るので、Read型クラスのインスタンスにもしません。 さらに、Show型クラスのインスタンスにしなかったのは、 実際にカードのリストを表示した時にCard 11 Heartsみたいなのがいっぱい表示されてもわけわからんので、 もうちょっと見やすい感じでShow出来るようにしたかったからです。

まず、番号を文字列にする関数を定義します。 本来、A(エース)は1ですが、強さで比較する際に最強の番号なので、 14がAになるようにしておいたほうが後々楽できそうです。

showCardNumber :: Int -> String
showCardNumber 14 = "A_"
showCardNumber 13 = "K_"
showCardNumber 12 = "Q_"
showCardNumber 11 = "J_"
showCardNumber 10 = "10"
showCardNumber x = (show $ x) ++ "_"

んで、showする際に、それぞれスートの頭文字を頭にくっつけるよう、Show型クラスのインスタンスにしてやります。

instance Show Card where
  show (Card i Hearts) = "H" ++ showCardNumber i
  show (Card i Diamonds) = "D" ++ showCardNumber i
  show (Card i Clubs) = "C" ++ showCardNumber i
  show (Card i Spades) = "S" ++ showCardNumber i

GHCiで実際に表示してみます。

*Cards> Card 5 Hearts
H5_
*Cards> Card 12 Clubs 
CQ_
*Cards> Card 10 Spades 
S10

ASCII文字以外を使うと何かしら問題が出てくる可能性があるので、この段階では♡とか♣みたいな全角記号は使いません。 余裕があったらprintする時に良い感じに出力出来るような仕組みを用意しても良いですが、デバッグ用ならこんなもんで十分でしょう。

全てのカードを列挙する

ここは単なるデータの列挙なので、ちゃちゃっといきましょう。

まずリスト内包表記を使ったパターン。

allCards :: [Card]
allCards = [ Card num suit | suit <- [Hearts ..], num <- [2..14] ]

基本的にこのパターンがわかればOKです。

あとは、知識として、リストモナドを使ったパターンもご紹介しておきましょう。
こちらは、今の所「こんな書き方も出来るんだー」くらいの認識で構いません。

allCards :: [Card]
allCards = do
  suit <- [Hearts ..]
  num <- [2..14]
  return $ Card num suit

単純に慣れの問題ですが、僕はリストモナドのパターンが最初に思いつきます。 とはいえ、このくらいならリスト内包表記のほうが綺麗ですね。 どっちにしても、52枚全てのカードを列挙する事ができますので、お好きな方でどうぞ。

*Cards> allCards
[H2_,H3_,H4_,H5_,H6_,H7_,H8_,H9_,H10,HJ_,HQ_,HK_,HA_
,D2_,D3_,D4_,D5_,D6_,D7_,D8_,D9_,D10,DJ_,DQ_,DK_,DA_
,C2_,C3_,C4_,C5_,C6_,C7_,C8_,C9_,C10,CJ_,CQ_,CK_,CA_
,S2_,S3_,S4_,S5_,S6_,S7_,S8_,S9_,S10,SJ_,SQ_,SK_,SA_]

ありえないカードは作らせない

カードの番号はInt型なので、以下のように本来はあり得ないカードを作れてしまいます。

*Cards> Card 100 Hearts 
H101_

この問題を解決する方法は、オブジェクト指向でフィールドをprivateにし、 ゲッターのみを提供するアプローチと少し似ているかもしれません。

定義した型をエクスポートする場合、通常次のように(..)と書いて、 データコンストラクタも一緒にエクスポートする事を明示します。

module Cards 
  ( Suit(..) 
  , Card(..)
  ) where

この時に以下のようにして、Card型に関しては、あえて(..)を書かずにデータコンストラクタを使えないようにします。 同時にallCardsを一緒にエクスポートする事によって、 CardsモジュールをインポートしたモジュールがCard型の値が欲しい場合には、allCardsから取得する必要があるようにしましょう。

module Cards 
  ( Suit(..) 
  , Card
  , allCards
  ) where

これだけだと、欲しいカードをallCardsから探す事が出来ないので、 カードからスートや番号を取得する関数を定義します。

cardSuit :: Card -> Suit
cardSuit (Card _ s) = s

cardNumber :: Card -> Int
cardNumber (Card n _) = n

最終的に、モジュールの定義部分は、次のようになるでしょう。

module Cards 
  ( Suit(..) 
  , Card
  , allCards
  , cardSuit
  , cardNumber
  ) where

動かしてみる

Main.hsを作成して、たった今作ったCardsモジュールをインポートします。

module Main where
import Cards

GHCiを起動して、allCardsから目的のカードを検索できる事や、 Cardデータコンストラクタで変なデータを作ったり出来ない事を確認してみましょう。

*Main> filter (\card -> Hearts == cardSuit card) allCards
[H2_,H3_,H4_,H5_,H6_,H7_,H8_,H9_,H10,HJ_,HQ_,HK_,HA_]
*Main> filter (\card -> 13 == cardNumber card) allCards
[HK_,DK_,CK_,SK_]
*Main> Card 999 Diamonds 

<interactive>:4:1: Not in scope: data constructor `Card'

うむ、問題無いようですね。
あとは、System.Random.ShuffleをインポートしshuffleM関数を使って、 ランダムに5枚のカードを取ってくる簡単な実験をしてみましょう。
尚、sort関数を使うために、Data.Listモジュールをインポートしておく必要があります。

main :: IO ()
main = do
  shuffled <- shuffleM allCards
  print . sort . take 5 $ shuffled

このプログラムを実行すれば、毎回5枚分のランダムなカードが出力されます。 実際に強い役が出るまで何度も実行してみると時間つぶしくらいにはなるかもしれませんね。

まとめ

というわけで、最初の一歩ということで、トランプを扱うための仕組みを整えました。 次回はポーカーの役を判定する仕組みを作っていく予定です。

ときに、プログラム自体は1時間もかかってないと思うのですが、 文章書くのにまる1日くらいかかってしまいましたorz<手が遅すぎる
完成するまで、どのくらいかかるか解りませんが、気長にお付き合いいただければと思います。

それではノシノシ

次→

株式会社はてなに入社しました

株式会社はてなに入社しました

株式会社はてなに入社しました - hitode909の日記

Haskellオブジェクト指向に触れてみよう〜中級編〜

PRITZサラダ味は神、異論は認めない。

はいどうも、直接本題に入ったら負けだと思っているタイプのHaskeller、ちゅーんさんですこんにちは。 今日も前回に引き続き、「Haskellオブジェクト指向」と題しまして、objectiveを紹介したいと思います。

本題に入る前に

前回の記事に、objective開発者の@fumievalさんからコメント頂きました。

最新のobjectiveでは`stateful handle s where handle` …の代わりに、

{-# LANGUAGE LambdaCase #-}
stringObject :: String -> Object StringObject IO
stringObject s = s @~ \case
   GetString -> get
   SetString s -> put s
   PrintString -> get >>= liftIO . putStrLn

のような書き方が可能で、記述量がかなり減るので実際に使う場合はこちらがおすすめです。  

という事です。
((@~)演算子の存在には気づいていましたけど、そうか、こうやって使えば良いのか・・・)

あとsequentialについては、

`sequential`を使うと一度にたくさんのメッセージを送るメッセージカスケードが実現できます。
採用している言語は少ないですが、自分自身を返すメソッドのチェインより美しくかける上、
対象のインスタンスとは独立してメッセージを組み合わせられるため有用性が高いです。

という事です。

下手にメソッドチェインを使って、

human.birthday().birthday().birthday();

とするよりは、

human.- do
  birthday
  birthday
  birthday

のように書けたほうが綺麗ですし、「メソッドそのものを拡張出来る」という点で有用かもしれません。

中級編でやる事

  • 初級 - objectiveを使ってオブジェクトを実装でき、写経プログラミングで簡単な拡張が行える
  • 中級 - オブジェクトの合成を理解し、自在に拡張できる
  • 上級 - 定命のオブジェクトやストリーム等の高度な応用を使えるようになる
  • 番外 - オブジェクトの圏を理解し、圏論の言葉でオブジェクト指向を定式化できる

初級編では、基本的なオブジェクトの作り方や、オブジェクトを拡張する具体的なコードを紹介しました。

中級編ではぐっと本質に踏み込んで、objective自信の「オブジェクト」に対する考え方や、 オブジェクトの合成の説明を行い。 objectiveを使って、Java等のオブジェクト指向言語における「継承」と同等の拡張を行う方法について議論します。

オブジェクトとメッセージ

前回までは、特に良く知られたクラスベースオブジェクト指向言語の考え方に合わせるため、 オブジェクトに対する振る舞いを指示する操作を「メソッド呼び出し」と呼びましたが、 objectiveにおけるオブジェクトの基本的な考え方はメッセージパッシング方式です。

Object F Gという型は、「何処か」からFという型のメッセージを受け取ると、 自身の状態を変更して、Gという型のメッセージを「何処か」へ送ります。

尚、この時FGという型自体の事は、元論文でも「インターフェイス」と読んでいるようです。

Object HumanObject IOという型のオブジェクトは、 HumanObjectという型のメッセージを何処かから受け取ると、 IOというメッセージを何処かへ送信します。 (.-)演算子の役割は、オブジェクトのインスタンスに対して、 直接メッセージを送信し、受信したMonadIO m => mのメッセージを実行する事なのです。

以下のようなインターフェイスFGを考えましょう。

data F a where
  MessageX :: F ()
  MessageY :: F ()

data G a where
  MessageA :: G ()
  MessageB :: G ()

ひとまず、オブジェクトの「状態を保持する」という性質を忘れて、 受信メッセージを元に送信メッセージを作成するという性質に着目すると、 次のような関数を考える事ができます。

messagePassing :: F a -> G a
messagePassing MessageX = MessageA
messagePassing MessageY = MessageB

objectiveパッケージには、このような「受信メッセージを元に送信メッセージを作成する関数」を、 直接オブジェクトに変換するliftO関数が定義されています。

*Main> :t liftO
liftO :: Functor g => (forall x. f x -> g x) -> Object f g

しかし、GFunctor型クラスのインスタンスになっていないため、うまくオブジェクトにliftする事ができません。 メッセージはGADTsを使って定義される事が多いため、簡単にFunctorインスタンスにする事もできなさそうです。

そこで、Operationalを使い、messagePassing型の返却値の型をProgram G aとします。

messagePassing :: F a -> Program G a
messagePassing MessageX = singleton MessageA
messagePassing MessageY = singleton MessageB

こうすると、ProgramMonadであり、同時にFunctorでもありますから、 一度に複数のメッセージを送れるようになるだけでなく、 以下のように、liftOによってオブジェクトにする事ができるのです。

objX :: Object F (Program G)
objX = liftO messagePassing

Object F GFGがそれぞれメッセージである、という考え方に則ると、 liftOのFunctor制約は、暗にメッセージはFunctorでなくてはならないという事を述べています。

というと、少々小難しい話になってしまいますので、次の一文は最悪読み飛ばしてしまっても良いのですが・・・(´・ω・`)

FProgram Gも「メッセージ」ですから、気持ちとしてはFもFunctorであって欲しいわけですよね。
しかし結局の所Object (Program F) (Program G)というオブジェクトはsequential objXとする事で簡単に作ることができ、 このオブジェクトの成す事は本質的にobjXそのものと違い無いので、基本的に同じものと考えても良さそうです。
Program F -> Program Gという関数を作る事の面倒さを考えれば、このように定義したほうが楽ですし、さしあたりobjXの定義はこれで問題ありません。

これ以上突っ込むと、ちょっとアカデミックな空気感漂う話をしなくてはいけなくなってしまう気がするので、 そのへんはまた、機会があればするかもしれません。しない気もします。

はい、本題に戻りましょう。

messagePassingは、基本的にオブジェクトを作るために作成したものですから、 LambdaCase言語拡張を使って、以下のようにliftOに直接渡してしまったほうが綺麗ですね。

objX :: Object F (Program G)
objX = liftO $ \case 
    MessageX -> singleton MessageA
    MessageY -> singleton MessageB

尚、型の関係がわかりやすいため、本エントリでは次のように書くようにします。

objX :: Object F (Program G)
objX = liftO handle
  where
    handle :: F a -> Program G a
    handle MessageX = singleton MessageA
    handle MessageY = singleton MessageB

オブジェクトの合成

objectiveによるオブジェクト指向がメッセージパッシングであるという説明をした際に、 オブジェクトは「何処か」からメッセージを受け取り「何処か」へとメッセージを送信するという話をしましたが、 この「何処か」というのは、自分以外の別のオブジェクトの事を表しています。

前の章で定義したobjXオブジェクトは、Fというメッセージを受け取りGというメッセージをを送るわけですが。 メッセージの型を辺とすると、次のような図で表す事ができます。

            objX
      F ------------> G

続いて、Object G IOという型を持ったobjYを定義します。

data G a where
  MessageA :: G ()
  MessageB :: G ()

objY :: Object G IO
objY = liftO handle
  where
    handle :: G a -> IO a
    handle MessageA = putStrLn "MessageA"
    handle MessageB = putStrLn "MessageB"

このオブジェクトを先ほどの図に加筆しましょう。

            objX             objY
      F ------------> G ------------> IO

このように並べると、objXFのメッセージを受け取って、GのメッセージをobjYに対して送信する事が可能で、 全体としてFのメッセージを受け取り、IOのメッセージを送信する大きな流れが出来ている事がわかります。 この流れは、全体としてFを受信メッセージ、IOを送信メッセージとする新たなオブジェクトに見えるでしょう。

実際に、そのような合成を行うのが、(@>>@)演算子です。

Main> :t (@>>@)
(@>>@) :: Functor h => Object f g -> Object g h -> Object f h

objectiveではこの演算の事をオブジェクトの合成と呼んでいます。

オブジェクトの合成を具体的なコードにして見てみましょう。 objXが送信するメッセージの実際の型はProgram Gなので、 sequential関数でobjYの受信メッセージをモナドにする必要があります。

objZ :: Object F IO
objZ = objX @>>@ sequential objY

main :: IO ()
main = do
  y <- new objY
  y.-MessageA
  y.-MessageB

  putStrLn "----"
  z <- new objZ
  z.-MessageX
  z.-MessageY

実行結果:

MessageA
MessageB
----
MessageA
MessageB

基本的には、前回Sum(@||@)を使って行ったようなメッセージの合成と オブジェクトの合成を組み合わせる事によって、 さまざまな拡張が可能になるというのが、objectiveの骨子になります。

状態を扱うオブジェクト

さて、内部状態を持つという事は、オブジェクトには欠かすことの出来ない要件です、 前回、stateful関数を使って状態を扱うオブジェクトを作りましたが、 ここでは少しだけこのstateful関数の仕組みを分解してみましょう。

次のような、送信メッセージがStateT Int IOという型となるオブジェクトを考えてみます。

data S a where
  GetS :: S Int
  SetS :: Int -> S ()
  PrintS :: S ()

objS :: Object S (StateT Int IO)
objS = liftO handle
  where
    handle :: S a -> StateT Int IO a
    handle GetS = get
    handle (SetS x) = put x
    handle PrintS = get >>= liftIO . print

StateT s IO aMonadIOインスタンスですから、objSはこのままでも次のようにして使う事ができます。

main :: IO ()
main = runStateT stateInt 100 >>= print

stateInt :: StateT Int IO ()
stateInt = do
  s <- new objS
  s.-PrintS
  s.-SetS 200
  s.-PrintS
  put 300
  s.-PrintS

実行結果:

100
200
300
((),300)

objSというオブジェクトは、自分のインスタンスが使われている文脈に直接手を入れて状態を操作します。 何故このような事が起こるかと言えば簡単で、objSはそれ自身が内部状態を持っているわけではなく、 あくまでSというメッセージを受け取るとそれに応じてStateTのメッセージを送信しているだけだからです。

もちろん、Readerモナド等と組み合わせればこのような仕組みが有用な場合もあるかもしれませんが、 基本的にオブジェクトの状態はカプセル化され、スコープはオブジェクトで完結しているべきでしょう。

直接内部状態を書き換える事ができるオブジェクトを作るには、Object型自体の仕組みを理解している必要がありますが、 実際には自分で作るまでもなく、objectiveパッケージで用意されています。

*Main> :t variable
variable :: Monad m => s -> Object (StateT s m) m

このオブジェクトはStateTメッセージが送られてくると、自分自身の状態を書き換える事で、カプセル化を行います。

試しに単体で動かしてみましょう。

main :: IO ()
main = do
  v <- new $ variable "Hoge"
  -- vのインスタンスにStateTをメッセージとして送信
  v.- do
    x <- get
    put $ "~~~" ++ x ++ "~~~"
  -- 先ほど送ったメッセージにより、
  -- vの内部状態が書き換えられている
  a <- v.-get
  putStrLn a

実行結果:

~~~Hoge~~~

そして、先ほど作成したobjSと、variableオブジェクトを以下のように合成する事によって、 Sをメッセージとして受け取り、IOをメッセージとして送信するオブジェクトが定義できるのです。

objT :: Object S IO
objT = objS @>>@ variable 0

objTの使い方を説明する必要は、もうありませんね。 RankN多相の知識が必要にはなってしまいますが、variableオブジェクトを使えば、stateful関数の再実装も簡単です。

stateful' :: (Functor m, Monad m) 
  => (forall a. f a -> StateT s m a) -> s -> Object f m
stateful' f x = liftO f @>>@ variable x

echoとオーバーライド

objectiveパッケージには、受信メッセージをそのまま返すechoという関数が用意されています。

*Main> :t echo
echo :: Functor f => Object f f

このオブジェクトは、送られてきたメッセージをそのまま送信するだけです。

main :: IO ()
main = do
  -- IO上でインスタンス化されれば、
  -- 単純にIOをメッセージとして受け取り、そのまま送信するため、
  -- 以下のコードはただ単にHello, Worldを実行する
  e <- new echo
  e.-do
    putStrLn "Hello, World!"

次に、話を進めるために、 本エントリで最初に紹介したobjXおよびobjYを再掲します。

data F a where
  MessageX :: F ()
  MessageY :: F ()

data G a where
  MessageA :: G ()
  MessageB :: G ()

objX :: Object F (Program G)
objX = liftO $ \case 
    MessageX -> singleton MessageA
    MessageY -> singleton MessageB

objY :: Object G IO
objY = liftO handle
  where
    handle :: G a -> IO a
    handle MessageA = putStrLn "MessageA"
    handle MessageB = putStrLn "MessageB"

objZ :: Object F IO
objZ = objX @>>@ sequential objY

ここに、受信メッセージも送信メッセージもFである、objFを導入する事を考えましょう。
受信メッセージのFと送信メッセージのProgram Fは同一視しても良いのでしたね。

objF :: Object F (Program F)
objF = liftO handle
  where
    handle :: F a -> Program F a
    handle MessageX = do
    -- MessageXを二回送信
      singleton MessageX
      singleton MessageX
    -- MessageX以外のメッセージはそのまま
    handle t = singleton t

このオブジェクトとobjZを合成する事で、オブジェクトのインターフェイスをそのままに、 既存のオブジェクトの動作が書き換えられます。

この働きは、まさにメソッドのオーバーライドです。

objZ' :: Object F IO
objZ' = objF @>>@ sequential objZ

main :: IO ()
main = do
  z1 <- new objZ
  z2 <- new objZ'
  invoke z1
  invoke z2
    where
      invoke :: Instance F IO -> IO ()
      invoke z = do
        putStrLn "----"
        z.-MessageX
        z.-MessageY

実行結果:

----
MessageA
MessageB
----
MessageA
MessageA
MessageB

ところで、echoとオーバーライドを一度に説明したのには理由があります。

このような、送信メッセージと受信メッセージが同じオブジェクトには、もうひとつ興味深い特徴があるのです。 それは、自分自身を何度も合成する事ができるという事です。
(このような特徴に目を向けるのは、HaskellHaskellらしく書く上でとても重要な事ですね。)

objF' :: Object F (Program F)
objF' = objF @>>@ sequential objF @>>@ echo @>>@ sequential objF

さて、当たり前の事のようですが、echoはいくら合成させても合成させたオブジェクトとまったく同じ振る舞いをします。 そして、(@>>@)演算子結合則を満たします。(詳しい証明は元論文の付録を参照してください)

即ち、送信元と送信先のメッセージが同じオブジェクトは、モノイドとしての性質を持っているのです!

継承の実現

さて、これでobjective上で継承を実現する道具立てが揃いました。 まず、親となるオブジェクトを次のように定義しましょう。

data A a where
  SetA :: String -> A ()
  GetA :: A String
  PrintA :: A ()

objA :: Object A IO
objA = stateful handle ""
  where
    handle :: A a -> StateT String IO a
    handle (SetA s) = put s
    handle GetA = get
    handle PrintA = get >>= liftIO . putStrLn . ("PrintA : "++)

objAを親として作成するobjBは、Aのメッセージだけでなく、 以下に示すBのメッセージも受信できるようにします。

data B a where
  PrintB :: B ()
  PrintA2 :: B ()

従って、objBの型は次のようになりますね。

objB :: Object (Sum A B) IO

今回は、話を簡単にするために、Aのメッセージのオーバーライドは行わない事にします。

PrintBでは単純なIO処理を行いましょう。 PrintA2インターフェイスBの要素ですが、 親オブジェクトとなるobjAPrintAメッセージを二回呼ぶ事にします。

少々複雑な手順を踏む必要があるので、 いきなり全体を作ろうとするのではなく、順番に考えていくと良いです。 まずはBのメッセージそれぞれの振る舞いだけ定義したオブジェクトを作って、 最終的に合成する事を考えましょう。

受信メッセージはBで問題無いとして、送信メッセージはどうなっているべきでしょうか。 objBに新たに追加されるメッセージはそれぞれ、 親オブジェクトのメッセージPrintAと、IOのメッセージのどちらも送信できなくてはいけません。 従って、Sum A IOが送りたいメッセージの型です。

当然、SumはFunctorではないのでliftO出来ませんし、Monadでも無いので何度もメッセージを送ることが出来ませんので、 Operationalの力を借りる必要があります。 結果、次のような部品が出来ます。

mg :: Object B (Program (Sum A IO))
mg = liftO handle
  where
    handle :: B a -> Program (Sum A IO) a
    handle PrintB = do
      x <- singleton $ InL GetA
      singleton . InR . putStrLn $ "PrintB : " ++ x
    handle PrintA2 = do
     singleton $ InL PrintA
     singleton $ InL PrintA

さて、このオブジェクトを上手く合成して、objBの型を満足させる事を考えます。 まず、受信メッセージをSum A Bにするためには、前回の記事で定義した@||@演算子が使えそうですね。

(@||@) :: Functor m => Object f m -> Object g m -> Object (Sum f g) m

これを使うためには、Object A (Program (Sum A IO))という型のオブジェクトが必要です。 もっとも、Aの操作を変えたいわけではないので簡単です。

mf :: Object A (Program (Sum A IO))
mf = liftO handle
  where
    handle :: A a -> Program (Sum A IO) a
    handle x = singleton $ InL x

mfmgの受信メッセージを合成したオブジェクトを、objA'として定義します。

objA' :: Object (Sum A B) (Program (Sum A IO))
objA' = mf @||@ mg

あとはこの、objA'を何か別のオブジェクトと合成して、 最終的にIOを送信するように出来れば、目的のobjBが作成できそうですね。

そもそも今回の目的はobjAを継承して新たなオブジェクトを作る事でしたから、 mfmgの送信するAのメッセージはobjAに送られる事を想定しています。 一方、IOのメッセージは、特に何かしちあわけではありません。 送ったものをそのまま送りなおしてくれれば良いのですが、 そのような働きをするオブジェクトがありましたね。そう、echoの事です。

これをそのまんま定義すると、次のようなobjA''オブジェクトとなります。

objA'' :: Object (Sum A IO) IO
objA'' = objA @||@ echo

後は、objA'objA''を合成すれば、最終目標のobjBが完成です。

objB :: Object (Sum A B) IO
objB = objA' @>>@ sequential objA''

このobjBは、ちゃんとobjAの動作を引き継ぎつつ、 新たに追加されたBのメッセージも、ちゃんと期待通りに動作します。

main :: IO ()
main = do
  a <- new objA
  a.-SetA "Hoge"
  a.-PrintA 
  
  putStrLn "----"
  b <- new objB
  b.-InL (SetA "Piyo")
  b.-InL PrintA 
  b.-InR PrintB
  b.-InL (SetA "Fuga")
  b.-InR PrintA2

実行結果:

PrintA : Hoge
----
PrintA : Piyo
PrintB : Piyo
PrintA : Fuga
PrintA : Fuga

実際には、継承に伴って新たな変数(状態)の追加を行いたい場合も多いでしょうから、 もう少々複雑になるかもしれません。

いまいち手順を整理し切れない方は、次の図を見て考えてみると良いでしょう。 (汚い手書きで申し訳ないです。あと裏面の落書きは親戚のようぢょに描いてあげてたミ◯ーちゃんです。)

f:id:its_out_of_tune:20150330044342j:plain

赤い点線が@||@による受信メッセージの合成を表します。

尚、この継承はobjAobjBの間に多相性が無いという意味で不完全です。 しかしこの問題は、現在Sumとなっている部分がより良い方法に置き換わる事により、 解決されるでしょう。

まとめ

というわけで、中級編では、objectiveにおけるオブジェクト指向は、メッセージパッシングであること。 メッセージの送受信先を定める「オブジェクトの合成」という演算があり、 オブジェクトの合成により、大きなオブジェクトを構築していく事ができるのだと言うこと。 オブジェクトの合成とSum等を組み合わせて、オブジェクトの継承を行う方法等を説明しました。

くどいようですが、objective自体はまだ研究段階ですので、使い勝手という点ではこれからどんどん改善されて行くでしょう。 それよりも、ここまで説明した範囲でも、以下のような凄み(という言い方はアレですが)があると考えています。

  • 大きくわけて2つの演算で、OOPの要求に答えている
    • オブジェクトの合成とインターフェイスの合成
    • 必要な拡張に対して最小の道具立てを選択できる
    • 意味論との親和性がめちゃんこ高い
      • 今までの感覚だとOOPの定式化とかほぼ無理っぽいし・・・
      • 圏論的な話を、機会があれば。
  • 状態の拡張がファーストクラス
    • その気になれば動的にどんな拡張でも出来そう
      • 動的に継承できる静的型付けOOPLなんて今まで無かった
      • オブジェクトのリストの全要素を一斉にオーバーライドとか
  • Haskellの型システムの上で動作する
    • なんかもう、これだけでもやばい

今後書くかもしれない上級編では、Object型の内部実装についてさらに追求し、 定命のオブジェクト、ストリームの表現等、高度な応用について説明しようと思います。

それでは皆様、良いOOPライフをノシノシ

Haskellオブジェクト指向に触れてみよう〜初級編〜

RITZクラッカーは神、異論は認めない。

はいどうも、直接本題に入ったら負けだと思っているタイプのHaskeller、ちゅーんさんですこんにちは。 今日は、「Haskellオブジェクト指向」と題しまして、objectiveというライブラリを紹介したいと思います。

いんとろだくしょん

objectiveは日本人によって開発されたHaskellオブジェクト指向を行うためのライブラリです。いちおうまだ研究段階といった感じではありますが、 色々といじくり回してみた限り、かなり期待が持てる内容になっているため、紹介します。

近い将来には、Lensくらいには手軽に、 Haskellプロジェクトにオブジェクト指向プログラミングを導入できそうです。

手っ取り早く実装レベルで知りたい人は以下の日本語の論文を読むとよいでしょう。

http://fumieval.github.io/papers/ja/2015-Haskell-objects.pdf

また、Hackageへのリンクは以下になります。

objective: Extensible objects | Hackage

前提知識

本当は、まっさらな知識で読み進めてもOKだよんと言いたいところなのですが、 いちおう、本記事を理解するのには、以下の前提知識が必要です。

っていうかオブジェクトを定義/拡張するのに以下の技術が必要なのです(´・ω・`)

  • StateTモナドを使って簡単なプログラムが書ける
  • MonadIO型クラスを理解し、liftIOを通じたIO処理が書ける
  • Operationalモナドの基本的な使い方を知っている
  • GADTs言語拡張を知っている

ある程度Haskell慣れしていれば、見様見真似で書けば動かしていじれると思うので、 とりあえず動かす派の人はそのまま読み進めちゃっても良いんじゃないかとは思います。

基本的な考え方とか

objectiveの目的は「拡張性のある状態の管理手法を導入し、OOPに相当する記述力をHaskellで獲得すること」であり、 端的に言えば、オブジェクト指向言語にありがちな機能を直接提供するのではなく、 あなたが想像する(静的型システムを持った)オブジェクト指向言語で可能な大概の事を、 Haskell上で実現するための一貫した仕組みを得られる道具立てと考えると良いと思います。

けっこう最近出来たものなので、 最小の道具立だと不十分だったり、現状まだ記述性が良くなかったりという問題も残っていますので、 今回は細々と補足しながらご紹介する事になると思います。

これらは今後、改善されるものと思って間違いないでしょう。

このように、周辺が整っていないという意味で、 今すぐあなたのHaskellプロダクトにオブジェクト指向を採用するのは難しいかもしれませんが、 着々と実用に近づいている技術ですから、知っておいて損は無いと思います。

初級編ってどういう事なの

一応、次のようなステップを考えています。

  • 初級 - objectiveを使ってオブジェクトを実装でき、写経プログラミングで簡単な拡張が行える
  • 中級 - オブジェクトの合成を理解し、自在に拡張できる
  • 上級 - 定命のオブジェクトやストリーム等の高度な応用を使えるようになる
  • 番外 - オブジェクトの圏を理解し、圏論の言葉でオブジェクト指向を定式化できる

が、基本的に気まぐれで更新してるブログなので、実際に書くかどうかわかりません:-P

文字列オブジェクトの定義

まず最初に、Objectの定義を見てみましょう。

newtype Object f g
  = Object {runObject :: forall x. f x -> g (x, Object f g)}
    -- Defined in `Control.Object.Object'

現段階で中身について理解する必要がありません(多分上級編で解説します)
Objectfg二つの型引数を取る型だという事だけ確認すればOKです。 とりあえずは、それぞれ次のような意味だと思って頂ければ良いかと思います。

さて、objectiveを用いたオブジェクトの作成は、最初にインターフェイスを定義する所からはじまります。

文字列を内部状態に持ったオブジェクトを想定して、StringObjectインターフェイスを定義する事を考えましょう。 インターフェイスはGADTsを用いて、次のように定義します。

data StringObject a where
  GetString :: StringObject String
  SetString :: String -> StringObject ()
  PrintString :: StringObject ()

よく知られたJava等のクラスベースオブジェクト指向言語であれば、 次にインターフェイスに対する実装クラスを作る事になるでしょう。

objectiveにクラスという概念はありませんが、 インターフェイスの各メソッドに対して具体的な「振る舞い」を定義し、 何かしらの初期値によって状態を初期化し、オブジェクトを返す関数が定義出来れば、 それはインターフェイスの実装クラスのコンストラクタに相当しますね。

実際に文字列オブジェクトを初期化して返す関数の定義を、以下に示します。

stringObject :: String -> Object StringObject IO
stringObject s = stateful handle s
  where
    handle :: StringObject a -> StateT String IO a
    handle GetString = get
    handle (SetString s) = put s
    handle PrintString = get >>= liftIO . putStrLn

statefulhandleのような関数と初期値を引数にオブジェクトを作成する関数です。 handleで実際に各メソッドに対する具体的な「振る舞い」を定義します。

実際には、IOにlift出来る色々なモナド変換子の上でオブジェクトが使えると便利なので、 以下のようにIOの部分はMonadIOに書き換えると良さそうです。

stringObject :: MonadIO m =>  String -> Object StringObject m
stringObject s = stateful handle s
  where
    handle :: MonadIO m => StringObject a -> StateT String m a
    handle GetString = get
    handle (SetString s) = put s
    handle PrintString = get >>= liftIO . putStrLn

インスタンス生成とメソッド呼び出し

objectiveには、クラスという概念は存在しませんが、 Object自体には自身の状態を管理する力が無いため、メソッド呼び出しによって変更された状態を、 上手く管理する仕組みは外部のもっと大きな仕組みに頼るしかありません。

new関数は、IOの裏方でObjectを状態として管理するための型、Instanceに変換します。

*Main> :t new
new :: MonadIO m => Object f g -> m (Instance f g)

objectiveにおいて「インスタンス」とは、 ObjectMonadIO m => m内で扱えるようにラップした型だと考えると良いでしょう。

そして、new関数によって取得したインスタンスメソッドは(.-)演算子によって呼び出す事ができます。

*Main> :t (.-)
(.-)
  :: (MonadIO m, Control.Monad.Catch.MonadMask m) =>
     Instance f m -> f a -> m a

MonadMaskという見慣れない型クラスがありますが、StateTRWST等、 Objectを使いたい大概の型の上ではインスタンスになっているので、 あまり気にしなくても良さそうです。

っていうかこれ何すか?初めて見た。

んでは、実際にこれらを使って、インスタンスの生成からメソッド呼び出しまでを、実際に試してみましょう。

main :: IO ()
main = do 
  -- インスタンス生成
  str1 <- new $ stringObject "Hoge"
  -- メソッド呼び出し
  str1.-PrintString
  str1.-SetString "Foo"
  str1.-PrintString
  -- 取得
  x <- str1.-GetString
  putStrLn $ "x = " ++ x

実行結果:

Hoge
Foo
x = Foo

ところで、ここでstringObjectの型はObject StringObject mとなっており、 オブジェクトの「振る舞い」は型に現れない事に注目すると、 同インターフェイス実装クラス間にある、振る舞いのポリモルフィズムは既に達成されている事がわかるでしょう。

実際、以下のようなnamedStrObjectオブジェクトを実装し・・・

namedStrObject :: MonadIO m => String -> String -> Object StringObject m
namedStrObject n s = stateful handle s
  where
    handle :: MonadIO m => StringObject a -> StateT String m a
    handle GetString = get
    handle (SetString s) = put s
    handle PrintString = get >>= liftIO . putStrLn . ((n ++ ": ") ++)

以下のように、同じinvoke関数に適用する事ができます。

main :: IO ()
main = do
  str1 <- new $ stringObject "Hoge"
  str2 <- new $ namedStrObject "Tune" "Hoge" 
  invoke str1
  invoke str2
    where
      invoke :: Instance StringObject IO -> IO ()
      invoke obj = do
        obj.-PrintString
        obj.-SetString "Piyo"
        obj.-PrintString

実行結果:

Hoge
Piyo
Tune: Hoge
Tune: Piyo

人間オブジェクトとカプセル化

オブジェクトの振る舞いを定義する際、handle内でStateTに複雑なデータ型を内包させれば、 複雑な状態を扱うオブジェクトを実装する事ができそうです。

例として、名前と年齢を内包する人間オブジェクトを実装してみましょう。 あーそこ、ありがちな例で面白くないとか言わないこと。 まず、人間オブジェクトが内包する状態のために、型HumanStateを定義します。

data HumanState = HumanState
  { humanName :: String
  , humanOld :: Int
  }

文字列オブジェクトの例では、GetStringおよびSetStringメソッドを通じて、 stringObjectが内包する状態の全てに自在にアクセスする事ができました。

言い換えれば、メソッドの定義によっては、状態に対する操作を制限・・・所謂カプセル化が可能です。 具体例として、人間オブジェクトのインターフェイスを以下のように定義します。

data HumanObject a where
  GetName :: HumanObject String
  GetOld :: HumanObject Int
  Birthday :: HumanObject ()
  Greeting :: HumanObject ()

名前や年齢に対してそれぞれのゲッターを定義します。 年齢に対する操作は、Birthdayメソッドによるインクリメントのみサポートし、 名前の変更は認めません。

また、状態をアウトプットする機能として、自己紹介を行うGreetingメソッドを提供する事にしましょう。

そして、人間オブジェクトの具体的な振る舞いを、以下のように実装します。

humanObject :: MonadIO m => String -> Int -> Object HumanObject m
humanObject n o = stateful handle (HumanState n o)
  where
    handle :: MonadIO m => HumanObject a -> StateT HumanState m a
    handle GetName = get >>= return . humanName 
    handle GetOld = get >>= return . humanOld
    handle Birthday = do
      s <- get
      put $ s { humanOld = humanOld s + 1 }
    handle Greeting = do
      s <- get
      liftIO . putStrLn $ "Hello! I'm " 
        ++ humanName s ++ ", " ++ show (humanOld s) ++ " years old!"

このオブジェクトを使ったプログラム例を以下に示します。

main :: IO ()
main = do
  yuzuko <- new $ humanObject "Yuzuko" 16
  yuzuko.-Greeting
  yuzuko.-Birthday
  yuzuko.-Greeting

実行結果:

Hello! I'm Yuzuko, 16 years old!
Hello! I'm Yuzuko, 17 years old!

実際に、main関数からインスタンスyuzukoの名前を変更したり、若返えるような操作をする事はできません。 stateful関数を用いてオブジェクトを生成すると、内部状態をカプセル化する事ができるのが、わかると思います。

インターフェイスの拡張

この章からは、オブジェクトを拡張していく技術を紹介していきましょう。

まず、既にあるインターフェイスを拡張する方法について考えます。 人間オブジェクトのインターフェイスは次のように定義されていました。

data HumanObject a where
  GetName :: HumanObject String
  GetOld :: HumanObject Int
  Birthday :: HumanObject ()
  Greeting :: HumanObject ()

このインターフェイスを拡張して、文字列を一つ保持出来るようにする事を考えてみましょう。 文字列を保持するインターフェイスStringObjectとして既に定義されていました。

data StringObject a where
  GetString :: StringObject String
  SetString :: String -> StringObject ()
  PrintString :: StringObject ()

両方のインターフェイスの実装クラスを作る事を考えますと、 呼び出されるメソッドHumanObjectStringObjectの「どちらか」に属します。 「どちらか」を表すのは直和型・・・つまりEitherですが、インターフェイスは多相型でなくてはいけません。

そこで、多相版Eitherとなる、Sum型を定義しましょう。

data Sum f g a = InL (f a) | InR (g a)

これで、Sum HumanObject StringObjectインターフェイスとして見た時に、 次の7つのメソッドが定義されていると考える事ができます。

  InL GetName :: Sum HumanObject StringObject String
  InL GetOld :: Sum HumanObject StringObject Int
  InL Birthday :: Sum HumanObject StringObject ()
  InL Greeting :: Sum HumanObject StringObject ()
  InR GetString :: Sum HumanObject StringObject String
  InR . SetString :: String -> Sum HumanObject StringObject ()
  InR PrintString :: Sum HumanObject StringObject ()

もちろん、これをベースにオブジェクトを実装しなおしても良いのですが、 既存のオブジェクトを組み合わせて、二つのオブジェクトの機能を併せ持ったオブジェクトを作る、 (@||@)演算子を定義する事ができます。

(@||@) :: Functor m => Object f m -> Object g m -> Object (Sum f g) m
a @||@ b = Object $ \r -> case r of
  InL f -> fmap (fmap (@||@b)) $ runObject a f
  InR g -> fmap (fmap (a@||@)) $ runObject b g

尚、この演算子の実装はObject型の仕組みにまで踏み込まないといけないため、理解できなくても構いません。

実際に、この演算子を使って、humanObjectnamedStrObjectを組み合わせ、 記憶力のある人間オブジェクト・・・いわば「賢い人オブジェクト」を実装して、使ってみましょう。

humanObjectGe :: (Functor m, MonadIO m) => 
  String -> Int -> String -> Object (Sum HumanObject StringObject) m
humanObjectGe n o s = humanObject n o @||@ namedStrObject n s

main :: IO ()
main = do
  h <- new $ humanObjectGe "Yuzuko" 16 "tune is nice guy."
  h.-InL Greeting
  h.-InL Birthday
  h.-InL Greeting
  h.-InR PrintString
  h.-InR (SetString "haskell is cool!")
  h.-InR PrintString

実行結果:

Hello! I'm Yuzuko, 16 years old!
Hello! I'm Yuzuko, 17 years old!
Yuzuko: tune is nice guy.
Yuzuko: haskell is cool!

論文で紹介されているこのSum型や(@||@)演算子は、現在のobjectiveパッケージには採用されていません。

一連の説明を読んでいて感じていたように、 メソッドの呼び出しにInLInRの記述が必要なのは筋が良くありません。 組み合わせるインターフェイスの数が増えれば増えるほど扱いが困難になりますし、 多相に扱う事ができないというかなり深刻な問題もあります。

じつは、現状のobjectiveが抱えている問題は概ねここに集約されており、 extensibleを応用してSumに変わる良い感じの仕組みがあるそうなのですが、 この記事を書いている段階では、まだ公開には至っていないという事なんですね(´・ω・`)

実際に採用された場合は、Sumより抽象度が高くなるのは間違いなく、 解説も難しくなってしまうのですが、基本的な考え方は変わらないはずなので、 このままSum(@||@)を使って解説していきます。

Operationalとメソッドのオーバーライド

もう一つ、オブジェクトを拡張する方法として、 あるメソッドの効果を上書きするオーバーライドがあります。

メソッドのオーバーライドには、Operationalモナドの仕組みを利用します。 まず、天下り的に、インターフェイスをOperationalによってモナドにする、 sequential関数を実装します。

sequential :: Monad m => Object t m -> Object (Program t) m
sequential r = Object $ liftM (fmap sequential) . inv r
  where
    inv :: Monad m => Object t m -> Program t t1 -> m (t1, Object t m)
    inv obj (Program (Pure x)) = return (x, obj)
    inv obj (Program (Free (CoYoneda f x))) 
      = runObject obj x >>= \(a, obj') -> inv obj' (Program . f $ a)

尚、このsequentialは、自作のreasonable-operationalパッケージに依存したもので、 Operationalの実装にあわせて書き換えが必要かもしれません。 (例によって現状のobjectiveパッケージには導入されていません)

本来、(.-)演算子によるメソッド呼び出しは一度に一回ですが、 こうする事によって複数メソッドをいっぺんに送る事ができます。

main :: IO ()
main = do
  h <- new . sequential $ humanObject "Yuzuko" 16
  h.- do
    singleton Greeting
    singleton Birthday
    singleton Birthday
    singleton Greeting

この仕組みだけ見ると、使いどころあるんだか無いんだかという感じですが、 メソッドのオーバーライドを実現する際に必要です。

実例として、1年で3回も年を取る、早熟人間オブジェクトを定義しましょう。 尚、体の成長が3倍なのか、3倍の速さで時間が進んでいるか、単なる痛い子なのか等、難しい事を考えてはいけません。

以下が、その実装です。

humanObjectPr :: MonadIO m => String -> Int -> Object HumanObject m
humanObjectPr n o = liftO handle @>>@ sequential (humanObject n o)
  where
    handle :: HumanObject a -> Program HumanObject a
    handle Birthday = do
      -- 親のBirthdayメソッドを3回呼び出す
      singleton Birthday 
      singleton Birthday
      singleton Birthday
    handle t = singleton t

初級編ではどういう理屈で実現出来るかは説明しません。 基本的に、sequentialの引数を親オブジェクトに差し替えて、 handleの実装を書き換える事で自在に振る舞いを書き換える事ができるので、色々と書き換えてみると良いでしょう。

動作を確認します。

main :: IO ()
main = do 
  h <- new $ humanObjectPr "Kaede" 6
  h.-Greeting 
  h.-Birthday
  h.-Greeting 
  h.-Birthday
  h.-Greeting 

実行結果:

Hello! I'm Kaede, 6 years old!
Hello! I'm Kaede, 9 years old!
Hello! I'm Kaede, 12 years old!

Birthdayメソッドの振る舞いが変更されて、 他のメソッドは親オブジェクトから変更されていない事が確認できると思います。


humanObjectPrのパターンでは、 オーバーライドの際にIOアクションを実行する事ができず、出来る事がかなり限られてしまいます。

前の章で定義したSum型を使って、 handle関数の右辺の型をProgram (Sum StringObject IO)と出来れば、 handle関数の振る舞いを定義する際に、親クラスのメソッドとIOアクション、両方使う事が出来ますね。

その際には、(@>>@)関数の右辺に少し工夫が必要ですが、 今回はhumanObjectPrの時と同じように、結果の実装だけお見せしましょう。

以下のサンプルコードは、自己紹介を日本語で行う日本人版人間オブジェクトを、普通の人間オブジェクトを親として作成したものです。
例によって、handle関数と、親オブジェクトを指定する部分だけ書き換えれば、 任意のオブジェクトのメソッドをオーバーライドする事が可能です。

humanObjectJa :: (Functor m, MonadIO m) => String -> Int -> Object HumanObject m
humanObjectJa n o = liftO handle @>>@ sequential (humanObject n o @||@ echo)
  where
    handle :: (Functor m, MonadIO m) => HumanObject a -> Program (Sum HumanObject m) a
    handle Greeting = do
      n <- singleton $ InL GetName
      o <- singleton $ InL GetOld
      singleton . InR . liftIO . putStrLn
        $ "こんにちは!私の名前は" ++ n ++ "、" ++ show o ++ "歳です!"
    handle t = singleton . InL $ t

main :: IO ()
main = do
  yuzuko <- new $ humanObjectJa "ゆずこ" 16
  yuzuko.-Greeting
  yuzuko.-Birthday
  yuzuko.-Greeting

実行結果:

こんにちは!私の名前はゆずこ、16歳です!
こんにちは!私の名前はゆずこ、17歳です!

まとめ

「オーバーライド」や「インターフェイスの拡張」に使った仕組みを上手く組み合わせば、 継承と同等の拡張を実現する事が出来そうな雰囲気も感じ取って頂けるんじゃないでしょうか。 自信のある方は、GHCiの:tコマンド等を駆使して、挑戦してみると良いでしょう。

よく知られたオブジェクト指向言語の機能と同等の仕組みを実現する手法をいくつか紹介したわけですが、単純にコードだけ見ると、だいぶテクニカルに見えます。
これは、objectiveという比較的シンプルな仕組みをベースに実現している事が原因です。
よく使うパターンを扱いやすい単位で切り出して関数にするのは、そんなに難しいことでは無いでしょうし、記述性については今後どんどん改善されていくでしょう。

というわけで、初級編ではobjectiveの基本的な使い方を示し、 ちょっとしたオブジェクトの拡張を実演してみせました。

中級編では、Object型の仕組みにもう少し踏み込んで、 今回紹介したオブジェクトの拡張がどのような理屈で可能だったのかを説明し、 継承の実現方法の答え合わせをしようと思います。

それでは皆様、良いOOPライフをノシノシ

モナド基礎勉強会vol2(#monadBase)で「米田の補題」の話をして来ました

はいはいどうも、花粉症で鼻水じゅびじゅばなちゅーんさんです、おこんばんわ。

色々あって現在求職中なのもあり、自分の書きたいコードをゴリゴリ書いたり、理論的な事をがつがつ勉強したりする良い時間を作れていて、だいぶ下請けPG時代に貯まった疲れも取れてきたのかなぁとか思ったりしてます。 これを期に手をつけはじめた事が色々あるので、そのうちいくつかでも自分の中でケジメを付けるまで、もうちょっとのんびりしたいなぁと思い、寄生している実家の母親の顔色と、銀行口座残高とにらめっこしつつ、やりたいようにやってる感じです。

さて、そんな機会を使って、名古屋で開催された「モナド基礎勉強会vol2」へ足を運んで登壇させて頂きましたので、今日はそのレポートというか、雑記みたいなエントリです。

どんな勉強会だったのか

こんなんです

xbase.connpass.com

「基本」ではなく「基礎」ですから、午前の部でがっつり圏論入門をした後、 午後の部で色々人が色々な応用分野について、基礎的な・・・それでいて高度な話を発表しました。

それぞれどんな内容だったのかは、@hirataraさんがまとめてくださっているので、そちらを参照してください。

hiratara.github.io

米田の補題の話をして来たよ

発表内容が決まった経緯とか、具体的な内容は、発表スライドを参照してくだしあ。

http://tokiwoousaka.github.io/takahashi/contents/150321monadBase.html

正直、がっつり圏論な話を人前でするのは初めてで、かれこれ3年以上は色んな勉強会で登壇させて頂いているとはいえ、さすがにビビってました。

補足とか

スライド内で「米田埋め込み」について、「これは任意の圏をSetに移せるため有用・・・だそうだ。」なんて話をしたのですが、懇親会で@t6sさんにお話を伺ったところ、実際にはSetのelementが取れるので有用なんだとか、それを応用するとどんな凄い事が出来るのか、みたいな話を伺ってわりと刺激な内容だったのですが、現状それを十分理解できてアウトプット出来る地力が無いので、いずれまたまとめたいと思います。

また、余米田の補題については、現状いくつか(意味合いの異なる)定義があって、おそらくHaskell実装はそのうちのコレだろうみたいな話も出来たのですが、半分くらいしか理解できておらず、結局なんで自然変換が積に変わったのか理解できなかったので、今後の課題という事になりそうです。

名古屋観光に関する話とか

せっかく名古屋へ行くんだからと、二泊三日して最終日は軽く観光して来ました。 といっても、そんなに時間があったわけでは無いので、現地で合流した @halcat0x15a 君と、@igrep さんと名古屋城をぐるっと一周した感じです。

個人的に、名古屋城天守閣にエレベーターが設置されていたのが一番の衝撃だった気がします。 あときしめん美味しかったです。

さいごに

なによりも、このような機会を下さった @kyon_mm さん、どうもありがとうございました。本当に楽しかったです。

今回の勉強会を受けて、今後の身のふり方について決めた事があるのですが、それについてはまた期が来たら公にしようかなぁとか思います。

Haskellでプレゼン資料を作ってみよう!!

gdgdと文章を読みたくない人のためにとっとと本題済ませます。 ちゅーんさんがHaskell上でプレゼン用のスライドを生成するための言語内DSLを公開しました。

Hackage : http://hackage.haskell.org/package/takahashi
サンプル兼ドキュメント:http://tokiwoousaka.github.io/takahashi/contents/20150213takahashi.html (ソースコード) Githubhttps://github.com/tokiwoousaka/takahashi

以上のURLを元によろしくやって頂ければ幸いです。

開発に至った経緯とか

はいども、こころぴょんぴょんちゅーんさんです。

皆さん、プレゼンしてますか? プレゼンは大事ですよー、婚活と同じくらいには大事です。

というわけで、 イマドキのプレゼンテーションには欠かすことの出来ない、 発表用のスライドですが、普段どうやってスライドを作りますか?

Windows使いの皆さんは爽やかにMS OfficePowerPointなんかを使ってるんじゃないかなーと思いますが、 Linuxを愛用しているギークの皆さんは、 OOoのImpressだと納得できない(マウス持ちたくない的な意味で)人も多いんじゃないかと思われます。 わかります、発表スライドもvimで作りたいじゃないですか。LaTeX?あーあーきこえないきこえない。

あと、Macは知らない。


というわけで、ちゅーんでも使えるLinuxUbuntuを愛用するちゅーんことちゅーんさんも、 納得できるスライド作成環境を求めてそれはそれは長いこと彷徨ってきました。

一時期、gistのMarkDownをスライドにするようなWebサービスを愛用していたのですが、 いずれもgistの仕様変更について行けず軒並み動かなくなってしまい、 んで、似たような事をローカルで出来るツールを探してみたのですが、納得いけるものは見つからず。

無いなら作ればいいじゃない、という事で半年ほど前に開発に至ったわけです。

もともとは、名古屋こわいで有名なよんたさん(@keita44_f4)が、 関数型LT大会高橋メソッドで発表していたのにインスピーレーションを受け、 これなら実装も簡単だろうという事で、 高橋メソッドのスライドをちゃかちゃかっと作るような仕組みを、半日くらい使って作りました。

で、しばらくそのまんま運用していたんですが、 LTならまだしも、15分〜の発表に高橋メソッドを採用するのは色々と限界があるなーと思ってた矢先、 3月下旬にモナド基礎勉強会vol2で発表する機会を頂きました。 それを受け、本ライブラリを色々なスライドを作れるように大改造を施し、 わりと実用的なレベルになってきたので、今回の公開に至ったわけです。

Takahashiという名称にはそんな経緯があったりとかなかったりとかするのです。

Takahashi Monadの良いところ

  • エディタだけで作れる
  • 導入が楽
    • ただしHaskellerに限る
  • それっぽく書けばそれっぽくなる
  • 出来上がったスライドはブラウザさえあれば閲覧できる
  • モナドとして提供されている
    • 無駄にチューリング完全
    • 拡張に対する可能性が高い
      • もくじページの自動生成とか出来そう
    • 型安全 ← 超重要

Takahashi Monadの悪い所ところ

  • おだてにも読みやすいとか書きやすいとか言えない
  • 細かい表示制御は出来ない
  • 出力されるHtmlソースがなかなか酷い
    • 誰か助けて
  • 抜ける所は手を抜いて作ってるので影響無い程度には遅い
  • モナド変換子になっていない

というわけで

Linuxでのスライド作成環境に悩んでいる方は、試しにTakahashiMonadを導入してみは如何でしょうか。

そんで、バグ報告とかPullRequestとか頂けると嬉しいです。 Let's join me!!

「圏論を学んで得られる事、得られない事」に対する補足

はいどうも、萌え豚にはなるまいと思っていた数年前の自分はどこへやら、今ではゆゆ式が生きがいのちゅーんさんです。

以前

こんな感じの記事を書きました所、主にブクマにて色々な意見を頂きまして、 色々と説明不足だったかなーという所もありましたので、補足のような記事を書くことにしました。

尚、本記事では、コメント欄やはてブに付けられたコメントを引用させて頂いております。 問題ありましたらはてブやコメント欄、あるいは僕のTwitterアカウントまでご連絡頂ければ、 対応致しますのでご一報ください。

何が言いたかったか

何度も言ってることですけど、これが伝わってないと話にならないので、 もう一度簡単に触れておきます。 この記事は読者に圏論を教えるためのブログ記事ではなく、 圏論とプログラミングの立ち位置を、伝える事が目的でした。

もともと、一部で「銀の弾丸」的なニュアンスで広まっている雰囲気がありましたので、 多少ネガティブなニュアンスで紹介してしまったが故に、余計に敬遠されている方も居たような気がしたので、 今回はもう少し、ポジティブな側面を紹介して行く事にしましょう。

知るべきか知らざるべきか

結論から言うと、「知るべき」です。

これは別に圏論に限らず、我々がエンジニアであれば、関連する技術/知識の中に「知らなくて良いこと」なんて無いのだという意味だと、 ご理解いただければと思います。 自戒を込めて。

と、

そんな感じのタテマエを前提とした上で、時間は有限ですから、もう少し現実的な目線から圏論を知るべきか、という話をします。 Haskellもくもく会なんかでお世話になっているigrepさんが、面白い事をおっしゃっていました。

igrep :
敢えてここで持論を述べるなら、プログラマにとっての圏論社会学者にとっての統計学みたいなもんだと思ってる。 詳しい仕組みを知るに越したことはないけど、知らなくても使えればいい。

社会学者にとっての統計学」というのは絶妙な例えだなーと思いました。 ただ、「社会学統計学」ならば「プログラミングと意味論」の方がよりニュアンスが近いかもしれない、という印象があります。 意味論について、なるべく平たく表現すると、「プログラミングの仕組み」を扱う計算機科学の一分野ですね。

「プログラミングの仕組み」というと、コンパイラアセンブリ機械語、んで、論理回路なんかをイメージする人が多いかもしれないですが、 (そしてそれも重要ではあるのですが)ここで言う「プログラミングの仕組み」とは、 「変数に代入するってどういう事?」とか「ループってどういう事?」みたいな、我々が「高級」と呼んでいる部分にスポットを当てて、 数学のノウハウを使ってその仕組みを再定義し、応用していこうという分野です。

そして意味論には、圏論の応用も含まれます。 関数型プログラミング言語の型システムと対応する圏論は、JavaC#が「関数型化」しつつある現代において、 決して「Haskellerだけのもの」ではなくなっているという事を述べておきましょう。

ラムダ式、カリー化、Maybe型(あるいはOption型)、これらはもはや関数型言語だけのものではありません。 これらの技術を有意に応用した設計を行うにあたり、 圏論は(直接的な力にはならないかもしれませんが)大いに「ヒント」となり得るのです。

amaliche:
まぁ実務で役立つ知識ではないし、色んな意味で近寄りがたい。

以上の事を踏まえ、「実務で役立つ知識ではない」は誤りなので訂正させて下さい。

知識として習得した上で、あなたの実務で役立たせる事が出来るか否かはあなた次第です。 先ほど申し上げましたように、 圏論はあなたの開発の設計に対して大いに「ヒント」となり得るものです。

「詳しい仕組みを知るに越したことはないけど、知らなくても使えればいい。 」はまさにそうなのですが、 「知っておくに越したことはない」程度には、プログラミングと強い関わりがあるという事は、認識したほうが良いかもしれません。

いかにして学ぶべきか

で、一番説明不足だったなぁと思ったのがこれ。

masarusanjp:
圏論、勉強したいんだけど、何をとっかかりにしていっていいかわからない。何か良い書籍ないかな

ですよねー、そうなりますよねー。

基本的に僕は、小学生の時にサンプルコードを使ってVB5の基本的な構文を覚えた程度には独学派なので、 大抵の事は、あっちこっちのWebサイトをつまみ読みし、自分で図式を書いて確認し、 Twitter有識者の方に訂正されながら細かい部分を固めていくような、わりと反面教師的な学習方法を取っているので、 なかなかこういう話をする時にスッと参考資料を出せないのはカコワルイです、精進します。

さて、とりあえず入門には、2013年にワークスアプリケーションズで開催された圏論勉強会の資料が比較的わかりやすいです。

この会にはひと通り参加していましたが、後半徐々に難易度が上がっていくので、 他の資料や書籍と合わせながら勉強していくと良いでしょう。

僕は読んでないのですが、日本語の文献では「圏論の基礎」が有名っぽい。

こっちも読んで無いですが、一番有名なのがCategory Theory (Oxford Logic Guides)

あと、「英語わからんー」とかいいながら、たまにパラパラめくってるのが、「圏論の薄い本」こと「Basic Category Theory for Computer Scientists」とか

あと、意味論の本ですが、「プログラム意味論 (情報数学講座) 」とか。すんませんまだ圏論の部分まで読めてないっす。うっす。

と、と、とりあえず知ってる主な情報元を並べてみましたけど、本人あっちこっちつまみ食いで覚えていった類なので、 どの書籍でどの順に読むと良いという明確なサポートが出来ないのはちょっと恥ずかしいぽよ。

前の記事については、

Cunliffe:
これ読むとかえってわからなくなるんじゃないかなあ。ちゃんと知ってる人に話聞かないとだめだよね、何事も。

と仰ってる方も居まして、いや圏論入門じゃないからそらそうだと思いつつ、「ちゃんと知ってる人」かと言われると自身なくなってくるので、 そういう不満のある方は、Twitterの数学クラスタやHaskellerが積極的に情報交換しているので、我々とjoinして一緒に学んでいってみては如何でしょう。

ただ、基本的に自習方式で、わからない部分をツイートすると、有識者が助け舟を出してくれる「かもしれない」とか、そういう感じだと思って下さい。 僕も含め、多くのコミュニティのメンバーは自分たちの知識を共有する事に抵抗はありませんが、何の準備もなく「教えて♡」って言ってくる人に教えるほど「ちゃんと知っている人達」は暇ではないので、学校とちがって他力本願だと何も情報入ってこないです。

参考までに、多少本格的な内容なので参考にはならないかもしれませんが、 僕がTLで圏論を教わってた時のTogetterまとめがあったので置いときます。 ゆるゆるっとした雰囲気が伝われば幸い。

なーにが「頭がパーンwww」じゃ落ち着け