Skip to content

Commit 7339a57

Browse files
committed
trying to improve doco for Elm/MVU newbies
1 parent 8ec3928 commit 7339a57

3 files changed

Lines changed: 123 additions & 28 deletions

File tree

src/Fabulous/Cmd.fs

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,35 @@
33
open System.Threading
44
open System.Threading.Tasks
55

6-
/// Dispatch - feed new message into the processing loop
6+
/// <summary>
7+
/// A function that feeds a new Message into the processing loop.
8+
/// <seealso href="https://elmish.github.io/elmish/#dispatch-loop" />
9+
/// </summary>
710
type Dispatch<'msg> = 'msg -> unit
811

9-
/// Subscription - return immediately, but may schedule dispatch of a message at any time
12+
/// <summary>
13+
/// A function that returns immediately, but may schedule dispatch of one or multiple Messages at any time
14+
/// via its access to a Dispatch function.
15+
/// This is an abstraction over raw Messages passed along to the MVU processing loop
16+
/// necessary to deal with real-world scenarios in which Effect functions may take a while to complete.
17+
/// This models an Elm "side-effect" like in
18+
/// <seealso href="https://elmish.github.io/elmish/#tasks-and-side-effects" />
19+
/// or
20+
/// <seealso href="https://elmprogramming.com/side-effects.html" />
21+
/// </summary>
1022
type Effect<'msg> = Dispatch<'msg> -> unit
1123

12-
/// Cmd - container for effects that may produce messages
24+
/// <summary>
25+
/// A list of Effects that may dispatch Messages;
26+
/// the carriers of instructions you issue from the init and update functions.
27+
/// See
28+
/// <seealso href="https://elmish.github.io/elmish/#commands" />
29+
/// </summary>
1330
type Cmd<'msg> = Effect<'msg> list
1431

15-
/// Cmd module for creating and manipulating commands
32+
/// A module for creating and manipulating Commands
33+
/// with a Command being a list of Message-dispatching Effects you issue from the init and update functions
34+
/// and an Effect being a function with access to a Dispatch function receiving a Message.
1635
[<RequireQualifiedAccess>]
1736
module Cmd =
1837
/// Execute the commands using the supplied dispatcher
@@ -24,32 +43,48 @@ module Cmd =
2443
with ex ->
2544
onError ex)
2645

27-
/// None - no commands, also known as `[]`
46+
/// No command; an empty list of Message-dispatching Effects equivalent to `[]`.
47+
/// For when you don't want to issue a Command.
2848
let none: Cmd<'msg> = []
2949

30-
/// When emitting the message, map to another type
50+
/// <summary>
51+
/// Converts a Command of type 'a into a Command of type 'msg.
52+
/// This is useful for emitting Commands of a uniform type,
53+
/// like when receiving child messages in a parent-child composition scenario. See
54+
/// <seealso href="https://elmish.github.io/elmish/docs/parent-child.html" />
55+
/// </summary>
3156
let map (f: 'a -> 'msg) (cmd: Cmd<'a>) : Cmd<'msg> =
3257
cmd |> List.map(fun g -> (fun dispatch -> f >> dispatch) >> g)
3358

34-
/// Aggregate multiple commands
59+
/// <summary>
60+
/// Concatenates the Effects of multiple Commands into one list.
61+
/// Use for emitting multiple Commands at the same time from the init or update function.
62+
/// E.g. in an parent-child composition scenario:
63+
/// <seealso href="https://elmish.github.io/elmish/docs/parent-child.html" />
64+
/// </summary>
3565
let batch (cmds: Cmd<'msg> list) : Cmd<'msg> = List.concat cmds
3666

37-
/// Command to call the effect
67+
/// <summary>
68+
/// Returns a command to call a custom Effect function with access to a Dispatch function.
69+
/// Use for example to dispatch status updates or yield partial results from long-running background tasks.
70+
/// </summary>
3871
let ofEffect (effect: Effect<'msg>) : Cmd<'msg> = [ effect ]
3972

40-
/// Command to issue a specific message
73+
/// Command to issue a specific message.
74+
/// Wraps the message into the Command structure returned from the update and init functions.
4175
let ofMsg (msg: 'msg) : Cmd<'msg> = [ fun dispatch -> dispatch msg ]
4276

43-
/// Command to issue a specific message, only when Option.IsSome = true
77+
/// Command to issue the message from the message option if Option.IsSome
4478
let ofMsgOption (msg: 'msg option) : Cmd<'msg> =
4579
[ fun dispatch ->
4680
match msg with
4781
| None -> ()
4882
| Some msg -> dispatch msg ]
4983

84+
/// Creates Commands from the return values and/or exceptions of simple functions,
85+
/// wrapping the call in a try/with statement. Use this to deal with code that may throw exceptions.
5086
module OfFunc =
51-
/// Command to evaluate a simple function and map the result
52-
/// into success or error (of exception)
87+
/// Creates a Command to evaluate a simple function and map either the return value or exception to a message
5388
let either (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
5489
let bind dispatch =
5590
try
@@ -59,7 +94,7 @@ module Cmd =
5994

6095
[ bind ]
6196

62-
/// Command to evaluate a simple function and map the success to a message
97+
/// Creates a Command to evaluate a simple function and map the return value to a message
6398
/// discarding any possible error
6499
let perform (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
65100
let bind dispatch =
@@ -70,7 +105,8 @@ module Cmd =
70105

71106
[ bind ]
72107

73-
/// Command to evaluate a simple function and map the error (in case of exception)
108+
/// Creates a Command to evaluate a simple function returning unit
109+
/// and map the error (in case of exception) to a message
74110
let attempt (task: 'a -> unit) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
75111
let bind dispatch =
76112
try
@@ -80,6 +116,9 @@ module Cmd =
80116

81117
[ bind ]
82118

119+
/// Internal module for building Commands from the return values or exceptions of Async functions
120+
/// using Async.Catch like an async try/with statement.
121+
/// You'll probably want to use either of the modules OfAsync or OfAsyncImmediate instead of this.
83122
module OfAsyncWith =
84123
/// Command that will evaluate an async block and map the result
85124
/// into success or error (of exception)
@@ -98,6 +137,7 @@ module Cmd =
98137
[ bind >> start ]
99138

100139
/// Command that will evaluate an async block and map the success
140+
/// discarding any possible error
101141
let perform (start: Async<unit> -> unit) (task: 'a -> Async<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
102142
let bind dispatch =
103143
async {
@@ -136,6 +176,10 @@ module Cmd =
136176

137177
[ bind >> start ]
138178

179+
/// For building Commands from Async functions queued to be run in the background, started on a thread pool thread using Async.Start.
180+
/// Suitable for long-running or CPU-bound computations where you want to free up the UI thread to remain responsive to do other work.
181+
/// See https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/async#asyncstart
182+
/// and https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/async-padl-revised-v2.pdf page 5.
139183
module OfAsync =
140184
/// Command that will evaluate an async block and map the result
141185
/// into success or error (of exception)
@@ -150,12 +194,18 @@ module Cmd =
150194
let inline attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
151195
OfAsyncWith.attempt Async.Start task arg ofError
152196

197+
/// Command that will evaluate an async block and map the success 'msg
153198
let inline msg (task: Async<'msg>) =
154199
OfAsyncWith.perform Async.Start (fun () -> task) () id
155200

201+
/// Command that will evaluate an async block and map the success 'msg Option.Value if Option.IsSome
156202
let inline msgOption (task: Async<'msg option>) =
157203
OfAsyncWith.performOption Async.Start (fun () -> task) () id
158204

205+
/// For building Commands from Async functions started immediately on the current operating system thread
206+
/// using Async.StartImmediate. This is helpful if you need to update something on the calling thread during the computation.
207+
/// For example if an asynchronous computation must update a UI (such as updating a progress bar).
208+
/// See https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/async#asyncstartimmediate
159209
module OfAsyncImmediate =
160210
/// Command that will evaluate an async block and map the result
161211
/// into success or error (of exception)
@@ -170,21 +220,27 @@ module Cmd =
170220
let inline attempt (task: 'a -> Async<_>) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
171221
OfAsyncWith.attempt Async.StartImmediate task arg ofError
172222

223+
/// <summary>
224+
/// For building Commands from executing ("hot") .NET Tasks using Async.AwaitTask.
225+
/// <seealso href="https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/async#core-concepts" />
226+
/// </summary>
173227
module OfTask =
174-
/// Command to call a task and map the results
228+
/// Command to map both the success and possible error result of a Task
175229
let inline either (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
176230
OfAsync.either (task >> Async.AwaitTask) arg ofSuccess ofError
177231

178-
/// Command to call a task and map the success
232+
/// Command to map the success result of a Task
179233
let inline perform (task: 'a -> Task<_>) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
180234
OfAsync.perform (task >> Async.AwaitTask) arg ofSuccess
181235

182-
/// Command to call a task and map the error
236+
/// Command to map the error of a Task without a success result
183237
let inline attempt (task: 'a -> #Task) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
184238
OfAsync.attempt (task >> Async.AwaitTask) arg ofError
185239

240+
/// Command to map the success 'msg returned from a Task
186241
let inline msg (task: Task<'msg>) = OfAsync.msg(task |> Async.AwaitTask)
187242

243+
/// Command to map the success 'msg Option.Value returned from a Task if Option.IsSome
188244
let inline msgOption (task: Task<'msg option>) =
189245
OfAsync.msgOption(task |> Async.AwaitTask)
190246

src/Fabulous/Program.fs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ open System.Diagnostics
66
/// Configuration of the Fabulous application
77
type Program<'arg, 'model, 'msg> =
88
{
9-
/// Give the initial state for the application
9+
/// Returns the initial model/state of the application and an intial Command
1010
Init: 'arg -> 'model * Cmd<'msg>
11-
/// Update the application state based on a message
11+
/// Returns a new model/application state
12+
/// updated from a message applied to the current model
13+
/// and optionally another Command
1214
Update: 'msg * 'model -> 'model * Cmd<'msg>
1315
/// Add a subscription that can dispatch messages
1416
Subscribe: 'model -> Sub<'msg>
@@ -22,7 +24,7 @@ type Program<'arg, 'model, 'msg> =
2224
type Program<'arg, 'model, 'msg, 'marker> =
2325
{
2426
State: Program<'arg, 'model, 'msg>
25-
/// Render the application state
27+
/// Renders the model/application state
2628
View: 'model -> WidgetBuilder<'msg, 'marker>
2729
/// Indicates if a previous Widget's view can be reused
2830
CanReuseView: Widget -> Widget -> bool
@@ -50,6 +52,12 @@ module ProgramDefaults =
5052
Trace.WriteLine(String.Format("Unhandled exception: {0}", exn.ToString()), "Debug")
5153
false
5254

55+
/// <summary>
56+
/// A module for building and configuring Elm programs using an MVU loop.
57+
/// See also
58+
/// <seealso href="https://docs.fabulous.dev/advanced/composing-larger-applications/splitting-into-independent-mvu-states" />
59+
/// <seealso href="https://docs.fabulous.dev/basics/mvu" />
60+
/// </summary>
5361
module Program =
5462
let inline private define (init: 'arg -> 'model * Cmd<'msg>) (update: 'msg -> 'model -> 'model * Cmd<'msg>) =
5563
{ Init = init
@@ -58,21 +66,50 @@ module Program =
5866
Logger = ProgramDefaults.defaultLogger()
5967
ExceptionHandler = ProgramDefaults.defaultExceptionHandler }
6068

61-
/// Create a program using an MVU loop
69+
/// <summary>
70+
/// Creates a simple stateful, side-effect-free Program using an MVU loop.
71+
/// The init function takes an input 'arg and returns a 'model representing the initial state,
72+
/// the update function receives a 'msg and a 'model and returns an updated 'model.
73+
/// See the upper diagram in <seealso href="https://elmprogramming.com/elm-architecture-conclusion.html" />
74+
/// </summary>
6275
let stateful (init: 'arg -> 'model) (update: 'msg -> 'model -> 'model) =
6376
define (fun arg -> init arg, Cmd.none) (fun msg model -> update msg model, Cmd.none)
6477

65-
/// Create a program using an MVU loop
78+
/// <summary>
79+
/// Creates a stateful Program using an MVU loop that supports side-effects
80+
/// - by both the init and update functions returning not only a 'model,
81+
/// but an additional Cmd{'msg} alongside it.
82+
/// This is an abstraction of a list of Effect{'msg} functions that return immediatly,
83+
/// but may dispatch zero or many additional messages when given a Dispatch{'msg} function.
84+
/// The Cmd{'msg} is fed into the MVU loop for processing of the side-effects by supplying them with a dispatch.
85+
/// Note that the Cmd{'msg} may not only wrap (async) side-effects,
86+
/// but also simple messages (using Cmd.ofMsg) or even be an empty list (using Cmd.none) .
87+
/// See the lower diagram in <seealso href="https://elmprogramming.com/elm-architecture-conclusion.html" />
88+
/// </summary>
6689
let statefulWithCmd (init: 'arg -> 'model * Cmd<'msg>) (update: 'msg -> 'model -> 'model * Cmd<'msg>) = define init update
6790

68-
/// Create a program using an MVU loop. Add support for CmdMsg
91+
/// <summary>
92+
/// Creates a Program using an MVU loop supporting isolated side-effects.
93+
/// This is similar to statefulWithCmd, but instead of directly returning a Cmd{'msg}
94+
/// (which may wrap a side-effect like a network or DB call),
95+
/// the init and update functions return a simple 'cmdMsg list - making them pure and easy to test.
96+
/// This will require you to add an additional discriminated union type for 'cmdMsg to your Program
97+
/// with a case representing each side-effect.
98+
/// An additional mapCmd function maps a 'cmdMsg back to the intended side-effect.
99+
/// </summary>
69100
let statefulWithCmdMsg (init: 'arg -> 'model * 'cmdMsg list) (update: 'msg -> 'model -> 'model * 'cmdMsg list) (mapCmd: 'cmdMsg -> Cmd<'msg>) =
70101
let mapCmds cmdMsgs = cmdMsgs |> List.map mapCmd |> Cmd.batch
71102
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)
72103

73-
/// Subscribe to external source of events, overrides existing subscription.
74-
/// Return the subscriptions that should be active based on the current model.
75-
/// Subscriptions will be started or stopped automatically to match.
104+
/// <summary>
105+
/// Subscribe the program to an external source of events represented by the subscribe function, overriding existing subscriptions.
106+
/// The subscribe function should return the subscriptions that should be active based on the supplied model.
107+
/// Subscriptions will be started or stopped accordingly.
108+
/// See also
109+
/// <seealso href="https://docs.fabulous.dev/basics/application-state/commands#triggering-commands-from-external-events" />
110+
/// <seealso href="https://elmprogramming.com/subscriptions.html" />
111+
/// <seealso href="https://elmish.github.io/elmish/docs/subscriptionv3.html" />
112+
/// </summary>
76113
let withSubscription (subscribe: 'model -> Sub<'msg>) (program: Program<'arg, 'model, 'msg>) = { program with Subscribe = subscribe }
77114

78115
/// Map existing subscription to external source of events.

src/Fabulous/Sub.fs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ namespace Fabulous
22

33
open System
44

5-
/// SubId - Subscription ID, alias for string list
5+
/// Subscription ID, alias for string list
66
type SubId = string list
77

8-
/// Subscribe - Starts a subscription, returns IDisposable to stop it
8+
/// Starts a subscription by supplying a Dispatch{'msg}
9+
/// which it may use to start dispatching messages similar to Effect{'msg}.
10+
/// Returns an IDisposable to stop it.
911
type Subscribe<'msg> = Dispatch<'msg> -> IDisposable
1012

1113
/// Subscription - Generates new messages when running

0 commit comments

Comments
 (0)