How to parse a JS Array containing multiple types


#1

Lets say I have the following Variant type:

type Fruit =
      | Apple(array(int))
      | Banana(string)
      | Orange(array((string, int))

and want to parse JS code:

[ "Apple", [1, 10, 20] ] 
[ "Banana", "taste nice" ] ]
[ "Orange", [ "sour", 5 ], ["sweet", 2] ]

What would be the best approach?
I would like to pattern match, but how do you usually deal with dynamically typed arrays like this?


#2

One way to do this would be to decode your JS code as JSON. I made a few assumptions from your snippet of JS (please correct me if I’m wrong)

  • The JS data is an array of arrays. The inner arrays are always two elements, the first a string and the second the data type of that “fruit”
  • All the second elements of the inner arrays are different

This example uses bs-json. You’ll need to understand that API first but will try to annotate what’s going on:

type fruit =
  | Apple(array(int))
  | Banana(string)
  | Orange(array((string, int)));

/*
 Json.Decode.tuple2 decodes a JSON array with two elements
 Json.Decode.string decodes a string
 Json.Decode.(array(int)) decodes an array of integers
 */
let decodeApple = Json.Decode.(tuple2(string, array(int)));

/*
 From the example above you can guess what's going on with the next two
 */
let decodeBanana = Json.Decode.(tuple2(string, string));

let decodeOrange = Json.Decode.(tuple2(string, array(tuple2(string, int))));

/*
 Json.Decode.oneOf will try each decoder and give you back the first
 one that succeeds.

 Json.Decode.map will map the result of the successful decode. We need
 this to map it into the fruit type. Notice how we only care about the
 second part of the tuple
 */
let decode =
  Json.Decode.oneOf([
    decodeApple |> Json.Decode.map(x => Apple(snd(x))),
    decodeBanana |> Json.Decode.map(x => Banana(snd(x))),
    decodeOrange |> Json.Decode.map(x => Orange(snd(x))),
  ]);

let json = {|
  [
    ["Apple", [1, 10, 20]],
    ["Banana", "taste nice"],
    ["Orange", [
            ["sour", 5],
            ["sweet", 2]
    ]]
  ]
|};

let fruits = Json.parseOrRaise(json) |> Json.Decode.array(decode);
/*
 fruits should be
 [|
   Apple([|1, 10, 20|]),
   Banana("taste nice"),
   Orange([|("sour", 5), ("sweet", 2)|]),
 |]
 */

#3

Thanks for taking the time to answer my question,
is there any other way with a direct conversion rather than Js -> Json -> Reasonml?

On the bucklescript docs I could only find a way to convert a Js object to a type: https://bucklescript.github.io/docs/en/generate-converters-accessors.html#convert-between-jst-object-and-record

My specific use case requires a good performance (im tying to learn reasonml by building a frontend for Neovim)


#4

Hey! Sorry, I couldn’t think of another way to convert the array from your snippet into that type without doing some other sort of runtime check and making sure you handle the possible errors. But at that point I’d just use Json :slight_smile: And I’m definitely curious about if there is a way!