Iterate over Reason Object properties and get their types

reasonreact

#1

Hello there,

I have a following problem, which I would like to solve with Reason :slight_smile:

I would like to build an abstract ReasonReact UI builder, which knows how to render bool, number, string, etc object property type into appropriate ReasonReact components.. So once I have this high-level ReasonReact UI builder, I will not have to writes renderers for each type in my app for UI.

Say, I have such type:

let personType = { alive: bool, name: string, lastname: string, age: number };

And I have a lot of instances of this type:

let person1:personType = resultOfSumFunc();
let person2:personType = resultOfSumFunc();

I would like to use ReasonReact to build UI which shows all properties of these objects (person1, person2).
For example, if object has property of type bool, then ReasonReact should render <Checkbox/> component, if number - <NumberPicker/>, string - <Label/> (component names do not matter).

Currently, I hard code property names in interface builder like this (pseudo ReasonReact code):

<Person>
   <Label value=person1.name/>
  <NumberPicker initialValue=person1.age/>
  <Checkbox label=person1.name checked=person1.alive/>
</Person>

You see, that property names are hard coded, and if I extend later my personType with another property:
let personType = { married: bool, alive: bool, name: string, lastname: string, age: number };

I will have to add to ReasonReact renderer:

<Person>
   <Label value=person1.name/>
  <NumberPicker initialValue=person1.age/>
  <Checkbox label=person1.name checked=person1.alive/>
 <Checkbox label="Married" checked=person1.married/>
</Person>

What I want to achieve:

let mainComponent = BuildReactFromObject(person1);

BuildReactFromObject just iterates over properties of person1 (of any type) instance and adds appropriate UI components to it’s render result:

(pseudo code):

render: (self) => 
    <div>
       for (let prop in self.props.instance) {
        switch prop {
           | String(s) => <Label value=s/>
           | Bool(b) => <Checkbox checkeed=b/>
        /*  ...etc */
      |> ReasonReact.arrayToElement
      }
     }
   </div>

#2

Reason/OCaml does not have runtime-accessible type information.
The way that I would probably solve this is:

// note that these cover both "access the attribute" and "update it"
type attribute('target) = 
  | Bool('target => bool, (bool, 'target) => 'target)
  | String('target => string, (string, 'target) => 'target)
  | Int('target => int, (int, 'target) => 'target);

let personType = { alive: bool, name: string, lastname: string, age: number };
let personAttributes: list((string, attribute(personType))) = [
  ("alive", Bool(p => p.alive, (alive, p) => {...p, alive})),
  ("name", Name(p => p.name, (name, p) => {...p, name})),
  ("age", Int(p => p.name, (name, p) => {...p, name})),
]

A fair sight more verbose, but here we are in static-land.
You could automate that code with a ppx (compile-time macro), but it probably wouldn’t be worth it. How often are you going to add attributes to a person anyway?


#3

Very interesting question. You can approach it in the same way as you would if you were trying to encode any arbitrary type into JSON, because you can use a very similar technique. If you look at how glennsl did it in bs-json, the technique he used is to write encoders for simple types and encoder combinators for more complex types. Essentially, build more complex encoders out of simpler ones.

The encoders are just functions for JSON, but for React components it makes sense for the encoders (let’s call them renderers) to be actual modules, i.e. components.

Let’s put this whole thing in a file called Render.re:

/* Render.re */
module type Type = {
  type t;

  let make:
    (~value: t, array(ReasonReact.reactElement)) =>
    ReasonReact.component(
      ReasonReact.stateless,
      ReasonReact.noRetainedProps,
      ReasonReact.actionless);
};

First, I’m assuming for now that all the components are stateless. Statefulness complicates thing, so I’m going to ignore it :blush:

The above module type lays out what a renderer should look like. It should know about its own type, and how to render it to a React component. To the outside world, each renderer will be made up of just these two characteristics.

module String = {
  type t = string;

  let component = ReasonReact.statelessComponent("RenderString");

  let make(~value, _) = {
    ...component,
    render: (_) => <p>(ReasonReact.stringToElement(value))</p>
  };
};

Second, we provide a built-in way to render any given string.

module Int = {
  type t = int;

  let component = ReasonReact.statelessComponent("RenderInt");

  let make(~value, _) = {
    ...component,
    render: (_) => <String value=string_of_int(value) />
  };
};

Third, we know how to render an int (just convert it to a string first).

module Tuple2(Type1: Type, Type2: Type): Type = {
  type t = (Type1.t, Type2.t);

  let component = ReasonReact.statelessComponent("RenderTuple2");

  let make(~value, _) = {
    ...component,
    render: (_) => <div>
      <Type1 value=(fst value) />
      <Type2 value=(snd value) />
    </div>
  };
};

Then, we also know how to render a pair of things, if we know how to render each of those things. That’s what the Tuple2 functor expresses. You can actually write a bunch of these functors–e.g. upto Tuple10 maybe, depending on your needs.

This technique is extensible. Anyone can create new renderers as long as they conform to the Render.Type module type. And you can render your personType by creating a renderer for it from (initially) a Tuple4 functor, e.g.:

module RenderPerson = Render.Tuple4(
  Render.Bool,
  Render.String,
  Render.String,
  Render.Int);

This person renderer, though, will only accept a 4-tuple created from the person record, in exactly the right order:

<RenderPerson value=(
  person.alive,
  person.name,
  person.lastname,
  person.age) />

It’s kinda restricted, but as Jared said, here we are in static typing land :slight_smile:

(Edit: you can of course create a custom person renderer that directly has type t = personType)


#4

Thank you for such detailed response! I like approach of bs-json, when we can make atomic decoders for each type and then combine them. I might do the same but in context of ReasonReact ‘decoders’ from my types into UI! I also think about combining this with jaredly’s answer above: each custom type will have to provide an attributeList function, which basically uses atomic ReasonReact ‘decoders’ to return its React UI components.

Something like this:

ReasonReactBuilder.buildFromAttributes = (attributes) => 
<div>
 (attributes |> List.iter((name, component) => {
        <div>
          component() //returns some ReasonReact component
        </div>
})) |> ReasonReact.arrayToElement)

Thank you for your reply! I also thought about implementing auxiliary personAttributes structure.

I wanted to reuse the same code for other objects, not only of Person Type. My REST JSON responses have very similar structure (string, number, url links, bool types) for Backend models, so I wanted to make a shortcut and render all of REST UI via one function :slight_smile: