Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to structure app in purescript

I've decided to try functional programming and Purescript. After reading "Learn you a Haskell for great good" and "PureScript by Example" and playing with code a little I think that I can say that I understand the basics, but one thing bothers me a lot - code looks very coupled. It's usual for me to change libraries very often and in OOP I can use onion architecture to decouple my own code from the library specific one, but I have no idea how to do this in Purescript.

I've tried to find how people do this in Haskell, but all I could find were answers like "No one has ever made complex apps in Haskell, so no one knows how to do it" or "You have input and you have output, everything in between are just pure functions". But at this moment I have a toy app that uses virtal dom, signals, web storage, router libs and each of them have their own effects and data structures, so it doesn't sound like one input and one output.

So my question is how should I structure my code or what technics should I use so that I could change my libs without rewriting half of my app?

Update:

Suggestion to use several layers and keep effects in the main module is quite common too and I understand why I should do so.
Here is a simple example that hopefully will illustrate the problem i'm talking about:

btnHandler :: forall ev eff. (MouseEvent ev) => ev -> Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace | eff) Unit
btnHandler e = do
  btn <- getTarget e
  Just btnId <- getAttribute "id" btn
  Right clicks <- (getItem localStorage btnId) >>= readNumber
  let newClicks = clicks + 1
  trace $ "Button #" ++ btnId ++ " has been clicked " ++ (show newClicks) ++ " times"
  setText (show newClicks) btn
  setItem localStorage btnId $ show newClicks
  -- ... maybe some other actions
  return unit

-- ... other handlers for different controllers

btnController :: forall e. Node -> _ -> Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace | e) Unit
btnController mainEl _ = do
  delegateEventListener mainEl "click" "#btn1" btnHandler
  delegateEventListener mainEl "click" "#btn2" btnHandler
  delegateEventListener mainEl "click" "#btn3" btnHandler
  -- ... render buttons
  return unit

-- ... other controllers

main :: forall e. Eff (dom :: DOM, webStorage :: WebStorage, trace :: Trace, router :: Router | e) Unit
main = do
  Just mainEl <- body >>= querySelector "#wrapper"
  handleRoute "/" $ btnController mainEl
  -- ... other routes each with it's own controller
  return unit

Here we have simple counter app with routing, web storage, dom manipulations and console logging. As you can see there is no single input and single output. We can get inputs from router or event listeners and use console or dom as an output, so it becomes a little more complicated.

Having all this effectful code in main module feels wrong for me for two reasons:

  1. If I will keep adding routes and controllers this module will quickly turn into a thousand line mess.
  2. Keeping routing, dom manipulations and data storing in the same module violates single responsibility principle (and I assume that it is important in FP too)

We can split this module into several ones, for example one module per controller and create some kind of effectful layer. But then when I have ten controller modules and I want to change my dom specific lib I should edit them all.

Both of this approaches are far from ideal, so the question is wich one I should choose? Or maybe there is some other way to go?

like image 433
starper Avatar asked May 19 '15 08:05

starper


1 Answers

There's no reason you can't have a middle layer for abstracting over dependencies. Let's say you want to use a router for your application. You can define a "router abstraction" library that would look like the following:

module App.Router where

import SomeRouterLib

-- Type synonym to make it easy to change later
type Route = SomeLibraryRouteType

-- Just an alias to the Router library
makeRoute :: String -> Route -> Route
makeRoute = libMakeRoute

And then the new shiny comes out, and you want to switch your routing library. You'll need to make a new module that conforms to the same API, but has the same functions -- an adapter, if you will.

module App.RouterAlt where

import AnotherRouterLib

type Route = SomeOtherLibraryType

makeRoute :: String -> Route -> Route
makeRoute = otherLibMakeRoute

In your main app, you can now swap the imports, and everything should work alright. There will likely be more massaging that needs to happen to get the types and functions working as you'd expect them, but that's the general idea.

Your example code is very imperative in nature. It's not idiomatic functional code, and I think you're correct in noting that it's not sustainable. More functional idioms include purescript-halogen and purescript-thermite.

Consider the UI as a pure function of current application state. In other words, given the current value of things, what does my app look like? Also, consider that the current state of the application can be derived from applying a series of pure functions to some initial state.

What is your application state?

data AppState = AppState { buttons :: [Button] }
data Button = Button { numClicks :: Integer }

What kind of events are you looking at?

data Event = ButtonClick { buttonId :: Integer }

How do we handle that Event?

handleEvent :: AppState -> Event -> AppState
handleEvent state (ButtonClick id) =
    let newButtons = incrementButton id (buttons state)
    in  AppState { buttons = newButtons }

incrementButton :: Integer -> [Button] -> [Button]
incrementButton _ []      = []
incrementButton 0 (b:bs)  = Button (1 + numClicks b) : bs
incrementButton i (b:bs)  = b : incrementButton (i - 1) buttons

How do you render the application, based on the current state?

render :: AppState -> Html
render state =
    let currentButtons = buttons state
        btnList = map renderButton currentButtons
        renderButton btn = "<li><button>" ++ show (numClicks btn) ++ "</button></li>" 
    in  "<div><ul>" ++ btnList ++ "</ul></div>"
like image 115
ephrion Avatar answered Sep 21 '22 20:09

ephrion