Warning: This is just a mind dump. It's not a coherent story and it's mostly for me to read back on my thought. I'm publishing it in the hope that other people can learn from my thought process.
To recap for the ones who aren't aware. Hom is a (hypothetical) library for writing (hopefully) blazingly fast clientside web applications using GHCJS. Its inspired by ReactJS which uses DOM diffing to calculate the optimal way to execute view changes. It basically turns your app into State -> Html
. A great fit for the functional paradigm. Because I hate JS as much as you guys I decided to bring this lovely goodness to haskell. Luite and I made a start with the ghcjs-vdom library which is a thin wrapper around the js dom diffing algorithm. I was really enthousiastic but the project somehow ended up on my shel of "I'll start working on this project again when I feel like it"-projects.
Currently Hom
is in the "showerthought" phase when every once in a while during a shower I come up with ways to improve my library.
This week I had to quickly create a dashboard for a TV at my university. I decided to use Elm. Development ended up being a joy and shipped the project after a day of elm-hacking.
The joy of working with elm-html
was awesome. elm-html
has similar goals to Hom though elm is a lot less powerful language than haskell.
I suggest reading up on Elm architecture tutorial before continuing.
One of the things that annoyed me during the project is that there is a lot of code duplication. Especially a lot of the same pattern matches to delegate actions down to sub-components etc.
Last night I was reading some lens stuff (one of those things I am trying to grock at this moment) and I started reading about prisms. "First class patterns" they're supposed to be. Well that sounds like something I could use right? I see that I keep doing the same kind of pattern matches for different scenarios. A 'pattern' seems to emerge (pun intended). Maybe I can abstract it with prisms? This was the shower thought.
So this morning I started type-hacking and I ended up with some awesome stuff.
I'm gonna assume for now that all elm's APIs are accessible in my hypothetical Hom
library. All the following code is haskell.
We're gonna build an app that combines two apps into one. Namely it includes a Facebook and a Twitter widget.
The facebook and twitter widgets are defined as follow:
module Facebook where
import Control.Lens
import VirtualDom
import Signal
data Model = Model
{ _likes :: Int
, _comments :: [String]
}
data Action = Like
| Comment String
deriving Show
update :: Action -> Model -> Model
update Like = ...
upate (Comment s) = ...
action :: Signal Action
action = -- facebookAPISource + input events etc
view :: Model -> Html
view m = ... code that renders the facebook widget ...
And Twitter:
module Twitter where
import VirtualDom
import Signal
data Model = Model
{ _status :: String
}
data Action = Tweet String
| Favorite
deriving Show
update :: Action -> Model -> Model
update (Tweet s) = ...
update (Favorite) = ...
action :: Signal Action
action = -- twitter api and input events etc.
view :: Model -> Html
view m = -- code that renders the widget --
Now we want to combine these widgets. So our new model is going to be the union of those two widgets:
data Model = Model
{ _facebook :: Facebook.Model
, _twitter :: Twitter.Model
}
And the actions is going to be the union of the two widgets
data Action = FBAction Facebook.Action
| TAction Twitter.Action
| Close -- our own action for our own component
And our update function would be lame pattern matches. Every time we add a new subcomponent...
update :: Action -> Model -> Model
update Close m = ... change the state so the app is closed ...
update (FBAction a) m = m { _facebook = Facebook.update a (_facebook m) }
update (TAction a) m = m { _twitter = Twitter.update a (_twitter m) }
Render the views...
view :: Model -> Html
view m =
div []
[ Facebook.view (m^.facebook)
, Twitter.view (m^.twitter)
]
And run the app ...
app :: Signal Html
app = Signal.foldp update initialState action
I don't know why but I wanted to hack this into something more nice with lenses and prisms.
Now we do:
makePrisms ''Action
makeLenses ''Model
and we can start doing magic!
After reading the Prism documentation and stack overflow ( http://stackoverflow.com/questions/20774475/use-a-parameter-as-pattern-in-haskell) I found a way to easily extend
your action handler.
say we have:
update' :: Action -> Model -> Model
update' Close = .. state to close the app ..
and we want to add the facebook widget to that. We end up doing this:
update :: Action -> Model -> Model
update = update' & outside _FBAction .~ over facebook
. Facebook.update
If we want to add a twitter widget to that we can just add it to the chain:
update = update' & outside _FBAction .~ over facebook
. Facebook.update
& outside _TAction .~ over twitter
. Twitter.update
We can extract this pattern into a utility function:
You should see it as: If we have a prism that given an action might give us a local action apply the local action to the global state by lensing into the global state to a part that is our local state and update that.
( I simplified the types a lot. Apparently this works for any Profunctor. not just (->) . Though I'm not sure what that means. Heck I don't even know what a Profunctor is. This lens library thing sure is complex.)
updateWith :: APrism' action localAction
-> Setting' (->) model localModel
-> (localAction -> localModel -> localModel)
-> (action -> model -> model)
-> (action -> model -> model)
updateWith action lens update =
outside action .~ over lens . update
So our code becomes:
withWidgets :: (Action -> Model -> Model) -> (Action -> Model -> Model)
withWidgets = updateWith _FBAction facebook Facebook.update
. updateWith _TAction twitter Twitter.update
update = withWidgets update'
So now we can easily add as many widgets as we want using function composition! Nice! Because the local states of widgets don't overlap, the order in which we compose these updateWiths
doesn't matter. widgets actions are commutative.
Okay so we got state updating covered. How do we delegate signals from the main component to subcomponents? Lets see how we used to do it in elm.
action_ :: Signal Action
action = FBAction <$> Facebook.action
<|> TAction <$> Twitter.action
Actually this is quite elegant. But I want to do it with prisms because heck why not.
With prisms we end up with the following code:
action :: Signal Action
action = review _FBAction <$> Facebook.action
<|> review _TAction <$> Twitter.action
We can extract a utility function:
mountAction :: Functor f => AReview t a -> f a -> f t
mountAction r = (review r <$>)
action = mountAction _FBAction Facebook.action
<|> mountAction _TAction Twitter.action
This type is super general! I am going to dub it liftReview
. It's simply review lifted into a functor.
Anyhow. Here my type-hacking endevaours stop. It was nice and I learned a lot about lenses. Haskell on the clientside web is awesome. I need to build this hypothetical library. It's gonna be sick.
peace.