'How can we build a modular system in Haskell? [closed]

Below is a concept that I have applied in OOP, and I would like to apply in FP (specifically Haskell). Everything that I have read says it is possible, but I can find no good examples. I understand some of the logic is OOP based, but I can convey it easier that way.

Design Constraints:

  1. models contain three types of behavior: receive data, send data, and contact some service outside of the program. A model can receive and/or send data to other models, but there is no direct connection.
  2. The Engine is used to manage two of the models behaviors: receive data, and send data. All data transfer between models is passed through the Engine. The Engine is not passed to the models; the Engine inspects each model to find the relevant send/receive functions, which it processes and generates links between models. Data generated by a model is available to any model that wants to listen to it.

In OOP, some of this is very simple; each model is an object containing behavior and the required data to preform the calculations. The Engine is another object, and can inspect the attributes of each function in a model to determine it's purpose.

For example, I may have a system that contains a websocket datafeed, a GUI, and a email client; each is it's own model. When the websocket receives data it is processed and then passed to the Engine to be directed to subscribers. The GUI receives the data, and displays it. The email client receives the data, and will send an email once per day with an aggregation of that day's data. Eventually I may want to add a report generator that will receive data, and send out the results of a calculation to the Engine. I could then modify my email client to subscribe to this data, and include it in the daily email. This should require no code change in the Engine or any models other than the email client to add the additional functionality.



Solution 1:[1]

Here's a simple pub-sub engine:

import Control.Monad
import Control.Concurrent
import Control.Concurrent.MVar
import Control.Concurrent.Chan
import Data.Foldable

data Engine a = Engine (Chan a) (MVar [Chan a]) ThreadId

mk :: IO (Engine a)
mk = do
    c <- newChan
    m <- newMVar []
    t <- forkIO . forever $ do
        a <- readChan c
        subs <- readMVar m
        traverse_ (flip writeChan a) subs
    pure (Engine c m t)

pub :: Engine a -> a -> IO ()
pub (Engine c m t) a = writeChan c a

sub :: Engine a -> IO (Chan a)
sub (Engine c m t) = do
    chan <- newChan
    modifyMVar_ m (pure . (chan:))
    pure chan

kill :: Engine a -> IO ()
kill (Engine c m t) = killThread t

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Daniel Wagner