Some people prefer to think of foldp as some sort of time machine. To "create a past-dependent signal" or "step time forward" or "apply over a list of events". To me foldp is just simply memory. Every time you need to go from one thing to another thing, you need to change the thing is which getting remembered.
var foo = true;
and in elm:
foo = True
var foo = true; foo = false;
and done, easy. But what about elm? Since it's a functional language which doesn't know assignments, you can not do this
foo = True foo = False
False at the same time.
Let's take a look at foldp function signature first
foldp : (a -> state -> state) -> state -> Signal a -> Signal state
or rather this might make it more understandable
foldp : updateFunction -> defaultValue -> triggerSignal -> outputSignal
Let's start with the outputSignal first. This is the value you can use as the result of your memory. In other words if you needed foo somewhere (which changed from true to false) you can use it's outputSignal to get that changing value.
Then the defaultValue, this is the initialization value for when a change has not occured yet. In our case
Then the triggerSignal. This one is used to determine when to change the value to another one. It's actually rare two write sequences like
var foo = true; foo = false;
Why would you want to first change it to
true and then to
false right away? Most of the time you only want to it change on the occurrence of an event. Like a user mouse click. How to do mouse click events and the likes are outside the scope of this post. However let's imagine the user had two buttons one with
true and one with
false. In that case it would be useful to send this value along with your triggerSignal so it can be used for foldp.
And lastely the updateFunction with it's signature
a -> state -> state
It's a function that takes the value of your triggerSignal as first argument. foldp magically supplies the old value as second argument. In the case the foldp was never triggered before this would be the defaultValue. The function is supposed to return the new value which you want. By using a pure function it becomes more obvious that we are not just setting and getting a variable. Rather we describe how to get from one immutable state to another one.
Often you don't care what the previous value was and you just want to set a new one, you can do this:
(\a _ -> a)
or you want to toggle between true and false. Then the value is ONLY dependent on the previous one and not override it to a specific value.
update _ s = case s of True -> False False -> True
More complex function determine the new state both on the previous state and the new value. It's conceptually the same as a Finite State Machine.
Startapp uses foldp to not just change the state of a boolean (like in the examples above), but often a big record with a lot of fields in it (the model). This application model also frequently serves as memory for used modules.
When you have a part of your app, like a widget, you can tie it in with the state (memory) of your overall application. The Counter module can be regarded as a small widget which gets reused in CounterPair. It's tied in at various places, in the update function, the model and so on.
However this is not always necessary or wanted. Sometimes you don't need to communicate with the rest of your application. Maybe you have a couple of widgets that can function on their own. They can have memory, do http requests or do native ports on their own. If the requirement of sharing memory between widgets is not there, you shouldn't just tie stuff together.
Let's look at an example of to create a more localized use of memory without having to create an entire module for it. We start off with a little piece of jQuery that uses a button click to change it's size.
This elm code indeed does use foldp to memorize the size of your button. But it's still very similar to startapp and does not really feel "local". In fact there is only one memory here so there is no separation between "application memory" and "button memory". Eventually you should end up with at least two foldp functions to seperate those memories. First let's move a bunch of stuff in function scope of the view.
view = let box = Signal.mailbox "128px" subView size = button [ onClick box.address size , style [("font-size", size)] ] [text ("Size me")] update size _ = case size of "12px" -> "20px" "20px" -> "12px" memory = Signal.foldp update "12px" box.signal in Signal.map subView memory main = view
It now becomes more obvious that our original view can serve as a subView. No magic here .. all which was done was move functions and rename view to subView.
main = view is of course redundant but it's put there to make it explicit that you can now introduce more signaling on your main function (just like in startapp).
In case you want to repeat this pattern of localized memory more often, let's introduce as final step a function that takes care of the boilerplate code so you only have to write the code that is specific to your app.
memReplace default update view = let box = Signal.mailbox default state = Signal.foldp (\x _ -> update x) default box.signal in Signal.map (view box.address) state view = let default = "12px" update size = case size of "12px" -> "20px" "20px" -> "12px" subView addr size = button [ onClick addr size , style [("font-size", size)] ] [text ("Size me")] in memReplace default update subView main = view
Notice that the
memReplace does not care about the previous state of your memory. That's why it is named
memReplace for just replacing memory. The last piece of code is exactly the same as the code before that, only everything is moved around. If you find this difficult to understand try it yourself, it will be a valuable skill in working with elm in general. Also if you want different kind of local memory functions, with different features (like doing something with the previous state), try to construct them yourself. It's not that hard and once you understand this stuff you understand a very important part of the language.