A few small questions

1. I don’t understand how process.env works for ReScript - can someone help me with this? How could I, for example, do: let secret = process.env.secret

2. What does {.} and {..} mean?

3. Is async/await syntax planned?

4. How do I interpret the error messages? I get a lot of “Somewhere wanted”. Why is it saying that? Does it not know the linenumber or have I configured something incorrectly? In general, the error messages are somewhat lacking.

5. (Emacs specific). The error messages using rescript-mode with lsp and vscode-rescript are cut off - only the first line is displayed. Is this a bug or a configurational issue?

PS. I don’t know how you guys usually address questions like these so please inform me if I should do one post per question or such ahead for searchability.

  1. Here’s an example of how to use it:
switch Node.Process.process["env"]->Js.Dict.get("secret") {
  | Some(secret) => ...
  | None => ...
}
  1. {.} is the type of an empty object. {..} is the type of an object with an unspecified number of fields.

  2. Yes, it is being worked on.

  3. You can read that as 'expected type t1, but got type t2'. ‘Somewhere wanted’ wording is used because due to type inference, the compiler is not sure which usage is incorrect. For example, if you do:

let x = 1 + "2"

You get the error:

E] Line 1, column 12:
This has type: string
  Somewhere wanted: int
  
  You can convert string to int with Belt.Int.fromString.

But is the correct fix to change "2" to 2? Or to change the expression to "1" ++ "2" i.e. addition of two strings? It depends on the developer’s intention, the compiler doesn’t know.

Thank you.

As for {..}, it seems to require a type parameter whenever I used it in a type - what is that? How is it meant to be used?

I wanted to ask about the “somewhere wanted” wording as well. It’s quite incongruous. It looks like the compiler is unfinished, someone left a placeholder while they implement suitable tracking of the context. Is it really intentional and not planned to be made more specific? The message is already taking a stance on which type is correct (in the above example it’s heavily suggesting string should be changed to int) so I don’t buy that it’s helpfully agnostic.

“somewhere wanted” is a friendlier version of the OCaml error message, which for @yawaramin’s example is quite clinical:

Error: This expression has type string but an expression was expected of type
         int

I’m sure suggestions to improve it would be welcome.

For more well-typed NodeJS bindings, try rescript-nodejs. It has NodeJs.Process.env (the ["env"] style access not type safe at all).

1 Like

Actually now I’m realising that if “somewhere wanted” was simply “expected”, I wouldn’t have batted an eyelid. It’s just that it brings attention to its vagueness in an odd way.

It was an attempt to make the error message friendlier, but there’s always room for improvement. Perhaps you can file an issue suggesting a better message.

It is known as an ‘open object type’, and is actually a generic type due to the fact that it has an unspecified set of fields. Therefore when you try to give it a name, you need to give it a type parameter as well as part of the type name:

type obj<'a> = {..} as 'a

You can also specify a minimum set of fields that the object type must contain, e.g.:

type atLeastA<'a> = {.. "a": int} as 'a

These types are mostly convenient and useful as part of function types where you don’t have to explicitly name them, e.g.:

let fullName: {.. "firstName": string, "lastName": string} => string = obj =>
  obj["firstName"] ++ obj["lastName"]

Sde note, I would normally put the annotation in the interface file, not directly in the implementation.

1 Like

Thank you so much for your replies. I think there’s some documentation missing re. language features? Have they been deprecated? Or maybe they are too esoteric to even document?

What does let () = ... mean and how can it be used?

This is something I see now and then which looks entirely weird to me - assigning to the unit value!?

PS. I’ll keep the questions bigger to make this thread easier to scan for readers.

EDIT: Oh, and one more:

I see a a lot of files named in the style of Data_User.res in ReScript projects. Is this a sort of convention for file naming in ReScript?

Alright, so questions keep popping up:

What does it mean when a signature looks like this: (string, int, unit) => .... I don’t understand why the function takes a unit type as its last argument.

I have a function in JS that requires me to send the JS string type/constructor String. How can I do that from ReScript?

Sorry I’m shooting questions all over the place. I’m also reading the language docs but I’m unable to find answers to these specific questions there.

EDIT: But wait, there is more.

How can I know when a function is going to be autocurried? It almost seems to happen randomly which is causing a lot of problems for me during runtime (functions just not being invoked).

Don’t think of let as “assigning” a value (although it does do that), but think of it as destructuring a pattern. For example, you’re probably familiar with this: let {a, b} = ... which destructures a record and assigns its fields to bindings a and b. You can actually do the same thing with any kind of pattern, including variants:

type t = A(int) | B(int)
let A(x) | B(x) = foo

This will bind an int to the name x. Note that this is only possible if the patterns after let are exhaustive and contain all of the same variable names with the same types. Basically, it’s like syntax sugar for this:

type t = A(int) | B(int)
switch foo {
  | A(x) | B(x) => ...
}

With this in mind, consider that unit is technically just a variant with a single constructor, (). That means you can “destructure” it like any variant. In fact, we can make our own "unit’ type and use it exactly the same way:

type t = Unit
let ignore = _ => Unit
let Unit = ignore(1)

Although note that let () = ... is not usually necessary, since the ReScript compiler allows statements that return unit to not need a let. (OCaml, which ReScript is forked from, is stricter and requires it.) But sometimes people use let () = ... anyway just to be explicit.

Since all files are compiled as top-level modules, it’s common to prefix them as a kind of namespace if you have a lot of similar modules.

It’s hard to say why without seeing the actual function. Usually there’s a specific reason for it. The most common reason is that the function has optional named arguments (~arg=?). The compiler needs a positional argument to know when the function is fully applied and when it’s being curried. A final unit argument is conventional for that.

I’m not sure if there’s a built-in way to pass around JS constructors, but you can make a quick-and-dirty external:

type js_string
@val external js_string: js_string = "String"
let x = js_string

Someone else may have a more elegant solution, though.

There’s no bulletproof way, unfortunately. Sometimes the typechecker will alert you. For example, if it says it expected type int but received type string => int then that indicates that you forgot to apply a string argument. However, there are cases where that doesn’t get covered. Again, someone else may have more advice about this.

3 Likes

Can you give me an example?

I tried doing bindings to MongoDB but they were not invoked. I have redone them a few times (to learn how to do things properly). It seems they sometimes are invoked and sometimes aren’t. I didn’t save the code for each attempt, so it is of course likely or even definite that I am the cause - I am doing something wrong.

I might have put the wrong number of arguments in each binding, I don’t know. What I did find out was that the function did not get invoked, and I had to use dot-syntax to uncurry it and get it to execute. I think the confusion here is that ReScript is trying to be smart in its autocurrying. For a not-so-smart user, myself, it turns out to be confusing.

It’s difficult to answer without seeing a concrete example.

Thank you for an amazing post - this clarifies so much for me!

@yawaramin thanks but it’s not a particular case I need to understand, it’s the ability to understand when currying will be done and when it won’t be done on a more intuitive basis. I guess this will come with time, and there’s another thread dealing with this so I’ll leave it there. Thank you though - all of you are very helpful here so far.

The only robust way is to use uncurried types. Even if you know what the compiler does today, it might change tomorrow.

1 Like

How do I do this? By using the dot syntax everywhere?

Yes dot syntax everywhere you want this guarantee.

Ok, is that a common way of doing ReScript programming? It comes across as tedious and messy to me.