Pattern Matching: Any in tuple?

So I’m pattern matching against the status of 3 requests:

  <PageTitle title="ReScript Pattern Matching Example">
    {switch (requestA, requestB, requestC) {
    | (#Fulfilled(resourceA), #Fulfilled(resourceB), #Fulfilled(resourceC)) =>
      <FormPage resourceA resourceB resourceC />
    | (_, _, #Ready)
    | (_, #Ready, _)
    | (#Ready, _, _)
    | (_, _, #Pending)
    | (_, #Pending, _)
    | (#Pending, _, _) =>
      <Spinner size="2rem" />
    | _ => React.string("An error has occurred")
    }}

Is there a better way to pattern match if any state in the tuple are pending or if any have failed?

Not really, if you want to avoid repetition, you can transform requests to an array explicitly

    | (a,b,c) if [a,b,c]->Js.Array2.some(x => x == #Pending) =>

Also, note that you can match variants this way:

  | (#Ready| #Pending, _, _)
  | (_, #Ready| #Pending, _)
  | (_, _, #Ready | #Pending) =>

I think that pattern matching in your example is totally fine as is.

4 Likes

Thanks! That Ready | Pending syntax does a lot for me here. Another option might be to invert the state again like:

{
  state: Ready | Pending | Fulfilled | Rejected
  context: {
    error: None,
    response: None
  }
}

Then I could load the states into an array and query it as desired. Going with what you proposed for now and see if more querying is actually needed.

Actually, to eliminate illegal states, you could model your state as:

type state =
  | Ready
  | Pending
  | Fulfilled(response)
  | Rejected(error);
2 Likes

That’s pretty much what we started with but it makes it more difficult to query x number of concurrent requests since they can’t be put into an array with their unique response shape

Create a “view type” which you only use in the pattern match. This splits the code into two phases: analyze, where you figure out the validity of the data you have. It returns a new type (the “view”). And execute where you do actions on the data based on the view type.

The analyze phase is probably a variant of a partition into “good” and “bad” states. If there are no “bad” states, you know you have only good data, and if you have at least one “bad”, you can do proper error reporting. Just process the array of bad states, so you can report all of them to the user.

1 Like

That sounds like a manageable solution but currently it’s not clicking what that would look like. By chance do you have an example? Is that a function that takes my core type and returns the view type so I can stick them in the array?

[view(requestA), view(requestB), view(requestC)]

@hoichi a subtle flaw in my original design and this identical version is that when fetching a resource twice it means the original data will be wiped out as soon as the request starts. I think the updated shape is more accurate for a realistic use case as it allows previously requested data to persist until we have new data or an error.

Well, you can extend the variant as:

type state =
  | Ready
  | Fetching
  | Fulfilled(response)
  | Updating(response) // a previous response, that is
  | Rejected(error)
  | Retrying(error) // a previous error

Or something like that. You could use nested variants, for instance, and still pattern match it with a single switch expression.

A coworker has suggested a similar shape to @hoichi’s solution:

type state<'r, 'e> =
| Ready
| Pending
| Fulfilled('response)
| Refreshing('response)
| RefreshingFailed('response, 'error)
| Rejected('error)

It looks clean, but I can’t help but feel it makes too many assumptions about the relationship of the data. For example, not every UI may want to hold onto old data, while some will and I think it makes sense to make it flexible to support on a case-by-case basis so that each UI can determine how it wants to treat subsequent requests.

I’m still rooting for a more general shape:

{
  state: Ready | Pending | Fulfilled | Rejected
  context: {
    error: None,
    response: None
  }
}

Realistically, I don’t think there is an illegal state. Having an error and a response is not unreasonable to me.

Below is a quick demo of different UI scenarios which this solution seems more than capable of representing.

@react.component
let make = () => {
  let (listResourceReq, fetchlistResource) = RequestStateMachine.useRequest(Transport.listResource.fetchAll)

  React.useEffect1(() => {
    fetchlistResource()
  }, [fetchlistResource])

  React.useEffect1(() => {
    Js.log(`
		This side effect will only fire when request state changes,
		which might be more reliable given state is serializable and cheap to compare?
		`)
  }, [listResourceReq.state])

  //////////////////////////////////////////////////////////////////////////////
  // Basic results, error, or empty no helpers
  switch (listResourceReq.state, listResourceReq.error, listResourceReq.data) {
  | (Fulfilled, None, Some(listResource)) => <listResourceList listResource />
  | (Error, Some(error), None) => <RequestError error />
  | (Ready | Pending, _, _) => <Spinner text="Fetching data now" size="2rem" />
  }
  //////////////////////////////////////////////////////////////////////////////

  //////////////////////////////////////////////////////////////////////////////
  // Basic results but with some helpers for conciseness
  open RequestHelpers

  switch listResourceReq.state {
  | Fulfilled => <listResourceList listResource={listResourceReq->getResponse} />
  | Error => <RequestError error={listResourceReq->getError} />
  | Ready
  | Pending =>
    <Spinner text="Loading..." size="2rem" />
  }
  //////////////////////////////////////////////////////////////////////////////

  //////////////////////////////////////////////////////////////////////////////
  // Display the error along with the last data we have
  switch listResourceReq.data {
  | Some(listResource) => <listResourceList listResource />
  | None =>
    if isPendingOrReady(listResourceReq.state) {
      <Spinner text="Loading..." size="2rem" />
    } else {
      React.null
    }
  }

  // Error is conditionally displayed below UI
  switch listResourceReq.error {
  | Some(error) => <ErrorBox message={error.message} />
  | None => React.null
  }
  //////////////////////////////////////////////////////////////////////////////

  //////////////////////////////////////////////////////////////////////////////
  // Multiple requests, with helpers for conciseness
  let (reqA, fetchA) = RequestStateMachine.useRequest(Transport.ResourceA.fetchAll)
  let (reqB, fetchB) = RequestStateMachine.useRequest(Transport.ResourceB.fetchAll)
  let (reqC, fetchC) = RequestStateMachine.useRequest(Transport.ResourceC.fetchAll)

  // Use effect to fetch all three resources ...

  if allFulfilled([reqA.state, reqB.state, reqC.state]) {
    <SomePage a={reqA->getResponse} b={reqB->getResponse} c={reqC->getResponse} />
  } else if anyPendingOrReady([reqA.state, reqB.state, reqC.state]) {
    <Spinner text="Loading..." size="2rem" />
  } else {
    // Could also be a helper function
    let errors = [reqA.error, reqB.error, reqC.error]->Array.keepMap(error => error)
    <RequestErrors errors />
  }
  //////////////////////////////////////////////////////////////////////////////
}

While it’s definitely not as clean, I would wager the flexibility is more important given that in a medium sized application I don’t want to over-engineer how requests can behave.

Anyone want to weigh in on which solution strategy you would go with and why?

At the end of the day, the logic for fetching data is for the auth authors to decide: as you say yourself, it depends on the UI needs, so you should design what you want from the fetching layer. There are of course occasional attempts to come up with a universal solution, but I’m not sure a universal solution exists.

One thing though: you can always convert a tighter shape to a more loose shape, e.g.:

let toTriad = state =>
  switch state {
  | Ready => (Ready, None, None)
  | Pending => (Pending, None, None)
  | Fulfilled(r) => (Fulfilled, Some(r), None)
  | Refreshing(r) => (Pending, Some(r), None)
  | RefreshingFailed(r, e) => (Fulfilled, Some(r), Some(e)) // or Rejected?
  | Rejected(e) => (Rejected, None, Some(e))
}

Or you can convert to something like (pseudocode) (option<'data>, isLoading, option<'error>), if that’s how you’d like to consume it: e.g., displaying data, loader, and errors are independent in your JSX.

You can even choose to not expose the tighter variant at all, but have it help you reason about your hook’s implementation.

I like the idea of that but I’m not so sure it would work in practice. Say in the context of a search UI I feel it would be reasonable to have previous search results, an error from a query shown below caused by an invalid symbol, and is in the process of loading the new corrected query which will clear the error and replace the previous search results.

Even the looser structure would not be able to model that if I’m not mistaken.

After rereading your solution and discussing it more I realized I was wrong. We could model loading, while retaining the previous data, while we have an error it would just take a slightly different state to explicitly contain that. It’s also a good point that it’s much easier to go from a rigid structure to a loose structure than the other way. I’ve marked your proposal as the solution, my apologies on the slow pickup :sweat_smile:

1 Like

I was thinking along the lines of

switch view([reqA, reqB, reqC]) {
    | Ok(a,b,c) => ...
    | Failure(err) => ...
}

Where the view-function analyses the request providing the “view” which the main logic needs. Splitting the logic like this can some times make code easier to handle since you can mint a type on the spot to handle the logic you need.

It fits into the idea of eliminating illegal states too.

1 Like

That was the first approach I tried but I ran into two problems:

  1. Each request has a different response type and couldn’t be contained in an array
  2. Because of #1 it was not trivial to combine results

Thinking about it again maybe the trick is to write it like view2, view3, view4, view5 and just define them with all possible combinations in a switch pattern match in a tuple?

  • If any state is pending or ready or refreshing it is considered Pending
  • If any state is in error state it is an Error
  • If all states are fulfilled or refreshed it is in the Ok state

That seems like it could get complex with a large matrix of tuples to handle each of those scenarios. Maybe view functions should combine smaller predicate checks like isPending2 or isPending3, isError2, isError3 for each of those scenarios?