Belt vs Pure Reason and Json

interop
bsb

#1

AFAIK, if i use a tool like bs-json, on some Json array, the result im expecting should be typed as a record list. Is that correct?

If i’m getting a record, does it make sense to us Belt or Js.Api to manipulate that record vs using Pure reason to manipulate the record list?

Should I be paying special attention to whether I type some json as Js.t({…}), Js.Json.t, string, list(array)(?), Js.Dict.t etc?


#2

You’re not forced to use record. You could have used any type I think. But I do recommend record. There’s not much helper for working with records in most libraries; a record is kind of a specific data structure. But then again, you don’t really need many helpers when working with records.


#3

Does using [@bs.obj jsConverter], i mean [@bs.deriving jsConverter] return me a record? In my mind, I feel like, after I use that, I should be able to then manipulate that record list or a record, without seeing type erros referencing Js.t or Js.Json.t, cuz its a record now. Obviously, I’m thinking about i the wrong way. What might be the correct way to think about a record that I create from some json?


#4

[@bs.deriving jsConverter] on a record type does the following:

  1. Generates a converter function from the record to its equivalent Js.t object
  2. Generates a converter function from the equivalent Js.t object to the record

It doesn’t do anything else, you’ll need to explicitly use those two functions and manually do conversions. See https://bucklescript.github.io/docs/en/generate-converters-accessors.html#convert-between-jst-object-and-record for the details :slight_smile:

In general, everything in Reason is explicit, you’ll need to think about the source and target types and use the available functions and syntax to do the conversions you need.


#5

That example, which i’ve looked at a hundred time probably, is super tiny. I’m thinking about using [@bs.deriving jsConverter] on something like this json-gist using these types and decoder gist with multiple layers and nest arrays and what not. How would you recommend I go about doing that in a sane way?

2nd question is: why does the code here throw a type error. Error is generally one of these two:

[merlin]
Error: This expression has type StringMap.t('a) = Map.Make(String).t('a)
   but an expression was expected of type
     list(FunWithFunctors.Lab.componentItem)
     
     [bucklescript]
This has type:
  StringMap.t(componentItem) (defined as Map.Make(String).t(componentItem))
But somewhere wanted:
  list(componentItem)

#6

OK first things first :slight_smile: in labJsonDecoder.re, when you get a JSON file as an external, you need to type it as Js.Json.t, not Js.t({..}), because the former tells Reason that this is a raw JSON structure to be decoded, but the latter tells Reason that it’s an already-decoded JavaScript object. Since it’s actually JSON, treating it as a JavaScript object is not type-safe because at runtime, any of the fields which you try to get out of it may not be present.

As an aside, I see you’ve defined an option map function, if you’re using a recent bs-platform you can use Belt.Option.map instead.

Another thing, normally you would use type ... and ... style when the type definitions must recursively refer to each other; in this case they don’t, so you don’t need and. Also, hover and active have the same definition–you may be able to unify them.

I see you also have something like Json.Decode.(decodeProps) in a couple of places, you don’t need the local-opened module in these cases because the names inside the parens (e.g. decodeProps) are already in scope.

Finally, the JSON decode functions, I’m seeing some errors in how they’re implemented. JSON decoder functions must all take an input of type Js.Json.t and return an output of the type according to your implementation. In your implementation I’m seeing the inputs are typed as the various record types that you’ve defined. I expect this should throw a type error immediately. I’m not confident that the error you’re getting here is the correct one because it seems to be coming from Merlin; I recommend running this in command line bsb and looking at the output.

Once you correctly type the lab.json file as a Js.Json.t, you can define a decoder for it that grabs the exact data you need out of its components field. In fact, you may not even need to define all these record types, you could just decode into Js.t objects. It really depends on if you need the specific properties of record types: immutable update, nominal typing, etc.

If you’re having trouble with writing decoders I recommend starting with a small sample JSON file, writing a decoder for it to get the intuition right, then build your way up. Ideally at some point, we’ll have JSON derivation codegen tools in Reason but right now it’s manual…


#7

Thank you so much for all the attention to reasoning about decoding and typing. Im going to go through in detail and come back.


#8

To give you a head start, the decoders should look something like this:

module Decode = {
  open Json.Decode;

  let props = json => {
    "src": field("src", string),
    "w": field("w", float)
  };

  let style = json => {
    "display": field("display", string),
    "maxWidth": field("maxWidth", string)
  };

  let component = json => {
    "name": field("name", string),
    "type": field("type", string),
    "props": field("props", props),
    "style": field("style", style),
    "examples": field("examples", array(string))
  };

  let lab = json => {
    "components": field("components", array(component)
  };
};

[@bs.module] external lab: Js.Json.t = "./labdemo/lab.json";

let lab = Decode.lab(lab);

You’ll need to adjust the field decoders because some of them are likely to be nullable, especially in the component decoder. So e.g. if a component’s style is nullable, its field decode will look like this: "style": field("style", nullable(style))


#9

Belt.Option.map is bucklescript. Is there an advantage/disadvantage to use bucklescript for manipulation or pure reason/ocaml functions? Why not variants, for example?


#10

Belt is not tied to BuckleScript, I believe there are plans to release it for native. Anything under Node and Js is tied to BuckleScript, anything else you can assume is portable (eventually).


#11

I assumed it was bucklescript since https://bucklescript.github.io/bucklescript/api/Belt.html. Is there a link explaining what Belt is?


#12

I think your link provides the answer:

A stdlib shipped with BuckleScript

Currently, it’s shipped with BuckleScript to hopefully provide a nicer dev experience than the default OCaml standard library, but it’s not tied to JavaScript specifics (that’s what I mean by ‘not tied to BuckleScript’). Anyone can split it out into a standalone library and in future it will probably be shipped as a native library on opam and/or esy.


#13

Ok. So its its ocaml/reason. Totally happy to be the dumb question asker to make a record of it for others. I think what is happening there is that, before right this second, I have not conciously engaged stdlib. I get the context now. Thank you.


#14

Ok. So following your lead in this repo decode-gist, this compiles but I feel like I cant access anything. I went ahead and tried to decode all the fields in lab.json but that doesnt seem to be the problem.

lab |> Js.log; gets me

{ name: [Function: name],
  library: [Function: library],
  layout: [Function: layout],
  components: [Function: components] }
[Function: components]

Array.length(lab##components) |> Js.log; gets me

  Warning number 44
  /Users/prisc_000/code/BHYV/decode-gist/src/DCode.re 2:3-18

  1 │ module Decode = {
  2 │   open Json.Decode;
  3 │   let props = json => {
  4 │     "src": field("src", optional(string)),

  this open statement shadows the value identifier float (which is later used)

  We've found a bug for you!
  /Users/prisc_000/code/BHYV/decode-gist/src/DCode.re 40:14-16

  38 │ let lab = Decode.lab(lab);
  39 │
  40 │ Array.length(lab) |> Js.log;
  41 │
  42 │ Array.length(lab##components) |> Js.log;

  This has type:
    {. "components": Json.Decode.decoder(array({. "examples": Json.Decode.decoder(
                                                              array(string)),
                                                 "name": Json.Decode.decoder(
                                                         string),
                                                 "props": Json.Decode.decoder(
                                                          {. "src": Json.Decode.decoder(
                                                                    option(
                                                                    string)),
                                                            "w": Json.Decode.decoder(
                                                                 float)}),
                                                 "style": Json.Decode.decoder(
                                                          {. "display":
                                                            Json.Decode.decoder(
                                                            string),
                                                            "maxWidth":
                                                            Json.Decode.decoder(
                                                            string)}),
                                                 "type": Json.Decode.decoder(
                                                         string)})),
      "layout": Json.Decode.decoder(array({. "h": Json.Decode.decoder(int),
                                            "i": Json.Decode.decoder(string),
                                            "moved": Json.Decode.decoder(
                                                     option(bool)),
                                            "name": Json.Decode.decoder(
                                                    string),
                                            "static": Json.Decode.decoder(
                                                      Js.null(bool)),
                                            "w": Json.Decode.decoder(int),
                                            "x": Json.Decode.decoder(int),
                                            "y": Json.Decode.decoder(int)})),
      "library": Json.Decode.decoder(string),
      "name": Json.Decode.decoder(string)}
  But somewhere wanted:
    array('a)

I have this doing varying level of stuff in other ways but this here is DOA for me every time. What am i missing?


#15

Ach, sorry, my fault, each of those decoders needs to take the input json as its last argument, like:

let layout = json => {
  "i": field("i", string, json),
  "name": field("name", string, json),
  "w": field("w", int, json),
  "h": field("h", int, json),
  "x": field("x", int, json),
  "y": field("y", int, json),
  "moved": field("y", optional(bool), json),
  "static": field("y", nullable(bool), json),
};

… and so on.


#16

@yawaramin this is where we are updated. I could not get nulllable syntax but works works with optional. How would I use nullable here?


#17

I see comments in this thread that Belt can potentially be used in a native project. Has anyone done this? I wasn’t able to find any info…


#18

@borisd https://github.com/bsansouci/bsb-native already supports this.


#19

Great! So I tried rtop -I node_modules/bs-platform/lib/ocaml/native/, and then #require "Belt_Result";. I get an error “No such package: Belt_Result”. I see the files belt_Result.m* in that dir. Any idea why cannot be used in rtop?


#20

This works in rtop:
Reason # #directory “node_modules/bs-platform/lib/ocaml/native”;
Reason # open Belt;