recordreasonbucklescriptrescript

What is the best practice of iterating record keys and values in Reasonml?


I'm new to ReasonML, but I read through most of the official documents. I could go through the casual trial and errors for this, but since I need to write codes in ReasonML right now, I'd like to know the best practices of iterating keys and values of reason record types.


Solution

  • I fully agree with @Shawn that you should use a more appropriate data structure. A list of tuples, for example, is a nice and easy way to pass in a user-defined set of homogeneous key/value pairs:

    fooOnThis([
      ("test1", ["a", "b", "c"]),
      ("test2", ["c"]),
    ])
    

    If you need heterogeneous data I would suggest using a variant to specify the data type:

    type data =
      | String(string)
      | KvPairs(list((string, data)));
    
    fooOnThis([
      ("test1", [String("a"), String("b"), String("c")]),
      ("test2", [String("c"), KvPairs([("innerTest", "d")])]),
    ])
    

    Alternatively you can use objects instead of records, which seems like what you actually want.

    For the record, a record requires a pre-defined record type:

    type record = {
      foo: int,
      bar: string,
    };
    

    and this is how you construct them:

    let value = {
      foo: 42,
      bar: "baz",
    };
    

    Objects on the other hand are structurally typed, meaning they don't require a pre-defined type, and you construct them slightly differently:

    let value
      : {. "foo": int, "bar": string }
      = {"foo": 42, "bar": "baz"};
    

    Notice that the keys are strings.

    With objects you can use Js.Obj.keys to get the keys:

    let keys = Js.Obj.keys(value); // returns [|"foo", "bar"|]
    

    The problem now is getting the values. There is no Js.Obj API for getting the values or entries because it would either be unsound or very impractical. To demonstrate that, let's try making it ourselves.

    We can easily write our own binding to Object.entries:

    [@bs.val] external entries: Js.t({..}) => array((string, _)) = "Object.entries";
    

    entries here is a function that takes any object and returns an array of tuples with string keys and values of a type that will be inferred based on how we use them. This is neither safe, because we don't know what the actual value types are, or particularly practical as it will be homogeneously typed. For example:

    let fields = entries({"foo": 42, "bar": "baz"});
    
    // This will infer the value's type as an `int`
    switch (fields) {
    | [|("foo", value), _|] => value + 2
    | _ => 0
    };
    
    // This will infer the value's type as an `string`, and yield a type error
    // because `fields` can't be typed to hold both `int`s and `string`s
    switch (fields) {
    | [|("foo", value), _|] => value ++ "2"
    | _ => ""
    };
    

    You can use either of these switch expressions (with unexpected results and possible crashes at runtime), but not both together as there is no unboxed string | int type to be inferred in Reason.

    To get around this we can make the value an abstract type and use Js.Types.classify to safely get the actual underlying data type, akin to using typeof in JavaScript:

    type value;
    
    [@bs.val] external entries: Js.t({..}) => array((string, value)) = "Object.entries";
    
    let fields = entries({"foo": 42, "bar": "baz"});
    
    switch (fields) {
    | [|("foo", value), _|] =>
      switch (Js.Types.classify(value)) {
      | JSString(str) => str
      | JSNumber(number) => Js.Float.toString(number)
      | _ => "unknown"
      }
    | _ => "unknown"
    };
    

    This is completely safe but, as you can see, not very practical.

    Finally, we can actually modify this slightly to use it safely with records as well, by relying on the fact that records are represented internally as JavaScript objects. All we need to do is not restrict entries to objects:

    [@bs.val] external entries: 'a => array((string, value)) = "Object.entries";
    
    let fields = keys({foo: 42, bar: 24}); // returns [|("foo", 42), ("bar", 24)|]
    

    This is still safe because all values are objects in JavaScript and we don't make any assumptions about the type of the values. If we try to use this with a primitive type we'll just get an empty array, and if we try to use it with an array we'll get the indexes as keys.

    But because records need to be pre-defined this isn't going to be very useful. So all this said, I still suggest going with the list of tuples.

    Note: This uses ReasonML syntax since that's what you asked for, but refers to the ReScript documentation, which uses the slightly different ReScript syntax, since the BuckleScript documentation has been taken down (Yeah it's a mess right now, I know. Hopefully it'll improve eventually.)