问题
Using a number of newer language features in Scala it's possible to implement a composable component system and create components using the so called Cake Pattern, described by Martin Odersky in the paper Scalable Component Abstractions and also in a recent talk.
Several of the Scala features used in the Cake Pattern have corresponding Haskell features. For example, Scala implicits correspond to Haskell type classes and Scala's abstract type members seem to correspond to Haskell's associated types. This makes me wonder if the Cake Pattern could be implemented in Haskell and what it would look like.
Can the Cake Pattern be implemented in Haskell? Which Haskell features do the Scala features correspond to in such an implementation? If the Cake Pattern can't be implemented in Haskell, which language features are missing to make that possible?
回答1:
Oleg provided a very detailed answer here: http://okmij.org/ftp/Haskell/ScalaCake.hs
回答2:
Taking this as an example, it seems to me that the following code is quite similar:
{-# 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
And for dummy implementation:
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
However, this is the simplest case. In Scala you have:
Classes with abstract value and type members (reminds ML functors and dependent records, as in Agda).
Path-dependent types.
Automatic class linearization.
this and super.
Selftypes.
Subtyping.
Implicits.
...
It is just, well, different from what you have in Haskell.
回答3:
There are several solutions. The "obvious" one is to have several instances for given type classes (say Loader
, Player
, GUI
for a game) that can be combined freely, but in my opinion such a design is better suited for OO-languages.
If you think out the box and recognize that the fundamental building blocks in Haskell are functions (D'oh!), you come to something like this:
data Game = Game
{ load :: String -> IO [Level]
, player1 :: Level -> IO Level
, player2 :: Level -> IO Level
, display :: Level -> IO ()
}
play :: Game -> IO ()
With this design it's very easy to replace e.g. human players by bots. If this gets too complex, using the Reader
monad might be helpful.
来源:https://stackoverflow.com/questions/12947176/can-scalas-cake-pattern-be-implemented-in-haskell