Closed bradphelan closed 5 years ago
I personally like the centralised approach of having an update function. I see several benefits of doing it that way:
You are also free to roll you own State & Update management. I actually do this for the drawing app you see in the screenshots. I basically have a lot of different Messages that trigger complex operations. Messages and Operations are also in a different Project and not UI dependent.
I could basically write a web version and use $(FSharp MVU Framework) without touching the Logic Project.
Using function composition doesn't mean that the functions can't be isolated from the view. Doing it inline was just an example of the flexibility. If you wish to have a centralized system you still could but I'm not sure forcing it is necessary.
Hmm, that's right and as your example shows you can totally do that.
I think I personally just like having my Messages 😄.
I don't think that DUs exclusively equals messages is the right idea here. Functions are also messages. In the case of FuncUI it seems that DU's are trying to fake function overloading.
type Msg =
| Increment
| Decrement
| Specific of int
| RemoveNumber of int
let update (msg: Msg) (state: CounterState) : CounterState =
match msg with
| Increment -> { state with count = state.count + 1 }
| Decrement -> { state with count = state.count - 1 }
| Specific number -> { state with count = number }
| RemoveNumber number -> { state with numbers = List.except [number] state.numbers }
DU's are good where the pattern will be used in many different places and you want to garuntee exhaustive matching. Here this will never be the case. The message is dispatched in one place and picked up in once place. Why is the above better than
let increment (state: CounterState) = { state with count = state.count + 1 }
let decrement (state: CounterState) = { state with count = state.count - 1 }
let set (state: CounterState) = { state with count = number }
let remove (state: CounterState ) = { state with numbers = List.except [number] state.numbers }
That is four lines instead of 11 with no loss of seperation of view and model.
If you are doing it that way you can have your functions in one place, but you still need a way to tie your function to the desired view event. You could reference them directly but the you have a tight coupling between the view & logic.
Also all functions you would want to dispatch would need to have the same signature or get the arguments baked in using partial application.
This would not work because 'number' is not passed.
let set (state: CounterState) = { state with count = number }
let remove (state: CounterState ) = { state with numbers = List.except [number] state.numbers
so the functions would look more like this because all functions you would dispatch need to have the same signature. (or have to be dynamically invoked - but that's slow and unsafe)
type UpdateFunc = 'state * obj -> 'state
Maybe I am missing something here ? (I typed this on the train)
I would say both methods have some advantages and disadvantages. In the end it boils down to preference, and what's commonly used.
Maybe you also find some discussions about this in the elm / fable community.
I did a small example of uisng FuncUI without messages. The comment I made above was done without actually trying to compile it. I've a fork with a change to the Counter example. Maybe it's interesting
https://github.com/bradphelan/Avalonia.FuncUI/commit/f6de6438a3c7ea3b99d357cfe846c1d62dd5dd38
My update functions are defined
let increment (state: CounterState) : CounterState = { state with count = state.count + 1 }
let decrement (state: CounterState) : CounterState = { state with count = state.count + 1 }
let set_count (state:CounterState) count : CounterState = { state with count = count;}
and the view is defined
let view (state: CounterState) (dispatch): View =
Views.dockpanel [
Attrs.children [
Views.button [
Attrs.dockPanel_dock Dock.Bottom
Attrs.onClick (Binder.command increment dispatch)
Attrs.content "-"
]
Views.button [
Attrs.dockPanel_dock Dock.Bottom
Attrs.onClick (Binder.command decrement dispatch)
Attrs.content "+"
]
Views.textBox[
Attrs.dockPanel_dock Dock.Top
Attrs.fontSize 48.0
Attrs.verticalAlignment VerticalAlignment.Center
Attrs.horizontalAlignment HorizontalAlignment.Stretch
Attrs.text (string state.count)
Attrs.onKeyUp (Binder.oneWay 0 set_count dispatch )
]
]
]
I created a Binder object for managing commands and bindings. Just an experiment.
Here's your counter example rewritten just using functions instead of DU's. It removes the need for a centralised msg handler.
Is there an advantage to using the DU's here that just plain function composition can't do? For simple functions it is even possible to inline the operations.
Instead of
you could write