Tight variant types as external APIs

reasonreact

#1

Suppose I’m writing a useFetch hook and want to encode the result is returns so that it makes impossible states irrepresentable. Something like:

type t('d, 'e) =
  | Fetching
  | Complete(result('d, [> fetchError] as 'e));
  | Refetching(result('d, [> fetchError] as 'e));

So then the consumer has to go:

[@react.component]
let make = () => {
  UseFetch.(
    useFetch(
      "https://api.github.com/search/repositories?q=language:reason&sort=stars&order=desc",
    )
    ->(
        fun
        | Fetching
        | Refetching(_) => ReasonReact.string("Loading...")
        | Complete(Ok(({items}: GhRepo.t))) =>
          <ul>
            {Belt.Array.map(items, ({fullName, htmlUrl}: GhRepo.repo) =>
               <li key=fullName>
                 <a href=htmlUrl> {ReasonReact.string(fullName)} </a>
               </li>
             )
             ->React.array}
          </ul>
        | Complete(Error(`FetchError(_))) =>
          <div> {ReasonReact.string("Fetch error!")} </div>
      )
  );
};

Or something like that, depending on how you handle errors/refetches in your app. Sure, you can get closer to the good old {loading, data, error} by providing some helpers, but that’s more code, and anyway, is the whole thing even worth it in cases like this?

I mean, arguably the bigger pro of sum types and exhaustive matches is that they get your back during refactoring: you add or remove a variant and than compiler doesn’t stop yelling until you fix every match expression you have. But in case of a well-defined and ubiquitously-used component/hook that queries data, what are the chances that the variant will ever change?

And even if having both some data and an error is an impossible state, should it concern a consumer? Because if, say, UseFetch provides some helpers like isLoading and getError, and the implementation is wrong, the consumer can technically end up with some impossible UI too, only it’ll take more code.

So I kinda see the point of using that or similar variant type internally, but what do you guys think of it as of an external API? Isn’t it overcomplicating things?


#2

I personally don’t think it’s over complicating, it’s more correct and prevents the api consumer from making mistakes in a clean way.
BTW, relude provides a similar variant, I use it (and also IO.async) to wrap fetch requests.
https://reazen.github.io/relude/#/api/AsyncData
https://reazen.github.io/relude/#/api/AsyncResult


#3

The Relude version is based on Elm RemoteData (and its PureScript counterpart), but as you noticed @hoichi, there seems to be value in reusing the result type to represent failure, and “refetching” is a useful concept that is distinct from the initial fetch (so you can choose to render existing data while loading). And we have some render utilities in relude-reason-react to help produce views from this state (basically doing what your pattern match does).

So yeah, definitely of the opinion that this is a useful pattern for modeling async state! :slight_smile:


#4

Yeah, AsyncResult was certainly one of my inspirations :slight_smile: