Merge tuples types

Could I do something clever to merge tuples?

For example:

I have two values, and I want to create a new combined tuple value from them. If one of the two values is a tuple, I’d like to unfold it in the new type.

Any suggestions?

you’re trying to create some kind of heterogeneous list, right?

What problem are you trying to solve with this data structure?

Yes, I believe so.

Right now, in my bindings I invoke an external function that returns:

  type queryResult<'documentdata> = (
    option<Firebase.Firestore.querySnapshot<'documentdata>>,
    bool,
    option<Firebase.Firestore.firestoreError>,
  )

If I have two queries, I’d like to combine the queryResult to have something like:

type internalQueryResult<'t> =
  | Data('t)
  | Loading
  | Error(Firebase.Firestore.firestoreError)
  | Unknown

If either query is loading or has an error then return that for combined result.
If both have data, return a tuple of data 'a and data 'b.

let combine = (
  a: ReactFirehooks.Firestore.queryResult<'a>,
  b: ReactFirehooks.Firestore.queryResult<'b>,
) => {
  switch (a, b) {
  | ((_, true, _), _)
  | (_, (_, true, _)) =>
    Loading
  | ((_, _, Some(error)), _) => Error(error)
  | (_, (_, _, Some(error))) => Error(error)
  | (
      (Some(qsA), _, _), 
      (Some(qsB), _ ,_)
    ) => {
      Data(qsA, qsB)
    } 
  | _ => Unknown
  }
}

Some of my components have five queries, so I’d like to flatten that nested tuple result.

you likely know at compile time the types of the results you’re going to get, so depending on how you can to consume the results, you have multiple choices.

  1. create a record:
type okResults = {
  a?: A.res,
  b?: B.res,
  c?: C.res,
}
  1. or you can use an array of variants:
type okResult =
  | A(A.res)
  | B(B.res)
  | C(C.res)

type okResults = array<okResult>

You need to have some kind of knowledge of the shape of your result on compile time, otherwise the compiler can’t help you!

1 Like

It really depends on the component.

In some components, I have a single query, while in others, I have eight queries. Sometimes all the queries share the same generic type; other times, they do not.

Currently, I’m using something like this:

let combine8 = (
  a: ReactFirehooks.Firestore.queryResult<'a'>,
  b: ReactFirehooks.Firestore.queryResult<'b'>,
  c: ReactFirehooks.Firestore.queryResult<'c'>,
  d: ReactFirehooks.Firestore.queryResult<'d'>,
  e: ReactFirehooks.Firestore.queryResult<'e'>,
  f: ReactFirehooks.Firestore.queryResult<'f'>,
  g: ReactFirehooks.Firestore.queryResult<'g'>,
  h: ReactFirehooks.Firestore.queryResult<'h'>
) => {
  switch (a, b, c, d, e, f, g, h) {
  | ((_, true, _), _, _, _, _, _, _, _)
  | (_, (_, true, _), _, _, _, _, _, _)
  | (_, _, (_, true, _), _, _, _, _, _)
  | (_, _, _, (_, true, _), _, _, _, _)
  | (_, _, _, _, (_, true, _), _, _, _)
  | (_, _, _, _, _, (_, true, _), _, _)
  | (_, _, _, _, _, _, (_, true, _), _)
  | (_, _, _, _, _, _, _, (_, true, _)) =>
    Loading
  | ((_, _, Some(error)), _, _, _, _, _, _, _)
  | (_, (_, _, Some(error)), _, _, _, _, _, _)
  | (_, _, (_, _, Some(error)), _, _, _, _, _)
  | (_, _, _, (_, _, Some(error)), _, _, _, _)
  | (_, _, _, _, (_, _, Some(error)), _, _, _)
  | (_, _, _, _, _, (_, _, Some(error)), _, _)
  | (_, _, _, _, _, _, (_, _, Some(error)), _)
  | (_, _, _, _, _, _, _, (_, _, Some(error))) =>
    Error(error)
  | (
      (Some(qsA), _, _),
      (Some(qsB), _, _),
      (Some(qsC), _, _),
      (Some(qsD), _, _),
      (Some(qsE), _, _),
      (Some(qsF), _, _),
      (Some(qsG), _, _),
      (Some(qsH), _, _)
    ) =>
    Data((qsA, qsB, qsC, qsD, qsE, qsF, qsG, qsH))
  | _ => Unknown
  }
}

While these generic types are usually limited to three or four actual types, they may change over time.

at some point, you have to consume these results, so you have to know the dimension of your tuple and its inner types. What is the problem here? Is it about having tuples of dynamic size (which you can’t have), or is it that the combine function is too cumbersome?

If it’s the latter, you could combine them in a better manner:

let combine8 = (
  a: ReactFirehooks.Firestore.queryResult<'a>,
  b: ReactFirehooks.Firestore.queryResult<'b>,
  c: ReactFirehooks.Firestore.queryResult<'c>,
  d: ReactFirehooks.Firestore.queryResult<'d>,
  e: ReactFirehooks.Firestore.queryResult<'e>,
  f: ReactFirehooks.Firestore.queryResult<'f>,
  g: ReactFirehooks.Firestore.queryResult<'g>,
  h: ReactFirehooks.Firestore.queryResult<'h>,
) => {
  switch (combine7(a, b, c, d, e, f, g), h) {
  | (Loading, _)
  | (_, (_, true, _)) =>
    Loading
  | (Error(error), _)
  | (_, (_, _, Some(error))) =>
    Error(error)
  | (Data((qsA, qsB, qsC, qsD, qsE, qsF, qsG)), (Some(qsH), _, _)) =>
    Data((qsA, qsB, qsC, qsD, qsE, qsF, qsG, qsH))
  | _ => Unknown
  }
}

Hmm, if combine8 needs to call all the other combineX functions that doesn’t seem so beneficial.
Is there any reflection possible to merge 'a and 'b if they are tuples?
Or maybe even some inline JavaScript code?

Would need to be tested, but I think this variadic JS function would work as an implementation. It’s not exactly pretty.

const combineImpl = (...results) => {
  let result;
  for (const [data, loading, error] of results) {
    if (loading) return "Loading";
    if (result === "Unknown" || result?.TAG === "Error") continue;
    if (error) result = { TAG: "Error", _0: error };
    else if (data === undefined) result = "Unknown";
    else if (result?.TAG === "Data") result._0.push(data);
    else result = { TAG: "Data", _0: [data] };
  }
  return result ?? "Unknown";
};

The issue is not really about merging the tuples but about keeping the type information, right?

And if it’s about writing those cumbersome combine functions, well, you’re going to write those only once and use it many times, so it’s likely worth it!

Yes, I have different pages in my application.
Each react component linked to a page does a different set of queries (so multiple queryResults).
And I want to show a spinner when something is loading, an error when present and then have an inner render function with all the correct unwrapped types of my query.

1 Like

To clarify, my point was simply that, putting that JS function (or something like it) in a file, you could then bind to it for all of your combine functions without having to do the runtime busywork in rescript:

@module("my-js-module")
external combine2 = (
  ReactFirehooks.Firestore.queryResult<'a>,
  ReactFirehooks.Firestore.queryResult<'b>,
) => internalQueryResult<('a, 'b)> = "combineImpl"
...
@module("my-js-module")
external combine8 = (
  ReactFirehooks.Firestore.queryResult<'a>,
  ReactFirehooks.Firestore.queryResult<'b>,
  ReactFirehooks.Firestore.queryResult<'c>,
  ReactFirehooks.Firestore.queryResult<'d>,
  ReactFirehooks.Firestore.queryResult<'e>,
  ReactFirehooks.Firestore.queryResult<'f>,
  ReactFirehooks.Firestore.queryResult<'g>,
  ReactFirehooks.Firestore.queryResult<'h>,
) => internalQueryResult<('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h)> = "combineImpl"

@module("my-js-module") @variadic
external combineN = 
  array<ReactFirehooks.Firestore.queryResult<'a>> 
  => internalQueryResult<array<'a>> = "combineImpl"
2 Likes

Thanks for pointing this out! I didn’t realize it after you posted your sample.

1 Like

If each query returns a different type it’s nearly impossible to write a generic function for any scenario to combine all of the resulting data.

But if I understand correctly, your main goal is to have one single value describing your overall state of the queries (loading/done/error). If you don’t merge your resulting data together you could write a function which takes n query results and just returns the combined state. Handling the individual results would need to be done per request, since the query result is different for every query.

For example: ReScript Playground

type firestoreError
type queryResult<'documentdata> = (option<'documentdata>, bool, option<firestoreError>)

type overallState =
  | Loading({loading: int, done: int, error: int, errors: array<firestoreError>})
  | Done
  | Error(array<firestoreError>)
type abstractQueryResult = (option<unknown>, bool, option<firestoreError>)
external toAbstractQueryResult: queryResult<'a> => abstractQueryResult = "%identity"

let overallState: array<abstractQueryResult> => overallState = queries => {
  let (lc, dc, errs) = queries->RescriptCore.Array.reduce((0, 0, ([]: array<firestoreError>)), (
    s,
    q,
  ) => {
    let (lc, dc, errs) = s
    switch q {
    | (_, true, _) => (lc + 1, dc, errs)
    | (_, false, None) => (lc, dc + 1, errs)
    | (_, _, Some(err)) =>
      errs->RescriptCore.Array.push(err)
      (lc, dc, errs)
    }
  })
  if errs->RescriptCore.Array.length > 0 {
    Error(errs)
  } else if lc > 0 {
    Loading({loading: lc, done: dc, error: errs->RescriptCore.Array.length, errors: errs})
  } else {
    Done
  }
}

module ExampleUsage = {
  module Q1 = {
    type r = {name: string, age: int}
    type t = queryResult<r>
    let result: t = (Some({name: "me", age: 99}), false, None)
  }

  module Q2 = {
    type r = {weight: int, color: string}
    type t = queryResult<r>
    let result: t = (None, true, None)
  }

  module CombinedState = {
    let overallState =
      [Q1.result->toAbstractQueryResult, Q2.result->toAbstractQueryResult]->overallState
  }

  Console.log(CombinedState.overallState)
}