Может ли Scala Cake Pattern быть реализован в Haskell?

Используя ряд новых языковых функций в Scala, можно реализовать систему компонуемых компонентов и создавать компоненты, используя так называемый Cake Pattern, описанный Мартином Одерским в статье " Абстракции масштабируемых компонентов", а также в недавнем выступлении.

Некоторые функции Scala, используемые в Cake Pattern, имеют соответствующие функции Haskell. Например, импликации Scala соответствуют классам типов Haskell, а члены абстрактных типов Scala, похоже, соответствуют ассоциированным типам Haskell. Это заставляет меня задуматься, можно ли реализовать Cake Pattern в Haskell и как он будет выглядеть.

Можно ли реализовать Cake Pattern в Haskell? Каким функциям Haskell соответствуют функции Scala в такой реализации? Если Cake Pattern не может быть реализован в Haskell, какие языковые функции отсутствуют, чтобы сделать это возможным?

3 ответа

Решение

Олег дал очень подробный ответ здесь: http://okmij.org/ftp/Haskell/ScalaCake.hs

Принимая это в качестве примера, мне кажется, что следующий код довольно похож:

{-# LANGUAGE ExistentialQuantification #-}

module Tweeter.Client where

import Data.Time
import Text.Printf
import Control.Applicative
import Control.Monad

type User = String

type Message = String

newtype Profile = Profile User

instance Show Profile where
  show (Profile user) = '@' : user

data Tweet = Tweet Profile Message ZonedTime

instance Show Tweet where
  show (Tweet profile message time) =
    printf "(%s) %s: %s" (show time) (show profile) message

class Tweeter t where
  tweet :: t -> Message -> IO ()

class UI t where
  showOnUI :: t -> Tweet -> IO ()
  sendWithUI :: Tweeter t => t -> Message -> IO ()
  sendWithUI = tweet

data UIComponent = forall t. UI t => UIComponent t

class Cache t where
  saveToCache :: t -> Tweet -> IO ()
  localHistory :: t -> IO [Tweet]

data CacheComponent = forall t. Cache t => CacheComponent t

class Service t where
  sendToRemote :: t -> Tweet -> IO Bool
  remoteHistory :: t -> IO [Tweet]

data ServiceComponent = forall t. Service t => ServiceComponent t

data Client = Client UIComponent CacheComponent ServiceComponent Profile

client :: (UI t, Cache t, Service t) => t -> User -> Client
client self user = Client
  (UIComponent self)
  (CacheComponent self)
  (ServiceComponent self)
  (Profile user)

instance Tweeter Client where
  tweet (Client (UIComponent ui)
                (CacheComponent cache)
                (ServiceComponent service)
                profile)
        message = do
    twt <- Tweet profile message <$> getZonedTime
    ok <- sendToRemote service twt
    when ok $ do
      saveToCache cache twt
      showOnUI ui twt

И для фиктивной реализации:

module Tweeter.Client.Console where

import Data.IORef
import Control.Applicative

import Tweeter.Client

data Console = Console (IORef [Tweet]) Client

console :: User -> IO Console
console user = self <$> newIORef [] where
  -- Tying the knot here, i.e. DI of `Console' into `Client' logic is here.
  self ref = Console ref $ client (self ref) user

instance UI Console where
  showOnUI _ = print

-- Boilerplate instance:
instance Tweeter Console where
  tweet (Console _ supertype) = tweet supertype

instance Cache Console where
  saveToCache (Console tweets _) twt = modifyIORef tweets (twt:)
  localHistory (Console tweets _) = readIORef tweets

instance Service Console where
  sendToRemote _ _ = putStrLn "Sending tweet to Twitter HQ" >> return True
  remoteHistory _ = return []

test :: IO ()
test = do
  x <- console "me"
  mapM_ (sendWithUI x) ["first", "second", "third"]
  putStrLn "Chat history:"
  mapM_ print =<< localHistory x

-- > test
-- Sending tweet to Twitter HQ
-- (2012-10-21 15:24:13.428287 UTC) @me: first
-- Sending tweet to Twitter HQ
-- (2012-10-21 15:24:13.428981 UTC) @me: second
-- Sending tweet to Twitter HQ
-- (2012-10-21 15:24:13.429596 UTC) @me: third
-- Chat history:
-- (2012-10-21 15:24:13.429596 UTC) @me: third
-- (2012-10-21 15:24:13.428981 UTC) @me: second
-- (2012-10-21 15:24:13.428287 UTC) @me: first

Однако это самый простой случай. В Scala у вас есть:

  • Классы с абстрактным значением и членами типа (напоминает функторы ML и зависимые записи, как в Agda).

  • Пути-зависимые типы.

  • Автоматическая линеаризация классов.

  • это и супер.

  • Selftypes.

  • Подтипы.

  • Implicits.

  • ...

Это просто отлично от того, что есть в Хаскеле.

Есть несколько решений. "Очевидным" является наличие нескольких экземпляров для заданных классов типов (скажем, Loader, Player, GUI для игры), которые можно свободно комбинировать, но, на мой взгляд, такой дизайн лучше подходит для OO-языков.

Если вы продумаете рамку и поймете, что фундаментальными строительными блоками в Haskell являются функции (черт!), Вы придете к чему-то вроде этого:

data Game = Game
  { load    :: String -> IO [Level]
  , player1 :: Level -> IO Level
  , player2 :: Level -> IO Level
  , display :: Level -> IO ()  
  }  

play :: Game -> IO ()

С такой конструкцией очень легко заменить, например, игроков-людей ботами. Если это становится слишком сложным, используя Reader Монада может быть полезна.

Другие вопросы по тегам