A few small questions

Thanks, though this one I can figure out because it’s a rather trivial example. What if I have a, b, c, d, e where a, c and d are async and b, c depends on a, d depends on c and e depends on d? I’m not being facetious here - i feel as if figuring this out is a necessary next step to be able to use ReScript comfortable.

If let would be absolutely adequate for this as well. But we don’t have that either. I’m curious, from a maturity standpoint, where ReScript is considered to be on some sort of scale. There are a few vital things that I feel are missing.

A good rule of thumb is to listen to what issues are reported frequently by community member for the purpose of prioritising.
This isn’t one.

ReScript is essentially OCaml, so I think it boils down to how mature you consider OCaml.

Would still be interesting to see an example!

Thanks - this tells me that I am misunderstanding something about how to use ReScript in an idiomatic manner. No matter how I reason about it though, I feel that there is a high threshold. Not in the language in and of itself, necessarily, but e.g., bindings. Thinking back to 2016, this was the same in JS with NPM - I remember everything breaking week to week and extremely poor documentation.

Well, imagine

function myfn(arg) {
  if (!arg) {
    return null
  }

  // 30 lines of code
}

As I understand it, the way to do it in ReScript:

let myfn = (arg) => {
  switch arg {
    | None => None
    | Some(value) => {
      // 30 lines of code
    }
  }
}

This has a rather nasty side-effect of affecting the indentation of the entire function which feels really out-of-place to me. I’m not trying to be nitpicking here, please understand that it’s a process for me to wrap my head around the OCaml way of doing things!

Bindings are indeed one of the most frequently mentioned aspects in the context of pain points.

phil [another] pattern for that is to use Option.map

let myfnImpl = (value) => // 30 lines of code

let myfn = Option.map(mfnImpl)
3 Likes

Yeah bindings is definitely still a sore spot.

I read in some other thread that bindings are more an art form than science. I think I understand the sort of idea that the author was getting at (don’t remember who wrote it). However, is it sort of impossible to do a good 1:1 mapping in the sense that one set of TypeScript decl’s can be mapped into a particular set of ReScript bindings that would work generally for ReScript code, or do bindings in fact have to be adapted to the code base they are to be used in for them to make any real sense?

I saw this other thread about involving an AI for doing bindings - I like the idea of it though I would also like to reason about the feasibility of using some sort of correctness approach for coming up with THE set of bindings, given a set of TypeScript decl’s.

ultimately its impossible to do 1:1 bindings for typescript since typescript covers all of javascript and rescript does not? [edit: Theres more than one way to do things in both languages as well, so there will always be choice]

Not sure if you’re still wanting a way around this, but you can use this trick (link to Jane Street tech blog) to work on the interface for a while before starting to work on the implementation.

Here is their example converted to rescript for an example:

// Or put in StackIntf.res
module StackIntf = {
  module type S = {
    type t<'a>
    let empty: t<'a>
    let push: (t<'a>, 'a) => t<'a>
    let pop: t<'a> => option<(t<'a>, 'a)>
  }
}

// Or put in Stack.resi
module Stack: {
  include StackIntf.S
} = // Or put in Stack.res
{
  include unpack(failwith("unimplemented"): StackIntf.S)
}

Regarding indentation, flatmap can help flatten out the indentation. Not as clean as let! and F# computation expressions but not too bad.

// Get and add up x, y, z, q
// If any number is None, stop
let addNums = fetchNum =>
  fetchNum()
  ->Option.flatMap(x => fetchNum()->Option.map(y => {"x": x, "y": y}))
  ->Option.flatMap(xy => fetchNum()->Option.map(z => {"x": xy["x"], "y": xy["y"], "z": z}))
  ->Option.flatMap(xyz =>
    fetchNum()->Option.map(q => {"x": xyz["x"], "y": xyz["y"], "z": xyz["z"], "q": q})
  )
  ->Option.map(i => {
    let print = (label, value) => Js.log(`${label}: ${value->Js.Int.toString}`)
    print("x", i["x"])
    print("y", i["y"])
    print("z", i["z"])
    print("q", i["q"])
    print("sum", i["x"] + i["y"] + i["z"] + i["q"])
  })

1 Like

I’m assuming that you have a tricky real-world example in mind, but without knowing about it, just as general strategy, you could write “simpler” functions and lift those into the context you need (async, error, option, whatever).

Let’s just make some example functions to try to represent your example…you could imagine that any of the promise returning functions are hitting some external api or something like that. (Note: using thenResolve from rescript-core).

// Start of your pipeline.  This represents the 'a' from your example.
let getNumber = async n => {
  // api call here
  n
}

// take 'a' return 'b'
let add10 = n => n + 10

// take 'a' return 'c' promise
let adderService = async n => {
  // api call here
  n + 100
}

// take 'c' => return optional 'd' as a promise 
// (let's make it optional just for fun)
let maybeAddService = async n =>
  if n < 0 {
    None
  } else {
    // api call here
    Some(n + 1000)
  }

// finally, takes your 'd' value and returns an 'e', working directly rather than
// with the option, so we will need to "lift" this function to work with option.
let minusFive = n => n - 5

// pipeline going all the way from a to e
let pipeline = n => {
  let a = getNumber(n)

  let b = Promise.thenResolve(a, add10)
  // standin for doing something with b
  b->ignore

  let c = Promise.then(a, adderService)
  let d = Promise.then(c, maybeAddService)
  let e = Promise.thenResolve(d, Option.map(_, minusFive))

  e
}

// same, but writing it with the pipeline operator
let pipeline' = n => {
  let a = getNumber(n)

  Promise.thenResolve(a, add10)->ignore

  a
  ->Promise.then(adderService)
  ->Promise.then(maybeAddService)
  ->Promise.thenResolve(Option.map(_, minusFive))
}

let print = async v => {
  let v = await v
  Console.log(v)
}

let a = pipeline(-1000)->print
let b = pipeline'(1)->print

Running that little script with node prints out

undefined
1096

Okay, this is a silly example, I know. But up a few posts back people were talking about thinking through the types (or however it was said)…it’s doing the same thing here. Write your tiny data-transforming functions, and then lift them into the proper context. In this case it was promises and options, but it could be whatever.

2 Likes

Playing off jmagaram’s nice example of adding up numbers, but needed to have the “fetching” of the numbers sequenced…you could imagine something like this:

// dummy function
let fetchNum = n => Some(n)

// what we want to achieve once we have all the numbers
let f = (x, y, z, q) => x + y + z + q

let result =
  fetchNum(1)->Option.flatMap(x =>
    fetchNum(2)->Option.flatMap(y =>
      fetchNum(3)->Option.flatMap(z => fetchNum(4)->Option.map(_, f(x, y, z)))
    )
  )

Of course, that still has the problem with ever-increasing indentation in the callbacks…so, just extract what’s really going on (tallying a sum if numbers are there or not)…we pull out that key operation into a little function of its own, with arguments chosen to make it play nice with partial application and flatMap.

// this says:  
//   if x is Some number, then add it to y
//
// note that if you use partial application
//   let f = add(Some(1))
//   you get f: int => option<int>, which is exactly what flatMap needs
let add: (option<int>, int) => option<int> 
  = (x, y) => Option.map(x, x => x + y)

let result' = {
  fetchNum(1)
  ->Option.flatMap(add(fetchNum(2)))
  ->Option.flatMap(add(fetchNum(3)))
  ->Option.flatMap(add(fetchNum(4)))
}

Again, your real examples are surely trickier than this…but hopefully it helps to give you an idea of working within the context that you need.

Edit: you mention this…

I think this is one of the cases (your questions about sequencing within a context/monad/whatever) where rescript language is sort of pushing you do structure your code a little different than ocaml. Eg, in ocaml, I would use the let binding operators for the sequencing to make it look nice (or even maybe the infix operator >>= and >>| for the bind/map. But those aren’t available in rescript, and yet, the increasing indentation and callbacks isn’t ideal, so you could work around it like above…extracting little functions and sequencing them just right with functions to lift them into the correct context.

2 Likes

I think at this point the readability just starts suffering too much. Without some special do syntax support I’d prefer to just lean into a local exception

exception Bail

let getOrBail = opt =>
  switch opt {
  | Some(x) => x
  | None => raise(Bail)
  }

// Get and add up x, y, z, q
// If any number is None, stop
let addNums = fetchNum => 
  try {
    let x = fetchNum()->getOrBail
    let y = fetchNum()->getOrBail
    let z = fetchNum()->getOrBail
    let q = fetchNum()->getOrBail

    let print = (label, value) => Js.log(`${label}: ${value->Js.Int.toString}`)
    print("x", x)
    print("y", y)
    print("z", z)
    print("q", q)
    print("sum", x + y + z + q)
  } catch {
  | Bail => ()
  }
3 Likes

Thank you for very educational posts - these really help me move ahead.

Another one:

What does “this argument will not be used in the function” mean?

I understand the sentence, but I don’t understand it in the context that I am getting it:

            let el = Obj.magic(el)
            el["style"]["transform"] = "scale(2)" // warning here

I’m trying to use rescript-webapi by the way (but I don’t think that’s related to the warning - but maybe you can give me pointers on how to better the particular task above)

Can you show a minimal reproduction example with original error message from the compiler? Difficult to place this message in context without more info.

Why doesn’t type inference work in this scenario?

module M = {
  type t = {foo: string}
}

let a = []
let b: array<M.t> = a->Array.map(x => {foo: x})

It says the record field foo can’t be found. But I just gave it the context of M.t. This works:

let b = a->Array.map((x): M.t => {foo: x})

I understand why that works but it is way less intuitive to look at.

Record fields are found based on being scope in the current module. If the field is defined in a nested module, then it is not in scope in the current module. You can follow the instructions in the error message to fix the error:

let b = a->Array.map(x => {M.foo: x})

Type annotations are not required.

1 Like