Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design thoughts #2

Open
carlosmaniero opened this issue Nov 30, 2017 · 0 comments
Open

Design thoughts #2

carlosmaniero opened this issue Nov 30, 2017 · 0 comments

Comments

@carlosmaniero
Copy link
Owner

carlosmaniero commented Nov 30, 2017

Haskell Update Ecosystem (aka Hue) is a library to work with async and parallel operation in Haskell where IO operarions should be executed in parallel and their result concurrently. Providing a pure and side-effect free ecosystem.

To exemplify the Hue usage I'll describe a simple chat service where people can receive and send message to each other. To simplify this example, messages are not stored.

Like Elm, Redux and other libraries/frameworks that has an update system, Hue has an application state and updater.

Updater

The updater receive a message and returns a new state. In our context, we have a couple of messages. The first is sent when an user is waiting for a new message and the second is to clean the list of "waiters" when a message is sent for them.

data AppMsg
    = CleanWaiterList
    | AddWaiter HueContext

PS: Don't worry! The HueContext be explained soon. In this context, just assume that it's the user that is waiting for a new message.

data AppState = State
    { waiters :: [HueContext] }

updater :: AppMsg -> AppState -> AppState 
updater msg state = 
    case msg of 
      CleanWaiterList -> 
        state { waiters = [] } 
      AddWaiter user 
        state { waiters = waitingList } 
            where waitingList = [user] ++ (waiters state)

So as described above the updater receive a message, the current state and return a new state.

The actor

As the updater an actor receive a message (in this case an action). It can perform IO operation and "talk with" the "external world". To make it simple to understand let's to create an endpoint using the scotty library where users will waiting for new messages. We'll use the long polling mechanism, so the endpoint will block the request until a message is sent.

actor :: AppAction -> HueContext -> AppState -> HueIteration AppState AppResult 
actor action context state = 
    case action of 
      GetMessage -> 
          hueStateChange $ AddWaiter context

So, when the actor is called with the GetMessage, it calls a state change adding the actual "user" (context) in the list of waiters. The stateChange function returns a HueIteration that is an internal Hue structure that defines what kind of operation should be executed internally.

application :: HueApplication AppMsg AppAction AppState AppResult
application = hueCreateApplication
    { appUpdater = update
    , appActor = actor
    , initialState = AppState { waiters = [] } }

main = scotty 3000 $
    get "/message/" $ (hueDispatch application GetMessage) >>= serializeResult

The hueDispatch will block the execution until the application responds with a result.

The execution context

When we call the hueDispatch a context is created. This is the way that the actor has to comunicate with its action dispatcher. Let's a simple example. When we have a new message we can send it using the hueRespond passing the given context method:

hueRespond context $ GetMessageResult message

The hueRespond responds to dispatcher the action result.

Perform IO operation

actor :: AppAction -> HueContext -> AppState -> HueIteration AppState AppResult 
actor action context state = 
    case action of 
      GetMessage -> 
          hueStateChange $ AddWaiter context 
      NewMessage userAuthToken messageText -> 
          huePerform context
              (fetchUserByAuthToken userAuthToken)
              (SendNewMessage messageText)

Let's see the huePerform type:

huePerform :: HueContext -> IO ioResult -> (Either SomeException ioResult -> msg) -> HueItaration

So, the result of the IO operation is "lifted" by the Either Monad. When the IO operation is completed with success (Right) or by an error (Left), Hue generates the Either result and uses it to create a message that will be used as actor call.

Step chain

In this section we will go through the most important part of our chat: The message delivery. And we will see how to implement step chains.

Follow the full implementation of the our actor:

actor :: AppAction -> HueContext -> AppState -> HueIteration AppState AppResult 
actor action context state = 
    case action of 
      GetMessage -> 
          hueStateChange $ AddWaiter context 
      NewMessage userAuthToken messageText -> 
          huePerform context (fetchUserByAuthToken userAuthToken) (SendNewMessage messageText) 
      SendNewMessage messageText userResponse -> 
          case userResponse of -> 
              Right theUser -> 
                  hueRespond context MessageSentResult |> respondMessageForWaiters message 
                        where message = Message 
                            { user = theUser 
                            , message = messageText } 
              Left -> 
                  hueRespond context UserNotFoundResult

Look up the |> function. It is like a javascript Promise.then, in other words, it execute the steps from left to right. The main difference between the semicolon is that the step should be a function that receives a updated copy of the state.

HueStep :: state -> HueIteration state
(|>) :: HueIteration state -> HueStep state -> HueIteration state

The |> unifies an iteration and a step and return a new iteration that will be executed in the hue ecosystem.

respondMessageForWaiter :: Message -> HueContext -> HueIteration AppState AppResult 
respondMessageForWaiter message theWaiter = 
    hueRespond theWaiter $ GetMessageResult message 
 
 
generateRespondMessageList :: Message -> [HueContext] -> HueIteration AppState AppResult 
generateRespondMessageList message waiters = 
    map (respondMessageForWaiter message) waiters 
 
 
respondMessageForWaiters :: Message -> AppState -> HueIteration AppState AppResult 
respondMessageForWaiters message state = 
    foldl (>>>) (hueStateChange CleanWaiterList) (generateRespondMessageList message waiters) 
        where waiterList = waiters state 

If the next step does not need the updated version of the state, you should use the >>> operator. This is pretty close of |> the difference is that the >>> unifies two HueIterations.

(>>>) :: HueIteration state -> HueIteration state -> HueIteration state

You can see the complete example here.

@carlosmaniero carlosmaniero changed the title Design Thoughts Design thoughts Nov 30, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant