Current section
Files
Jump to
Current section
Files
src/optimist.gleam
//// When building user interfaces, we are often faced with the question of what
//// to do while waiting for a response from a server. One common pattern is to
//// update the ui with an optimistic value - one we expect to get back if an
//// operation is successful - and then update the ui again once we get an actual
//// response.
////
//// The `Optimistic` type is a simple way to model this pattern. A value can
//// either be fully resolved or it can be an optimistic update pending resolution.
//// The functions in this module help you manage and query the state of an
//// optimistic update.
////
// TYPES -----------------------------------------------------------------------
/// A value that is either fully resolved or an optimistic update pending resolution.
/// You can access the value of an `Optimistic` by using the [`unwrap`](#unwrap)
/// function.
///
pub opaque type Optimistic(a) {
Resolved(value: a)
Pending(value: a, fallback: a)
}
// CONSTRUCTORS ----------------------------------------------------------------
/// Construct an `Optimistic` update from a fully resolved value.
///
pub fn from(value: a) -> Optimistic(a) {
Resolved(value)
}
// QUERIES ---------------------------------------------------------------------
/// Determine if an `Optimistic` update is fully resolved.
///
/// **Note**: it is uncommon to need this function. The optimistic ui pattern
/// typically means we are pretending an operation succeeded before we know for
/// sure. If you want to know if some request or other async job is still in
/// progress, you probably have that information in a more direct form elsewhere
/// in your state!
///
pub fn is_resolved(optimistic: Optimistic(a)) -> Bool {
case optimistic {
Resolved(_) -> True
Pending(_, _) -> False
}
}
/// Determine if an `Optimistic` update is still pending resolution.
///
/// **Note**: it is uncommon to need this function. The optimistic ui pattern
/// typically means we are pretending an operation succeeded before we know for
/// sure. If you want to know if some request or other async job is still in
/// progress, you probably have that information in a more direct form elsewhere
/// in your state!
///
pub fn is_pending(optimistic: Optimistic(a)) -> Bool {
case optimistic {
Resolved(_) -> False
Pending(_, _) -> True
}
}
// MANIPULATIONS ---------------------------------------------------------------
/// Push an optimistic update. If the current value is already resolved, this
/// becomes a new optimistic update and the current value becomes the fallback.
/// If the current value is already an optimistic update, the value is updated
/// but the fallback remains unchanged.
///
/// ### Optimistic update of a resolved value
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.push(2)
/// |> optimist.unwrap
///
/// optimistic |> should.equal(2)
/// }
/// ```
///
/// ### Multiple optimistic updates
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.push(2)
/// |> optimist.push(3)
/// |> optimist.unwrap
///
/// optimistic |> should.equal(3)
/// }
/// ```
///
/// ### Rejecting multiple optimistic updates
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.push(2)
/// |> optimist.push(3)
/// |> optimist.revert
/// |> optimist.unwrap
///
/// optimistic |> should.equal(1)
/// }
/// ```
///
pub fn push(optimistic: Optimistic(a), value: a) -> Optimistic(a) {
case optimistic {
Resolved(fallback) -> Pending(value, fallback)
Pending(_, fallback) -> Pending(value, fallback)
}
}
/// Perform an optimistic update. If the current value is already resolved, this
/// becomes a new optimistic update and the current value becomes the fallback.
/// If the current value is already an optimistic update, the value is updated
/// but the fallback remains unchanged.
///
/// ### Optimistic update of a resolved value
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.update(int.add(_, 1))
/// |> optimist.unwrap
///
/// optimistic |> should.equal(2)
/// }
/// ```
///
/// ### Multiple optimistic updates
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.update(int.add(_, 1))
/// |> optimist.update(int.add(_, 2))
/// |> optimist.unwrap
///
/// optimistic |> should.equal(3)
/// }
/// ```
///
/// ### Rejecting multiple optimistic updates
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.update(int.add(_, 1))
/// |> optimist.update(int.add(_, 2))
/// |> optimist.revert
/// |> optimist.unwrap
///
/// optimistic |> should.equal(1)
/// }
/// ```
///
pub fn update(optimistic: Optimistic(a), f: fn(a) -> a) -> Optimistic(a) {
case optimistic {
Resolved(fallback) -> Pending(f(fallback), fallback)
Pending(_, fallback) -> Pending(f(fallback), fallback)
}
}
/// Take an `Optimistic` update and force it to be resolved. This will erase the
/// fallback value and commit to whatever value is currently stored.
///
/// ### Forcing an optimistic update
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.update(2)
/// |> optimist.force
/// |> optimist.unwrap
///
/// optimistic |> should.equal(2)
/// }
/// ```
///
pub fn force(optimistic: Optimistic(a)) -> Optimistic(a) {
case optimistic {
Resolved(_) -> optimistic
Pending(value, _) -> Resolved(value)
}
}
/// Take an `Optimistic` update and revert it back to its initial value if it is
/// still pending.
///
/// ### Reverting an optimistic update
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let optimistic =
/// optimist.from(1)
/// |> optimist.update(2)
/// |> optimist.revert
/// |> optimist.unwrap
///
/// optimistic |> should.equal(1)
/// }
/// ```
///
pub fn revert(optimistic: Optimistic(a)) -> Optimistic(a) {
case optimistic {
Resolved(_) -> optimistic
Pending(_, fallback) -> Resolved(fallback)
}
}
/// Take a `Result` and use it to resolve an optimistic update. If the result is
/// `Ok` that value becomes a new resolved value. If the result is an `Error` and
/// an optimistic update is pending, the fallback value is used to resolve the
/// update.
///
/// ```gleam
/// case result {
/// Ok(value) -> optimist.from(value)
/// Error(_) -> optimist.revert(optimistic)
/// }
/// ```
///
/// ### Resolving a successful optimistic update
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let result = Ok(2)
/// let optimstic =
/// optimist.from(1)
/// |> optimistic.update(2)
/// |> optimist.resolve(result)
/// |> optimist.unwrap
///
/// optimistic |> should.equal(2)
/// }
/// ```
///
/// ### Rejecting a failed optimistic update
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let result = Error("failed")
/// let optimistic =
/// optimist.from(1)
/// |> optimistic.update(2)
/// |> optimist.resolve(result)
/// |> optimist.unwrap
///
/// optimistic |> should.equal(1)
/// }
/// ```
///
pub fn resolve(optimistic: Optimistic(a), result: Result(a, _)) -> Optimistic(a) {
case result {
Ok(value) -> Resolved(value)
Error(_) -> revert(optimistic)
}
}
/// Take a `Result` and use it to resolve an optimistic update. If the result is
/// `Ok` that provided callback is run using either the value of an already-resolved
/// optimistic update, or the **fallback value** of a pending update.
///
/// ### Resolving a successful optimistic update
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let history = ["hey", "hi"]
/// let result = Ok("how are you?")
/// let optimistic =
/// optimist.from(history)
/// |> optimist.update(list.prepend(_, "how are you?"))
/// |> optimist.try(result, list.prepend)
/// |> optimist.unwrap
///
/// optimistic |> should.equal(["how are you?", "hey", "hi"]
/// }
/// ```
///
/// ### Resolving an unsuccessful optimistic update
///
/// ```gleam
/// import gleeunit/should
/// import optimist
///
/// pub fn example() {
/// let history = ["hey", "hi"]
/// let result = Error(Nil)
/// let optimistic =
/// optimist.from(history)
/// |> optimist.update(list.prepend(_, "how are you?"))
/// |> optimist.try(result, list.prepend)
/// |> optimist.unwrap
///
/// optimistic |> should.equal(["hey", "hi"]
/// }
/// ```
///
pub fn try(
optimistic: Optimistic(a),
result: Result(b, _),
f: fn(a, b) -> a,
) -> Optimistic(a) {
case result, optimistic {
Ok(a), Resolved(value) -> Resolved(f(value, a))
Ok(a), Pending(_, fallback) -> Resolved(f(fallback, a))
Error(_), _ -> revert(optimistic)
}
}
// CONVERSIONS -----------------------------------------------------------------
/// Unwrap an `Optimistic` value and return the underlying value.
///
pub fn unwrap(optimistic: Optimistic(a)) -> a {
optimistic.value
}