試み

https://github.com/nushio3/practice/tree/master/duck

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TypeFamilies #-}
module Data.Object where

import qualified Data.Map as Map
import           Data.Dynamic
import           Control.Lens

-- これが特異メソッドを持てるオブジェクトです。
newtype Object = Object (Map.Map TypeRep Dynamic)
  deriving (Show, Typeable)

-- レコード名(キー)としては文字列とかじゃなく型をつかいます。これは後述するように、
-- 名前衝突を回避するためです。このレコード(キー)に対応する値を型族として持たせます。
class Typeable a => KeyType a where
  type ValType a :: *

-- レコードはObjectから値型へのレンズとします。値をMaybeでくるんでおくことで、
-- 値が未定義の場合も扱えるようにしましょう。
type Record kt = Lens Object Object (Maybe (ValType kt)) (Maybe (ValType kt))

-- 空のオブジェクトです
empty :: Object
empty = Object $ Map.empty

-- レコードを作るヘルパ関数です。
mkRecord :: forall kt. (KeyType kt, Typeable (ValType kt)) => kt -> Record kt
mkRecord k1 = lens gettr settr
  where
    gettr :: Object -> Maybe (ValType kt)
    gettr (Object map0) = Map.lookup k map0 >>= fromDynamic
    settr :: Object -> (Maybe (ValType kt)) -> Object
    settr (Object map0) Nothing  = Object $ Map.delete k map0
    settr (Object map0) (Just x) = Object $ Map.insert k (toDyn x) map0
    k :: TypeRep
    k = typeOf k1

次に、飛行物体(Flying Objects)に関するレコードをいくつか定義します。ちょっと繰り返しが多いですが、試験版なので我慢してね・・・。

module Data.Object.Flying (speed, sound) where

import Data.Dynamic
import Data.Object


data Unidentified = Unidentified deriving Typeable
instance KeyType Unidentified where type ValType Unidentified = Bool
unidentified :: Record Unidentified
unidentified = mkRecord Unidentified

data Speed = Speed deriving Typeable
instance KeyType Speed where type ValType Speed = Double
speed :: Record Speed
speed = mkRecord Speed

data Sound = Sound deriving Typeable
instance KeyType Sound where type ValType Sound = String
sound :: Record Sound
sound = mkRecord Sound


さて、特異メソッドとDuck Typingの例です。

import           Control.Lens
import           Control.Monad
import           Data.List (isInfixOf)
import           Data.Object
import           Data.Object.Flying
import qualified Data.Object.Wav as Wav
import           Data.Maybe
import qualified Data.Vector as V

x1,x2,x3,x4,x5, santa :: Object
x1 = empty -- 空の物体

x2 = x1 & speed .~ Just 120 -- 飛行速度を追加

x3 = x2 & sound .~ Just "quack! quack. quack? quack..." -- 鳴き声追加

x4 = x3 & over speed (fmap (*2)) -- あひるを加速!

-- 音声データをWavフォーマットで追加。同じsoundという名前のレコードが複数ありますが、きちんと区別されます。
x5 = x2 & Wav.sound .~ Just (V.generate 44100 (\i -> floor (sin(2*pi*800 * fromIntegral i/44100))))

-- NOARDレーダーに反応あり!
santa = empty
 & speed .~ Just 64000
 & sound .~ Just "Merry Xmas! Hohohoho!"

main = do
  print x1
  print $ x1 ^. speed
  print x2
  print $ x2 ^. speed
  print $ x3 ^. sound
  let speeders :: [Object]
      speeders = do                       -- リストモナド
        x <- [x1, x2, x3, x4, x5, santa]  -- 飛行物体の一覧
        sp <- maybeToList $ x ^. speed    -- speedレコードを取り出す
        snd <- maybeToList $ x ^. sound   -- 音声データを取り出す
        -- レコードを持たないものはListモナドの機能により、
        -- 実行時エラーを出すことなく除外されます。
        guard $ sp < 200                  -- 速度が200以下
        guard $ "quack" `isInfixOf` snd   -- 鳴き声はガアガア
        return x
  print speeders

速度200以下で飛び、ガアガアと鳴く物体がいたら、それはあひるのはずです。あなたは無事あひるを見つけられましたか?