-
-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve code documentation for MVU/Elm newbies #1067
base: main
Are you sure you want to change the base?
Conversation
08d8400
to
29aa1f5
Compare
@@ -3,12 +3,19 @@ namespace Fabulous | |||
open System | |||
open System.Diagnostics | |||
|
|||
(*TODO Is either of these a program in the Elm sense? If so, where's the view in this one? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Originally we had a single Program
type containing the view
function like in typical Elm. In Fabulous 3, I had to split the Program type in 2 types to enable support of MVU components because the view function is defined later.
let init = ...
let update = ...
let view = ...
let program =
Program.stateful init update // Program<'arg, 'model, 'msg>
|> Program.withView view // Program<'arg, 'model, 'msg, 'view>
let init = ...
let update = ...
let program = Program.stateful init update
let view =
Component(program) {
// the actual view function here
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it would be better to rename the two Program types to avoid the confusion?
Or are these rather abstractions of or pre-cursors to an Elm Program? | ||
AFAIU in the Elm architecture a "program" manages the application's (or component's) state, actions, and view rendering. | ||
Please help me as a MVU/Elm newbie understand these types. *) | ||
//TODO what's the 'arg? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'arg
is a value you can pass to initialize a program. This 'arg
will be received by the init
function.
module App =
let init (isAdmin: bool) =
{ Model.Name = if isAdmin then "Admin" else "User" }
let program =
Program.stateful init update
|> Program.withView view
type MauiProgram =
static member CreateMauiApp() =
let isAdmin = true
MauiApp
.CreateBuilder()
.UseFabulousApp(App.program, isAdmin)
@@ -19,10 +26,11 @@ type Program<'arg, 'model, 'msg> = | |||
ExceptionHandler: exn -> bool | |||
} | |||
|
|||
//TODO how is this different to the above? What's a 'marker? what's the 'arg? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'marker
here is the type of the view. This is used by Fabulous.MauiControls and Fabulous.Avalonia to strongly-type the view being returned.
For example, a ListView
only accepts "cells" (anything inheriting from Cell
type like TextCell
, ImageCell
and so on). You don't want a page in a ListView. In Fabulous, this is represented by the marker interfaces: IFabTextCell
, IFabImageCell
which both inherit from IFabCell
. IFabPage
doesn't inherit from IFabCell
making it impossible to be used inside a ListView.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'marker
in Program type is mainly used by components to signal that this component will return that specific view type and can only be used in a specific way.
We also use this 'marker
type when running an application. UseFabulousApp
will require 'marker
to be IFabApplication
because Maui/Avalonia requires an Application type and not some random Label for example.
src/Fabulous/Program.fs
Outdated
@@ -58,23 +72,44 @@ module Program = | |||
Logger = ProgramDefaults.defaultLogger() | |||
ExceptionHandler = ProgramDefaults.defaultExceptionHandler } | |||
|
|||
//TODO when would I use this one? How does it compare to the other stateful* builders? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stateful
is the simplest type of program where a state is used. You send a message, the state is updated.
statefulWithCmd
adds the concept of side effects on top of the state via Cmd
s
statefulWithCmdMsg
is the same than statefulWithCmd
except it adds a layer of discriminated union to be easier to unit test. You can read more about it on https://zaid-ajaj.github.io/the-elmish-book/#/chapters/scaling/intent (Intent is just a different naming for the same concept)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For 'arg
, please see explanation above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimLariviere I've read about Intent and understand that it's a pattern used in component programs for cleanly separating messages to the component from messages to the parent program. It's used to avoid leaking the decision into the parent program about which component messages to intercept, inspect or forward. The parent only needs to worry about handling the intents and can blindly pass component messages on to the component's update function.
The diff of fabulous-dev/Fabulous.Avalonia#224, which switches the examples between between using statefulWithCmdMsg
and statefulWithCmd
, looks like what I've tried to describe here.
I'm failing to see the connection between Intent and CmdMsg - other than there's command mapping and batching involved. Can you help me connect the dots?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@h0lg True, sorry about the confusion. Intent
is indeed another concept we called ExternalMsg
in Fabulous to enable child -> parent communication. Not the same thing at all than CmdMsg
.
Your understanding of CmdMsg
is correct.
When side-effects are required, init
and update
can return a Cmd<'msg>
along the updated Model
. This Cmd<'msg>
is essentially a list of functions to be executed by Fabulous as side-effects, which might dispatch new messages.
The nice thing with MVU is that it's very easy to unit test since the init
and update
functions are pure and will always return an updated state, so you can check that a specific message with a specific state will return the expected updated state.
But unit testing the returned Cmd<'msg>
is extremely difficult as you would need to execute it yourself. What if that Cmd<'msg>
is making a network call or database update?
It's easier to separate the "intention" (not Intent
, that's where I got confused earlier) of side-effects with the actual implementation as it can just check against the "intention" (the CmdMsg
) in the unit tests.
type Msg = | Clicked
let doNetworkCallCmd () =
Cmd.OfAsync...
let update msg model =
match msg with
| Clicked -> { model with Clicked = true }, doNetworkCallCmd()
let ``When user clicks the button, do the network call``() =
let initialState = { Clicked = false }
let expectedState = { Clicked = true }
let actualState, actualCmd = update Clicked initialState
Assert.Equals(actualState, expectedState)
// TODO: How do you test that "actualCmd" is doing the right thing without actually doing the network call?
type Msg = | Clicked
type CmdMsg = | DoNetworkCall
let doNetworkCallCmd () =
Cmd.OfAsync...
let mapCmdMsgToMsg cmdMsg =
match cmdMsg with
| DoNetworkCall -> doNetworkCallCmd()
let update msg model =
match msg with
| Clicked -> { model with Clicked = true }, [ DoNetworkCall ]
let ``When user clicks the button, do the network call``() =
let initialState = { Clicked = false }
let expectedState = { Clicked = true }
let expectedCmdMsg = DoNetworkCall
let actualState, actualCmdMsg = update Clicked initialState
Assert.Equals(actualState, expectedState)
Assert.Equals(actualCmdMsg, expectedCmdMsg) // Way easier to unit test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimLariviere I think I understand now - and thank you for helping me!
statefulWithCmdMsg
allows you to refactor side-effects out of your update
function, making it pure and easy to unit-test without executing the side-effects.
You do so by defining an extra discriminated union for messages that trigger side-effects (in your example called CmdMsg
) and then return those messages from your update function instead of executing the side effects in place.
An additional function (in your example called mapCmdMsgToMsg
) maps each of those messages back to the side-effect.
If I understand correctly, I suggest that besides updating the code doco (which I'll try), your example would be a good addition to the doco as a topic called Unit testing the update function or something similar.
To
- make this concept more accessible to newbies by getting closer to known ELM/MVU concepts,
- avoid confusion between
CmdMsg
,Cmd<'msg>
andExternalMsg
and - capture the variant of Program this creates,
I suggest the following renaming:
CmdMsg
-> SideEffect
(in examples - of course we can call it whatever we like)
mapCmdMsgToMsg
-> mapSideEffectToMsg
or simply mapSideEffect
statefulWithCmdMsg
-> statefulWithSideEffects
or statefulWithIsolatedSideEffects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimLariviere I have updated the doco on the stateful* builders according to my new found understanding - thanks again for clarifying!
src/Fabulous/Program.fs
Outdated
let statefulWithCmdMsg (init: 'arg -> 'model * 'cmdMsg list) (update: 'msg -> 'model -> 'model * 'cmdMsg list) (mapCmd: 'cmdMsg -> Cmd<'msg>) = | ||
let mapCmds cmdMsgs = cmdMsgs |> List.map mapCmd |> Cmd.batch | ||
define (fun arg -> let m, c = init arg in m, mapCmds c) (fun msg model -> let m, c = update msg model in m, mapCmds c) | ||
|
||
(*TODO Subscriptions will be started or stopped automatically to match. | ||
- I don't understand what that means. What (other?) Subscriptions - or - to match what?*) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we are missing a word here. I guess it should be "... to match the lifecycle of the program".
Subscriptions are now disposed when the program exits, for example when a component is removed from the UI tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the doco changing the wording.
let withSubscription (subscribe: 'model -> Sub<'msg>) (program: Program<'arg, 'model, 'msg>) = { program with Subscribe = subscribe } | ||
|
||
//TODO In what scenario would I want to use this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just like Cmd.map
, mapSubscription
converts child subscriptions to dispatch a parent type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimLariviere Isn't that what Sub.map
is for?
d352c2a
to
665f4ec
Compare
98259ea
to
07f1a49
Compare
c151432
to
1118283
Compare
to prevent misuse
…eturned from the handler means
I'm currently wrapping my head around Elm, MVU and Fabulous, had a look at the changes in pre-release 2.5 #1065 6de97c0 and tried to update the code documentation in a helpful way by connecting types to Elm or MVU concepts. This is an attempt to make Fabulous a bit more approachable for newbies like me who may or may not have done their homework learning about MVU and Elm.
Please review my changes carefully to make sure I'm not confusing things and correct me if I did!
You'll find some questions concerning opaque concepts like
CmdMsg
vs.Cmd<'msg>
as well, which I mentioned in #927 .