diff options
-rw-r--r-- | examples/05-http-requests/README.md | 4 | ||||
-rw-r--r-- | examples/06-custom-effects/README.md | 23 | ||||
-rw-r--r-- | gleam.toml | 2 | ||||
-rw-r--r-- | pages/guide/02-state-management.md | 4 | ||||
-rw-r--r-- | pages/guide/03-side-effects.md | 258 | ||||
-rw-r--r-- | pages/hints/pure-functions.md | 55 |
6 files changed, 325 insertions, 21 deletions
diff --git a/examples/05-http-requests/README.md b/examples/05-http-requests/README.md index 4a118e4..c5fe073 100644 --- a/examples/05-http-requests/README.md +++ b/examples/05-http-requests/README.md @@ -51,8 +51,8 @@ want the Lustre runtime to execute before the next invocation of the `view` function. > **Note**: notice how the type of `view` remains the same. In Lustre, your `view` -> is always a [_pure function_](https://en.wikipedia.org/wiki/Pure_function) that -> takes a model and returns the UI to be rendered: we never perform side effects +> is always a [_pure function_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md) +> that takes a model and returns the UI to be rendered: we never perform side effects > in the `view` function itself. ## HTTP requests as side effects diff --git a/examples/06-custom-effects/README.md b/examples/06-custom-effects/README.md index 99b90c5..0af61bd 100644 --- a/examples/06-custom-effects/README.md +++ b/examples/06-custom-effects/README.md @@ -7,11 +7,10 @@ this example we'll see how to create effects of our own using Lustre's [`effect.from`](https://hexdocs.pm/lustre/lustre/effect.html#from) function. -Since we use effects to communicate with _external_ systems (like the -browser or the Erlang VM) you'll find creating custom effects usually involves -Gleam's [external -functions](https://tour.gleam.run/everything/#advanced-features-externals). So -be sure to read up on that! +Since we use effects to communicate with _external_ systems (like the browser or +the Erlang VM) you'll find creating custom effects usually involves Gleam's +[external functions](https://tour.gleam.run/everything/#advanced-features-externals). +So be sure to read up on that! > Gleam externals are part of its "foreign function interface", which is why > you'll typically see files with `ffi` in the name - like @@ -20,8 +19,9 @@ be sure to read up on that! ## Accessing Browser Storage In this example, the external system we want to interact with is the browser's -[local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). This way, we can write a message into the text input and it will -still be there when we refresh the page. Handy! +[local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). +This way, we can write a message into the text input and it will still be there +when we refresh the page. Handy! The `view`, `update` and `init` functions should look pretty familiar by now, so let's focus on the functions that generate our custom effects. @@ -58,9 +58,14 @@ Here, our effect is simpler. We tell the gleam compiler we don't need to use the pattern](https://tour.gleam.run/everything/#basics-discard-patterns). Then we write to local storage, and no more work needs to be done. -You may be wondering, why bother using an effect if we aren't also going to update our program with the result? Why not just fire off `do_write_localstorage` directly from the `update` function or a custom event handler? +You may be wondering, why bother using an effect if we aren't also going to update +our program with the result? Why not just fire off `do_write_localstorage` directly +from the `update` function or a custom event handler? -Using effects has many benefits! It lets us implement our `update` and `view` functions as [pure functions](https://en.wikipedia.org/wiki/Pure_function). This makes them easier to test, allows for time-travel debugging, and even opens the door to easily porting them to [server components](https://hexdocs.pm/lustre/lustre/server_component.html). +Using effects has many benefits! It lets us implement our `update` and `view` +functions as [pure functions](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md). +This makes them easier to test, allows for time-travel debugging, and even opens +the door to easily porting them to [server components](https://hexdocs.pm/lustre/lustre/server_component.html). ## Another note on message naming @@ -18,7 +18,7 @@ pages = [ { title = " ", path = "#", source = "" }, { title = "Quickstart guide", path = "guide/01-quickstart.html", source = "./pages/guide/01-quickstart.md" }, { title = "Managing state", path = "guide/02-state-management.html", source = "./pages/guide/02-state-management.md" }, - # { title = "Side effects", path = "#", source = "" }, + { title = "Side effects", path = "guide/03-side-effects.html", source = "./pages/guide/03-side-effects.md" }, # { title = "Server-side rendering", path = "#", source = "" }, # { title = "Components", path = "#", source = "" }, # { title = "Server components", path = "#", source = "" }, diff --git a/pages/guide/02-state-management.md b/pages/guide/02-state-management.md index 339b013..26cd752 100644 --- a/pages/guide/02-state-management.md +++ b/pages/guide/02-state-management.md @@ -10,8 +10,8 @@ The MVU architecture is an example of _unidirectional data flow_: - Your model describes the entire state of your application at a given point in time. -- The UI is a pure function of that model: if the model doesn't change, the UI - doesn't change. +- The UI is a [pure](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md)) + function of that model: if the model doesn't change, the UI doesn't change. - Events from the outside world – user interaction, HTTP responses, ... – send messages to an update function that constructs a new model. diff --git a/pages/guide/03-side-effects.md b/pages/guide/03-side-effects.md index fa86b92..f7ab3de 100644 --- a/pages/guide/03-side-effects.md +++ b/pages/guide/03-side-effects.md @@ -1,24 +1,268 @@ # 03 Side effects Lustre's implementation of the Model-View-Update architecture includes one -additional piece of the puzzle: managed side effects. +additional piece of the puzzle: managed side effects. If we take the MVU diagram +from the previous guide and upgrade it to include managed effects, it looks like +this: + +```text + +--------+ + | | + | update | + | | + +--------+ + ^ | + | | + Msg | | #(Model, Effect(msg)) + | | + | v ++------+ +------------------------+ +| | #(Model, Effect(msg)) | | +| init |------------------------>| Lustre Runtime | +| | | | ++------+ +------------------------+ + ^ | + | | + Msg | | Model + | | + | v + +--------+ + | | + | view | + | | + +--------+ +``` + +Well what does managed effects mean, exactly? In Lustre, we expect your `init`, +`update`, and `view` functions to be [_pure_](https://github.com/lustre-labs/lustre/blob/main/pages/hints/pure-functions.md). +That means they shouldn't perform side effects like making a HTTP request or writing +to local storage: we should be able to run your functions 100 times with the same +input and get the same output every time! + +Of course, in real applications performing HTTP requests and writing to local +storage turn out to be quite useful things to do. If we shouldn't perform side +effects in our code how do we do them then? Lustre has an [`Effect`](https://hexdocs.pm/lustre/lustre/effect.html) +type that _tells the runtime what side effects to perform_. So we say "Hey, I +want to make a HTTP request to this URL and when you get the response, dispatch +this message to me". The runtime takes care of performing the side effect and +turning the result into something our `update` function understands. ## Why managed effects? +This can feel like a lot of ceremony to go through just to make a HTTP request. +The natural question is: why not just let us make these requests ourselves? + +Managed effects have a number of benefits that come from _separating our programs +from the outside world_: + +1. **Predictability**: by keeping side effects out of our `update` function, we + can be confident that our application's state is only ever changed in one + place. This makes it easier to reason about our code and track down bugs. + +2. **Testability**: because our application code is pure, we can test it without + needing to mock out HTTP services or browser APIs. We can test our `update` + function, for example, by passing in a sequence of messages: no network mocks + required! + +3. **Reusability**: Lustre applications can run in a variety of environments and + contexts. The more we push platform-specific code into managed effects, the + easier time we'll have running our application as a [server component](https://hexdocs.pm/lustre/lustre/server_component.html) + or as a static site. + +## Packages for common effects + +The community has started to build packages that cover common side effects. For +many applications it's enough to drop these packages in and start using them +without needing to write any custom effects. + +> **Note**: _all_ of these packages are community maintained and unrelated to the +> core Lustre organisation. If you run into issues please open an issue on the +> package's repository! + +- [`lustre_http`](https://hexdocs.pm/lustre_http/) lets you make HTTP requests + and describe what responses to expect from them. + +- [`lustre_websocket`](https://hexdocs.pm/lustre_websocket/) handles WebSocket + connections and messages. + +- [`modem`](https://hexdocs.pm/modem/) and [`lustre_routed`](https://hexdocs.pm/lustre_routed/) + are two packages that help you manage navigation and routing. + +- [`lustre_animation`](https://hexdocs.pm/lustre_animation/) is a simple package + for interpolating between values over time. + +## Running effects + +We know that effects need to be performed by the runtime, but how does the runtime +know when we want it to run an effect? If you have been using the `lustre.simple` +application constructor until now, it is time to upgrade to +[`lustre.application`](https://hexdocs.pm/lustre/lustre.html#application)! + +Full Lustre applications differ from simple applications in one important way by +returning a tuple of `#(Model, Effect(Msg))` from your `init` and `update` +functions: + +```gleam +pub fn simple( + init: fn(flags) -> model, + update: fn(model, msg) -> model, + view: fn(model) -> Element(msg), +) -> App(flags, model, msg) + +pub fn application( + init: fn(flags) -> #(model, Effect(msg)), + update: fn(model, msg) -> #(model, Effect(msg)), + view: fn(model) -> Element(msg), +) -> App(flags, model, msg) +``` + +We can, for example, launch an HTTP request on application start by using `lustre_http.get` +in our `init` function: + +```gleam +fn init(_) { + let model = Model(...) + let get_ip = lustre_http.get( + "https://api.ipify.org", + ApiReturnedIpAddress + ) + + #(model, get_ip) +} +``` + +> **Note**: to tell the runtime we _don't_ want to perform any side effects this +> time, we can use [`effect.none()`](https://hexdocs.pm/lustre/lustre/effect.html#none). + ## Writing your own effects -## Effects without dispatch +When you need to do something one of the existing packages doesn't cover, you need +to write your own effect. You can do that by passing a callback to +[`effect.from`](https://hexdocs.pm/lustre/lustre/effect.html#from). Custom effects +are called with an argument – commonly called `dispatch` – that you can use to +send messages back to your application's `update` function. + +Below is an example of a custom effect that reads a value from local storage: + +```js +// ffi.mjs +import { Ok, Error } from "./gleam.mjs"; + +export function read(key) { + const value = window.localStorage.getItem(key); + return value ? new Ok(value) : new Error(undefined); +} +``` + +```gleam +fn read(key: String, to_msg: fn(Result(String, Nil) -> msg) -> Effect(msg) { + effect.from(fn(dispatch) { + do_read(key) + |> to_msg + |> dispatch + }) +} + +@external(javascript, "ffi.mjs", "read") +fn do_read(key: String) -> Result(String, Nil) { + Error(Nil) +} +``` + +> **Note**: we provide a default implementation of the `do_read` function that +> always fails. Where possible it's good to provide an implementation for all of +> Gleam's targets. This makes it much easier to run your code as a +> [server component](https://hexdocs.pm/lustre/lustre/server_component.html) in +> the future. + +### Effects that touch the DOM + +Lustre runs all your side effects after your `update` function returns but _before_ +your `view` function is called. A common bug folks run into is trying to interact +with a particular element in the DOM before it's had a chance to render. As a +rule of thumb, you should _always_ wrap custom effects that interact with the DOM +in a [`requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) +call to ensure the DOM has had a chance to update first. + +### Effects without dispatch So far, we have seen side effects that are expected to _return something_ to our program. If we fire an HTTP request, it wouldn't be much use if we couldn't get -the response back! +the response back! Sometimes folks wrongly assume effects _must_ use the `dispatch` +function their given, but this isn't true! + +It's also totally valid to write effects that don't dispatch any messages. Earlier +we saw an example of how to read from local storage, we might also want an effect +to _write_ to local storage and there's not much to dispatch in that case! + +```js +// ffi.mjs +export function write(key, value) { + window.localStorage.setItem(key, value); +} +``` + +```gleam +// app.gleam +fn write(key: String, value: String) -> Effect(msg) { + effect.from(fn(_) { + do_write(key, value) + }) +} + +@external(javascript, "ffi.mjs", "write") +fn do_write(key: String, value: String) -> Nil { + Nil +} +``` + +### Effects with multiple dispatch + +Similar to effects that don't dispatch any messages, some folks skip over the fact +effects can dispatch _multiple_ messages. Packages like [`lustre_websoket`](https://hexdocs.pm/lustre_websocket/) +and [`modem`](https://hexdocs.pm/modem/) set up effects that will dispatch many +messages over the lifetime of your program. + +Once you have a reference to that `dispatch` function, you're free to call it as +many times as you want! + +```js +// ffi.mjs +export function every(interval, cb) { + window.setInterval(cb, interval); +} +``` + +```gleam +// app.gleam +fn every(interval: Int, tick: msg) -> Effect(msg) { + effect.from(fn(dispatch) { + do_every(interval, fn() { + dispatch(tick) + }) + }) +} + +@external(javascript, "ffi.mjs", "every") +fn do_every(interval: Int, cb: fn() -> Nil) -> Nil { + Nil +} +``` + +Here we set up an effect that will continuously dispatch a `tick` message at a +fixed interval. + +## Related examples + +If you'd like to see some of the ideas in action, we have a number of examples +that demonstrate how Lustre's effects system works in practice: + +- [`05-http-requests`](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests) +- [`06-custom-effects`](https://github.com/lustre-labs/lustre/tree/main/examples/06-custom-effects) +- [`07-routing`](https://github.com/lustre-labs/lustre/tree/main/examples/07-routing) ## Getting help If you're having trouble with Lustre or not sure what the right way to do something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy). You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues). - -While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org) -is also a great resource for learning about the Model-View-Update architecture -and the kinds of patterns that Lustre is built around. diff --git a/pages/hints/pure-functions.md b/pages/hints/pure-functions.md new file mode 100644 index 0000000..4e8ae4a --- /dev/null +++ b/pages/hints/pure-functions.md @@ -0,0 +1,55 @@ +# Pure functions + +Throughout Lustre's documentation you may come across references to _purity_ or +_pure functions_. Lustre makes some fundamental assumptions around purity in your +programs so it's important to know what it means! + +## Functions as formulas + +Outside of programming, the concept of a "function" exists in mathematics too. +There, they are sometimes referred to as _formulas_ and you might have already +encountered them in school, something like: + +``` +f(x) = 3x +``` + +Or "a function over `x` is equal to `3` times `x`". Let's jump back to Gleam and +write that function again: + +```gleam +fn f(x) { + 3 * x +} +``` + +Functions like this are _pure_. They take an input, perform some computation, and +return an output. The output is _only_ determined by the input, and the function +doesn't change anything about the outside world like writing to a file or making +a HTTP request. + +**Lustre assumes your `init`, `update`, and `view` functions are pure** and breaking +this assumption can lead to unexpected behaviour. It is _really_ important to +make sure these functions are pure! + +## Functions as procedures + +Of course, what sets programming apart from mathematics is that we _can_ have +side effects in our programs. You may sometimes see functions that perform side +effects referred to as _procedures_, and they are useful too! + +Lustre may expect your `init`, `update`, and `view` functions to be pure, but +that doesn't mean _Gleam_ does. To learn more about how Lustre handles side +effects and procedures, check out the [side effects guide](https://hexdocs.pm/lustre/guide/03-side-effects.html). + +## Other resources + +Here are some other resources from around the Web that you might also find useful: + +- [Keeping components pure](https://react.dev/learn/keeping-components-pure) from + the React docs. + +- [Pure functions](https://elmprogramming.com/pure-functions.html) from the + "Beginning Elm" book. + +- [Pure functions](https://en.wikipedia.org/wiki/Pure_function) from Wikipedia. |