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:
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?
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>"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With