package ppx_pattern_bind

  1. Overview
  2. Docs
A ppx for writing fast incremental bind nodes in a pattern match

Install

Dune Dependency

Authors

Maintainers

Sources

ppx_pattern_bind-v0.16.0.tar.gz
sha256=8c67fe24726af67268534a39d841ae4d099ec761b5459e6b28cd89a052d5c1f6

Description

A ppx rewriter that is intended for use with Incremental. It makes it easier to write incremental computations using pattern-matching in a way that causes incremental nodes to fire as little as possible.

Published: 14 Jun 2023

README

README.mlt

[%%org
  {|
#+TITLE: ppx\_pattern\_bind

A ppx rewriter that is intended for use with Incremental. It makes it
easier to write incremental computations using pattern-matching in a
way that causes incremental nodes to fire as little as possible.

* Description

It's easiest to understand this rewriter in the context of
Incremental, and indeed, while it can be used with any monadic
library, we don't have any other immediate use-cases in mind.

To see why it's useful, consider what it might be like to build an
incremental rendering function for a value that is structured as a
variant.

Here's a simple example, starting with two model types, each with a
(in this case trivial) incremental function for rendering the model to
a string.
|}]

open Base
module Incr = Incremental.Make ()
open Incr.Let_syntax

module A_model = struct
  type t = int

  let render t =
    let%map t = t in
    Int.to_string t
  ;;
end

module B_model = struct
  type t = float

  let render t =
    let%map t = t in
    Float.to_string t
  ;;
end

[%%org {|
The final model type combines the underlying models under a variant.
|}]

module Model = struct
  type t =
    | A of A_model.t
    | B of B_model.t
end

[%%org
  {|
Writing a render function for this is a little tricky. We could do it
as follows, using bind:
|}]

let render (m : Model.t Incr.t) =
  match%bind m with
  | A a -> A_model.render (Incr.return a)
  | B b -> B_model.render (Incr.return b)
;;

[%%org
  {|
But while this is semantically reasonable, the implementation is
entirely non-incremental, since the bind means you redo the entire
rendering of the model from scratch on any change.

With ~match%pattern_bind~, we can build a more incremental version of
this computation as follows (note that ~x~ and ~y~ are incremental nodes).
|}]

let render (m : Model.t Incr.t) =
  match%pattern_bind m with
  | A x -> A_model.render x
  | B y -> B_model.render y
;;

[%%org
  {|
The bind part of this computation only refires when the /case/ of the
match changes and not when the variant fields change.

For convenience, we also have a ~match%pattern_map~, which behaves identically
to a ~%pattern_bind~, but its variables are not incremental and the value on the
right hand side does not need to be incremental.

The only difference between ~match%pattern_map~ and ~match%map~ is that using
~match%pattern_map~ cuts off computations in the non-matched part of the
patterns.  That is, updates to the non-matched part of the patterns will not
trigger a recomputation of the right-hand side of the ~%pattern_map~.
|}]

module Tuple_model = struct
  type t =
    | A of int * int
    | B of float
end

let increment_model (m : Tuple_model.t Incr.t) =
  match%pattern_map m with
  | A (x, _) -> Model.A (x + 1)
  | B y -> Model.B (y +. 1.)
;;

[%%org
  {|
Another use case for ~%pattern_bind~ occurs with incremental records. Often, we
wish to construct a new node that depends on only few values from the record.
A correct, but not very incremental, way of doing this would be to simply pattern
match on the record.
|}]

module Record_model = struct
  type t =
    { a : int
    ; b : bool
    ; c : float
    }
  [@@deriving fields]
end

let render_ab a b = if b then Int.to_string a else "Error"

let render (m : Record_model.t Incr.t) =
  let%map { a; b; _ } = m in
  render_ab a b
;;

[%%org
  {|
Since ~%pattern_bind~ projects out each variable from a pattern separately, we
can get more incrementality with a ~let%pattern_bind~, which is equivalent to
a ~match%pattern_bind~ with a single case:
|}]

let render (m : Record_model.t Incr.t) =
  let%pattern_bind { a; b; _ } = m in
  let%map a = a
  and b = b in
  render_ab a b
;;

[%%org {|
For convenience, we also have a ~let%pattern_map~:
|}]

let render (m : Record_model.t Incr.t) =
  let%pattern_map { a; b; _ } = m in
  render_ab a b
;;

[%%org
  {|
* Supported Patterns

The ~match%pattern_bind~ works with almost any pattern.

It supports nested patterns by projecting out each of the variables in
the pattern separately. This allows each variable in the pattern
to be an incremental, regardless of the nesting structure of the
pattern.

For example, the right hand side of a case such as
|}]

let f e =
  match%pattern_bind e with
  | (x, _), _, (_, y) ->
    let%map x = x
    and y = y in
    x + y
;;

[%%org {|
is translated to
|}]

let f e =
  let v = e in
  let x =
    match%map v with
    | (x, _), _, (_, _) -> x
  and y =
    match%map v with
    | (_, _), _, (_, y) -> y
  in
  let%map x = x
  and y = y in
  x + y
;;

[%%org
  {|
* Unsupported Patterns

Patterns that bind module names are not supported, as shown below. This
is because you can't put an ordinary module inside of a monadic value.

Patterns that match on exceptions are not supported, because they would
require support from ~ppx_let~.

~when~ is not supported because it creates ambiguities about whether a variable
is bound in the pattern for the purpose of use in the ~when~ computation, or
the right hand side, or both, and this matters for warnings and not depending
on spurious values.
|}]

let f e =
  match%pattern_bind e with
  | (module M : S) -> return 0
;;

let f e =
  match%pattern_bind e with
  | x -> return 0
  | exception _ -> return 1
;;

let f e =
  match%pattern_bind e with
  | x when is_ok x -> return 0
;;

[%%expect
  {|
Line _, characters _-_:
Error: %pattern_bind cannot be used with (module ..) patterns.
Line _, characters _-_:
Error: %pattern_bind cannot be used with exception patterns.
Line _, characters _-_:
Error: %pattern_bind cannot be used with `when`.
|}]

Dependencies (5)

  1. ppxlib >= "0.28.0"
  2. dune >= "2.0.0"
  3. ppx_let >= "v0.16" & < "v0.17"
  4. base >= "v0.16" & < "v0.17"
  5. ocaml >= "4.14.0"

Dev Dependencies

None

Used by (3)

  1. bonsai >= "v0.16.0"
  2. incr_dom_partial_render >= "v0.16.0"
  3. incr_map >= "v0.16.0"

Conflicts

None