Skip to content

Commit

Permalink
Merge pull request #750 from rescript-association/uncurried-docs
Browse files Browse the repository at this point in the history
Remove uncurried chapter and update existing docs to uncurried
  • Loading branch information
fhammerschmidt authored Nov 15, 2023
2 parents b28de11 + f1b025f commit 4d7cc30
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 261 deletions.
1 change: 0 additions & 1 deletion data/sidebar_manual_latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
"import-export",
"attribute",
"unboxed",
"uncurried-mode",
"reserved-keywords"
],
"Advanced Features": [
Expand Down
22 changes: 22 additions & 0 deletions misc_docs/syntax/decorator_uncurried.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
id: "uncurried-decorator"
keywords: ["uncurried", "decorator"]
name: "@@uncurried"
summary: "This is the `@@uncurried` decorator."
category: "decorators"
---

If you have uncurried mode turned off in `rescript.json` and still want to try it on a per-file basis, you can turn it on via

```rescript
@@uncurried
```

at the top of a `.res` file.

_Available since ReScript `11.0.0`._

### References

- [Uncurried Mode blogpost](/blog/uncurried-mode)
- [Build System configuration](/docs/manual/latest/build-configuration#uncurried)
12 changes: 0 additions & 12 deletions pages/docs/manual/latest/async-await.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ canonical: "/docs/manual/latest/async-await"

# Async / Await

**Since 10.1**

ReScript comes with `async` / `await` support to make asynchronous, `Promise` based code easier to read and write. This feature is very similar to its JS equivalent, so if you are already familiar with JS' `async` / `await`, you will feel right at home.

## How it looks
Expand Down Expand Up @@ -113,16 +111,6 @@ let fetchData: string => promise<string> = async (userId: string): string {

**Note:** In a practical scenario you'd either use a type signature, or inline types, not both at the same time. In case you are interested in the design decisions, check out [this discussion](https://github.com/rescript-lang/rescript-compiler/pull/5913#issuecomment-1359003870).

### `async` uncurried functions

The `async` keyword does also work for uncurried functions.

```res
let fetchData = async (. userId: string): string {
await fetchUserMail(userId)
}
```

### Promises don't auto-collapse in async functions

In JS, nested promises (i.e. `promise<promise<'a>>`) will automatically collapse into a flat promise (`promise<'a>`). This is not the case in ReScript. Use the `await` function to manually unwrap any nested promises within an `async` function instead.
Expand Down
91 changes: 6 additions & 85 deletions pages/docs/manual/latest/bind-to-js-function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ It'd be nice if on ReScript's side, we can bind & call `draw` while labeling thi

```res example
@module("MyGame")
external draw: (~x: int, ~y: int, ~border: bool=?, unit) => unit = "draw"
external draw: (~x: int, ~y: int, ~border: bool=?) => unit = "draw"
draw(~x=10, ~y=20, ~border=true, ())
draw(~x=10, ~y=20, ())
draw(~x=10, ~y=20, ~border=true)
draw(~x=10, ~y=20)
```
```js
var MyGame = require("MyGame");
Expand All @@ -60,18 +60,16 @@ MyGame.draw(10, 20, undefined);

We've compiled to the same function, but now the usage is much clearer on the ReScript side thanks to labels!

**Note**: in this particular case, you need a unit, `()` after `border`, since `border` is an [optional argument at the last position](function.md#optional-labeled-arguments). Not having a unit to indicate you've finished applying the function would generate a warning.

Note that you can freely reorder the labels on the ReScript side; they'll always correctly appear in their declaration order in the JavaScript output:

<CodeTab labels={["ReScript", "JS Output"]}>

```res example
@module("MyGame")
external draw: (~x: int, ~y: int, ~border: bool=?, unit) => unit = "draw"
external draw: (~x: int, ~y: int, ~border: bool=?) => unit = "draw"
draw(~x=10, ~y=20, ())
draw(~y=20, ~x=10, ())
draw(~x=10, ~y=20)
draw(~y=20, ~x=10)
```
```js
var MyGame = require("MyGame");
Expand Down Expand Up @@ -348,83 +346,6 @@ doSomething("test");

**Note:** It's a pretty niche feature, mostly used to map to polymorphic JS APIs.


## Curry & Uncurry

Curry is a delicious Indian dish. More importantly, in the context of ReScript (and functional programming in general), currying means that function taking multiple arguments can be applied a few arguments at time, until all the arguments are applied.

See the `addFive` intermediate function? `add` takes in 3 arguments but received only 1. It's interpreted as "currying" the argument `5` and waiting for the next 2 arguments to be applied later on. Type signatures:

```res sig
let add: (int, int, int) => int
let addFive: (int, int) => int
let twelve: int
```

(In a dynamic language such as JS, currying would be dangerous, since accidentally forgetting to pass an argument doesn't error at compile time).

### Drawback

Unfortunately, due to JS not having currying because of the aforementioned reason, it's hard for ReScript multi-argument functions to map cleanly to JS functions 100% of the time:

1. When all the arguments of a function are supplied (aka no currying), ReScript does its best to to compile e.g. a 3-arguments call into a plain JS call with 3 arguments.

2. If it's too hard to detect whether a function application is complete\*, ReScript will use a runtime mechanism (the `Curry` module) to curry as many args as we can and check whether the result is fully applied.

3. Some JS APIs like `throttle`, `debounce` and `promise` might mess with context, aka use the function `bind` mechanism, carry around `this`, etc. Such implementation clashes with the previous currying logic.

\* If the call site is typed as having 3 arguments, we sometimes don't know whether it's a function that's being curried, or if the original one indeed has only 3 arguments.

ReScript tries to do #1 as much as it can. Even when it bails and uses #2's currying mechanism, it's usually harmless.

**However**, if you encounter #3, heuristics are not good enough: you need a guaranteed way of fully applying a function, without intermediate currying steps. We provide such guarantee through the use of the ["uncurrying" syntax](./function#uncurried-function) on a function declaration & call site.

### Solution: Use Guaranteed Uncurrying

[Uncurried function](function.md#uncurried-function) annotation also works on `external`:

<CodeTab labels={["ReScript", "JS Output"]}>

```res example
type timerId
@val external setTimeout: ((. unit) => unit, int) => timerId = "setTimeout"
let id = setTimeout((.) => Js.log("hello"), 1000)
```
```js
var id = setTimeout(function () {
console.log("hello");
}, 1000);
```

</CodeTab>

#### Extra Solution

The above solution is safe, guaranteed, and performant, but sometimes visually a little burdensome. We provide an alternative solution if:

- you're using `external`
- the `external` function takes in an argument that's another function
- you want the user **not** to need to annotate the call sites with the dot

<!-- TODO: is this up-to-date info? -->

Then try `@uncurry`:

<CodeTab labels={["ReScript", "JS Output"]}>

```res example
@send external map: (array<'a>, @uncurry ('a => 'b)) => array<'b> = "map"
map([1, 2, 3], x => x + 1)
```
```js
// Empty output
```

</CodeTab>

In general, `uncurry` is recommended; the compiler will do lots of optimizations to resolve the currying to uncurrying at compile time. However, there are some cases the compiler can't optimize it. In these cases, it will be converted to a runtime check.

## Modeling `this`-based Callbacks

Many JS libraries have callbacks which rely on this (the source), for example:
Expand Down
41 changes: 26 additions & 15 deletions pages/docs/manual/latest/build-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ canonical: "/docs/manual/latest/build-configuration"

# Configuration

`rescript.json` (or `rescript.json` in versions prior ReScript 11) is the single, mandatory build meta file needed for `rescript`.
`rescript.json` (or `bsconfig.json` in versions prior ReScript 11) is the single, mandatory build meta file needed for `rescript`.

**The complete configuration schema is [here](./build-configuration-schema)**. We'll _non-exhaustively_ highlight the important parts in prose below.

Expand Down Expand Up @@ -96,19 +96,6 @@ This is useful for working on multiple independent ReScript packages simultaneou

More details can be found on our [external stdlib](./build-external-stdlib) page.

## reason, refmt (old)

`reason` config is enabled by default. To turn on JSX for [ReasonReact](https://reasonml.github.io/reason-react/), specify:

```json
{
"reason": {"react-jsx": 3},
"refmt": 3
}
```

The `refmt` config **should be explicitly specified** as `3`.

## js-post-build

Hook that's invoked every time a file is recompiled. Good for JS build system interop, but please use it **sparingly**. Calling your custom command for every recompiled file slows down your build and worsens the building experience for even third-party users of your lib.
Expand Down Expand Up @@ -152,7 +139,19 @@ This configuration only applies to you, when you develop the project. When the p

## suffix

Either `".js"`, `".mjs"`, `".cjs"` or `".bs.js"`. Currently prefer `bs.js` for now.
**Since 11.0**: The suffix can now be freely chosen. However, we still suggest you stick to the convention and use
one of the following:
- `".js`
- `".mjs"`
- `".cjs"`
- `".res.js"`
- `".res.mjs"`
- `".res.cjs"`
- `".bs.js"`
- `".bs.mjs"`
- `".bs.cjs"`

Currently prefer `.bs.js` for now.

### Design Decisions

Expand All @@ -163,6 +162,18 @@ Generating JS files with the `.bs.js` suffix means that, on the JS side, you can
- It avoids the need of using a build system loader for ReScript files. This + in-source build means integrating a ReScript project into your pure JS codebase **basically doesn't touch anything in your build pipeline at all**.
- [genType](/docs/gentype/latest/introduction) requires `bs.js` for compiled JS artifacts. If you are using `genType`, you need to use `bs.js` for now.

## uncurried

**Since 11.0**: While we strongly encourage all users to use uncurried mode, it is still possible to opt out. Just set `"uncurried"` to `false` to get the old behavior back:

```json
{
"uncurried": false
}
```

More details can be found in the [blogpost about "Uncurried Mode"](/blog/uncurried-mode).

## warnings

Selectively turn on/off certain warnings and/or turn them into hard errors. Example:
Expand Down
51 changes: 36 additions & 15 deletions pages/docs/manual/latest/function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Labeled function arguments can be made optional during declaration. You can then

```res
// radius can be omitted
let drawCircle = (~color, ~radius=?, ()) => {
let drawCircle = (~color, ~radius=?) => {
setColor(color)
switch radius {
| None => startAt(1, 1)
Expand All @@ -191,7 +191,7 @@ let drawCircle = (~color, ~radius=?, ()) => {
```js
var Caml_option = require("./stdlib/caml_option.js");

function drawCircle(color, radius, param) {
function drawCircle(color, radius) {
setColor(color);
if (radius === undefined) {
return startAt(1, 1);
Expand All @@ -207,8 +207,6 @@ When given in this syntax, `radius` is **wrapped** in the standard library's `op

More on `option` type [here](null-undefined-option.md).

**Note** for the sake of the type system, whenever you have an optional argument, you need to ensure that there's also at least one positional argument (aka non-labeled, non-optional argument) after it. If there's none, provide a dummy `unit` (aka `()`) argument.

### Signatures and Type Annotations

Functions with optional labeled arguments can be confusing when it comes to signature and type annotations. Indeed, the type of an optional labeled argument looks different depending on whether you're calling the function, or working inside the function body. Outside the function, a raw value is either passed in (`int`, for example), or left off entirely. Inside the function, the parameter is always there, but its value is an option (`option<int>`). This means that the type signature is different, depending on whether you're writing out the function type, or the parameter type annotation. The first being a raw value, and the second being an option.
Expand All @@ -218,8 +216,8 @@ If we get back to our previous example and both add a signature and type annotat
<CodeTab labels={["ReScript", "JS Output"]}>

```res
let drawCircle: (~color: color, ~radius: int=?, unit) => unit =
(~color: color, ~radius: option<int>=?, ()) => {
let drawCircle: (~color: color, ~radius: int=?) => unit =
(~color: color, ~radius: option<int>=?) => {
setColor(color)
switch radius {
| None => startAt(1, 1)
Expand All @@ -228,7 +226,7 @@ let drawCircle: (~color: color, ~radius: int=?, unit) => unit =
}
```
```js
function drawCircle(color, radius, param) {
function drawCircle(color, radius) {
setColor(color);
if (radius !== undefined) {
return startAt(radius, radius);
Expand All @@ -255,16 +253,16 @@ Sometimes, you might want to forward a value to a function without knowing wheth
```res
let result =
switch payloadRadius {
| None => drawCircle(~color, ())
| Some(r) => drawCircle(~color, ~radius=r, ())
| None => drawCircle(~color)
| Some(r) => drawCircle(~color, ~radius=r)
}
```
```js
var r = payloadRadius;

var result = r !== undefined
? drawCircle(color, Caml_option.valFromOption(r), undefined)
: drawCircle(color, undefined);
? drawCircle(color, Caml_option.valFromOption(r))
: drawCircle(color);
```

</CodeTab>
Expand All @@ -274,10 +272,10 @@ This quickly gets tedious. We provide a shortcut:
<CodeTab labels={["ReScript", "JS Output"]}>

```res
let result = drawCircle(~color, ~radius=?payloadRadius, ())
let result = drawCircle(~color, ~radius=?payloadRadius)
```
```js
var result = drawCircle(1, undefined, undefined);
var result = drawCircle(1, undefined);
```

</CodeTab>
Expand All @@ -291,13 +289,13 @@ Optional labeled arguments can also be provided a default value. In this case, t
<CodeTab labels={["ReScript", "JS Output"]}>

```res
let drawCircle = (~radius=1, ~color, ()) => {
let drawCircle = (~radius=1, ~color) => {
setColor(color)
startAt(radius, radius)
}
```
```js
function drawCircle(radiusOpt, color, param) {
function drawCircle(radiusOpt, color) {
var radius = radiusOpt !== undefined ? radiusOpt : 1;
setColor(color);
return startAt(radius, radius);
Expand Down Expand Up @@ -388,6 +386,29 @@ function callFirst(_param) {

</CodeTab>

## Partial Application

**Since 11.0**

To partially apply a function, use the explicit `...` syntax.

<CodeTab labels={["ReScript", "JS Output"]}>
```res
let add = (a, b) => a + b
let addFive = add(5, ...)
```

```js
function add(a, b) {
return a + b | 0;
}

function addFive(extra) {
return 5 + extra | 0;
}
```
</CodeTab>

## Async/Await

Just as in JS, an async function can be declared by adding `async` before the definition, and `await` can be used in the body of such functions.
Expand Down
4 changes: 3 additions & 1 deletion pages/docs/manual/latest/jsx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ React.createElement(MyComponent, {
- Props spread is supported, but there are some restrictions (see below).
- Punning!

### Spread Props (from v10.1)
### Spread Props

**Since 10.1**

JSX props spread is supported now, but in a stricter way than in JS.

Expand Down
2 changes: 1 addition & 1 deletion pages/docs/manual/latest/pipe.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Let's say you have a function `namePerson`, which takes a `person` then a `name`
<CodeTab labels={["ReScript", "JS Output"]}>

```res
makePerson(~age=47, ())
makePerson(~age=47)
->namePerson("Jane")
```
```js
Expand Down
Loading

0 comments on commit 4d7cc30

Please sign in to comment.