Может ли 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
Монада может быть полезна.