From abb6f4319cfca6020e2b7e5372ddf6a95644716a Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Sat, 21 Oct 2023 01:18:48 +0200 Subject: [PATCH 1/3] Adapt some pages to ReScript 11 including records and variants --- pages/docs/manual/latest/function.mdx | 29 +- pages/docs/manual/latest/installation.mdx | 9 + pages/docs/manual/latest/let-binding.mdx | 4 +- pages/docs/manual/latest/primitive-types.mdx | 21 +- pages/docs/manual/latest/record.mdx | 117 ++++++++ pages/docs/manual/latest/variant.mdx | 288 +++++++++++++++++-- 6 files changed, 402 insertions(+), 66 deletions(-) diff --git a/pages/docs/manual/latest/function.mdx b/pages/docs/manual/latest/function.mdx index 4b638ebd2..c2dae821f 100644 --- a/pages/docs/manual/latest/function.mdx +++ b/pages/docs/manual/latest/function.mdx @@ -388,34 +388,7 @@ function callFirst(_param) { -## Uncurried Function - -ReScript's functions are curried by default, which is one of the few performance penalties we pay in the compiled JS output. The compiler does a best-effort job at removing those currying whenever possible. However, in certain edge cases, you might want guaranteed uncurrying. In those cases, put a dot in the function's parameter list: - - - -```res example -let add = (. x, y) => x + y - -add(. 1, 2) -``` -```js -function add(x, y) { - return x + y | 0; -} - -add(1, 2); -``` - - - -If you write down the uncurried function's type, you'll add a dot there as well. - -**Note**: both the declaration site and the call site need to have the uncurry annotation. That's part of the guarantee/requirement. - -**This feature seems trivial**, but is actually one of our most important features, as a primarily functional language. We encourage you to use it if you'd like to remove any mention of `Curry` runtime in the JS output. - -## Async/Await (from v10.1) +## 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. The output looks like idiomatic JS: diff --git a/pages/docs/manual/latest/installation.mdx b/pages/docs/manual/latest/installation.mdx index da0a5ae9b..97d1b8a8b 100644 --- a/pages/docs/manual/latest/installation.mdx +++ b/pages/docs/manual/latest/installation.mdx @@ -21,6 +21,15 @@ npm run res:build node src/Demo.bs.js ``` +or use the create-rescript-app tool: + +```sh +npm create rescript-app // Select basic template +cd +npm run res:build +node src/Demo.bs.js +``` + That compiles your ReScript into JavaScript, then uses Node.js to run said JavaScript. **We recommend you use our unique workflow of keeping a tab open for the generated `.bs.js` file**, so that you can learn how ReScript transforms into JavaScript. Not many languages output clean JavaScript code you can inspect and learn from! During development, instead of running `npm run res:build` each time to compile, use `npm run res:dev` to start a watcher that recompiles automatically after file changes. diff --git a/pages/docs/manual/latest/let-binding.mdx b/pages/docs/manual/latest/let-binding.mdx index ed4c15782..7728cbbbb 100644 --- a/pages/docs/manual/latest/let-binding.mdx +++ b/pages/docs/manual/latest/let-binding.mdx @@ -131,9 +131,9 @@ As a matter of fact, even this is valid code: ```res example let result = "hello" -Js.log(result) // prints "hello" +Console.log(result) // prints "hello" let result = 1 -Js.log(result) // prints 1 +Console.log(result) // prints 1 ``` ```js var result = 1; diff --git a/pages/docs/manual/latest/primitive-types.mdx b/pages/docs/manual/latest/primitive-types.mdx index 74949b4f4..d4ab01f29 100644 --- a/pages/docs/manual/latest/primitive-types.mdx +++ b/pages/docs/manual/latest/primitive-types.mdx @@ -48,7 +48,6 @@ There's a special syntax for string that allows - multiline string just like before - no special character escaping - Interpolation -- Proper unicode handling @@ -71,27 +70,9 @@ var greeting = "Hello\nWorld\nšŸ‘‹\n" + name + "\n"; This is just like JavaScript's backtick string interpolation, except without needing to escape special characters. -### Unsafe String Interpolation (Deprecated) - -> ReScript v10.1.4 deprecated unsafe string interpolation and will be removed in v11. - -For interpolation, you'll have to convert the binding (`name` in the example) into a string if it isn't one. If you want the interpolation to implicitly convert a binding into a string, prepend a `j`: - - - -```res example -let age = 10 -let message = j`Today I am $age years old.` -``` -```js -var message = "Today I am " + 10 + " years old."; -``` - - - ### Usage -See the familiar `Js.String` API in the [API docs](api/js/string). Since a ReScript string maps to a JavaScript string, you can mix & match the string operations in both standard libraries. +See the familiar `String` API in the [API docs](api/js/string). Since a ReScript string maps to a JavaScript string, you can mix & match the string operations in all standard libraries. ### Tips & Tricks diff --git a/pages/docs/manual/latest/record.mdx b/pages/docs/manual/latest/record.mdx index d6f859787..53c114a9b 100644 --- a/pages/docs/manual/latest/record.mdx +++ b/pages/docs/manual/latest/record.mdx @@ -394,6 +394,122 @@ var me = { +## Record Type Spread + +In ReScript v11, you can now spread one or more record types into a new record type. It looks like this: + +```rescript +type a = { + id: string, + name: string, +} + +type b = { + age: int +} + +type c = { + ...a, + ...b, + active: bool +} +``` + +`type c` will now be: + +```rescript +type c = { + id: string, + name: string, + age: int, + active: bool, +} +``` + +Record type spreads act as a 'copy-paste' mechanism for fields from one or more records into a new record. This operation inlines the fields from the spread records directly into the new record definition, while preserving their original properties, such as whether they are optional or mandatory. It's important to note that duplicate field names are not allowed across the records being spread, even if the fields share the same type. + +## Record Type Coercion + +Record type coercion gives us more flexibility when passing around records in our application code. In other words, we can now coerce a record `a` to be treated as a record `b` at the type level, as long as the original record `a` contains the same set of fields in `b`. Here's an example: + +```rescript +type a = { + name: string, + age: int, +} + +type b = { + name: string, + age: int, +} + +let nameFromB = (b: b) => b.name + +let a: a = { + name: "Name", + age: 35, +} + +let name = nameFromB(a :> b) +``` + +Notice how we _coerced_ the value `a` to type `b` using the coercion operator `:>`. This works because they have the same record fields. This is purely at the type level, and does not involve any runtime operations. + +Additionally, we can also coerce records from `a` to `b` whenever `a` is a super-set of `b` (i.e. `a` containing all the fields of `b`, and more). The same example as above, slightly altered: + +```rescript +type a = { + id: string, + name: string, + age: int, + active: bool, +} + +type b = { + name: string, + age: int, +} + +let nameFromB = (b: b) => b.name + +let a: a = { + id: "1", + name: "Name", + age: 35, + active: true, +} + +let name = nameFromB(a :> b) +``` + +Notice how `a` now has more fields than `b`, but we can still coerce `a` to `b` because `b` has a subset of the fields of `a`. + +In combination with [optional record fields](/docs/manual/latest/record#optional-record-fields), one may coerce a mandatory field of an `option` type to an optional field: + +```rescript +type a = { + name: string, + + // mandatory, but explicitly typed as option + age: option, +} + +type b = { + name: string, + // optional field + age?: int, +} + +let nameFromB = (b: b) => b.name + +let a: a = { + name: "Name", + age: Some(35), +} + +let name = nameFromB(a :> b) +``` + ## Tips & Tricks ### Record Types Are Found By Field Name @@ -438,3 +554,4 @@ After reading the constraints in the previous sections, and if you're coming fro 1. The truth is that most of the times in your app, your data's shape is actually fixed, and if it's not, it can potentially be better represented as a combination of variant (introduced next) + record instead. 2. Since a record type is resolved through finding that single explicit type declaration (we call this "nominal typing"), the type error messages end up better than the counterpart ("structural typing", like for tuples). This makes refactoring easier; changing a record type's fields naturally allows the compiler to know that it's still the same record, just misused in some places. Otherwise, under structural typing, it might get hard to tell whether the definition site or the usage site is wrong. + diff --git a/pages/docs/manual/latest/variant.mdx b/pages/docs/manual/latest/variant.mdx index 9e8624fa2..862b835a1 100644 --- a/pages/docs/manual/latest/variant.mdx +++ b/pages/docs/manual/latest/variant.mdx @@ -160,7 +160,7 @@ See the [Pattern Matching/Destructuring](pattern-matching-destructuring) section A variant value compiles to 3 possible JavaScript outputs depending on its type declaration: -- If the variant value is a constructor with no payload, it compiles to a number. +- If the variant value is a constructor with no payload, it compiles to a string. - If it's a constructor with a payload, it compiles to an object with the field `TAG` and the field `_0` for the first payload, `_1` for the second payload, etc. - An exception to the above is a variant whose type declaration contains only a single constructor with payload. In that case, the constructor compiles to an object without the `TAG` field. - Labeled variant payloads (the inline record trick earlier) compile to an object with the label names instead of `_0`, `_1`, etc. The object might or might not have the `TAG` field as per previous rule. @@ -193,44 +193,266 @@ let a1 = Warrior({score: 10.5}) let a2 = Wizard("Joe") ``` ```js -var g1 = /* Hello */0; -var g2 = /* Goodbye */1; +var g1 = "Hello"; -var o1 = /* Good */0; -var o2 = /* Error */{ +var g2 = "Goodbye"; + +var o1 = "Good"; + +var o2 = { + TAG: "Error", _0: "oops!" }; -var f1 = /* Child */0; +var f1 = "Child"; + var f2 = { - TAG: /* Mom */0, + TAG: "Mom", _0: 30, _1: "Jane" }; + var f3 = { - TAG: /* Dad */1, + TAG: "Dad", _0: 32 }; -var p1 = /* Teacher */0; -var p2 = /* Student */{ +var p1 = "Teacher"; + +var p2 = { + TAG: "Student", gpa: 99.5 }; var a1 = { - TAG: /* Warrior */0, + TAG: "Warrior", _0: { score: 10.5 } }; + var a2 = { - TAG: /* Wizard */1, + TAG: "Wizard", _0: "Joe" }; ``` +## Tagged variants + +- The `@tag` attribute lets you customize the discriminator (default: `TAG`). +- `@as` attributes control what each variant case is discriminated on (default: the variant case name as string). + +### Example: Binding to TypeScript enums + +```typescript +// direction.ts +/** Direction of the action. */ +enum Direction { + /** The direction is up. */ + Up = "UP", + + /** The direction is down. */ + Down = "DOWN", + + /** The direction is left. */ + Left = "LEFT", + + /** The direction is right. */ + Right = "RIGHT", +} + +export const myDirection = Direction.Up; +``` + +You can bind to the above enums like so: + +```rescript +/** Direction of the action. */ +type direction = + | /** The direction is up. */ + @as("UP") + Up + + | /** The direction is down. */ + @as("DOWN") + Down + + | /** The direction is left. */ + @as("LEFT") + Left + + | /** The direction is right. */ + @as("RIGHT") + Right + +@module("./direction.js") external myDirection: direction = "myDirection" +``` + +Now, this maps 100% to the TypeScript code, including letting us bring over the documentation strings so we get a nice editor experience. + +### String literals + +The same logic is easily applied to string literals from TypeScript, only here the benefit is even larger, because string literals have the same limitations in TypeScript that polymorphic variants have in ReScript. + +```typescript +// direction.ts +type direction = "UP" | "DOWN" | "LEFT" | "RIGHT"; +``` + +There's no way to attach documentation strings to string literals in TypeScript, and you only get the actual value to interact with. + +## Untagged variants + +With _untagged variants_ it is possible to represent a heterogenous array. + +```rescript +@unboxed type listItemValue = String(string) | Boolean(bool) | Number(float) + +let myArray = [String("Hello"), Boolean(true), Boolean(false), Number(13.37)] +``` + +Here, each value will be _unboxed_ at runtime. That means that the variant payload will be all that's left, the variant case name wrapping the payload itself will be stripped out and the payload will be all that remains. + +It, therefore, compiles to this JS: + +```javascript +var myArray = ["hello", true, false, 13.37]; +``` + +In the above example, reaching back into the values is as simple as pattern matching on them. + +### Pattern matching on nullable values + +```rescript +// The type definition below is inlined here to examplify, but this definition will live in [Core](https://github.com/rescript-association/rescript-core) and be easily accessible +module Null = { + @unboxed type t<'a> = Present('a) | @as(null) Null +} + +type userAge = {ageNum: Null.t} + +type rec user = { + name: string, + age: Null.t, + bestFriend: Null.t, +} + +let getBestFriendsAge = user => + switch user.bestFriend { + | Present({age: Present({ageNum: Present(ageNum)})}) => Some(ageNum) + | _ => None + } +``` + +> Notice how `@as` allows us to say that an unboxed variant case should map to a specific underlying _primitive_. `Present` has a type variable, so it can hold any type. And since it's an unboxed type, only the payloads `'a` or `null` will be kept at runtime. That's where the magic comes from. + +### Decoding and encoding JSON idiomatically + +With unboxed variants, we have everything we need to define a JSON type: + +```rescript +@unboxed +type rec json = + | @as(false) False + | @as(true) True + | @as(null) Null + | String(string) + | Number(float) + | Object(Js.Dict.t) + | Array(array) + +let myValidJsonValue = Array([String("Hi"), Number(123.)]) +``` + +Here's an example of how you could write your own JSON decoders easily using the above, leveraging pattern matching: + +```rescript +@unboxed +type rec json = + | @as(false) False + | @as(true) True + | @as(null) Null + | String(string) + | Number(float) + | Object(Js.Dict.t) + | Array(array) + +type rec user = { + name: string, + age: int, + bestFriend: option, +} + +let rec decodeUser = json => + switch json { + | Object(userDict) => + switch ( + userDict->Dict.get("name"), + userDict->Dict.get("age"), + userDict->Dict.get("bestFriend"), + ) { + | (Some(String(name)), Some(Number(age)), Some(maybeBestFriend)) => + Some({ + name, + age: age->Float.toInt, + bestFriend: maybeBestFriend->decodeUser, + }) + | _ => None + } + | _ => None + } + +let decodeUsers = json => + switch json { + | Array(array) => array->Array.map(decodeUser)->Array.keepSome + | _ => [] + } +``` + +Encoding that same structure back into JSON is also easy: + +```rescript +let rec userToJson = user => Object( + Dict.fromArray([ + ("name", String(user.name)), + ("age", Number(user.age->Int.toFloat)), + ( + "bestFriend", + switch user.bestFriend { + | None => Null + | Some(friend) => userToJson(friend) + }, + ), + ]), +) + +let usersToJson = users => Array(users->Array.map(userToJson)) +``` + +This can be extrapolated to many more cases. + +## Coercion + +You can convert a variant to a `string` or `int` at no cost: + + + +```res +type company = Apple | Facebook | Other(string) +let theCompany: company = Apple + +let message = "Hello " ++ (theCompany :> string) +``` + +```js +var theCompany = "Apple"; +var message = "Hello " + theCompany; +``` + + + ## Tips & Tricks **Be careful** not to confuse a constructor carrying 2 arguments with a constructor carrying a single tuple argument: @@ -285,14 +507,48 @@ function betterDraw(animal) { } betterDraw({ - TAG: /* MyFloat */0, - _0: 1.5 -}); + TAG: "MyFloat", + _0: 1.5 + }); +``` + + + +**Try not to do that**, as this generates extra noisy output. Instead, use the `@unboxed` attribute to guide ReScript to generate more efficient code: + + + + +```res example +// reserved for internal usage +@module("myLibrary") external draw : 'a => unit = "draw" + +@unboxed +type animal = + | MyFloat(float) + | MyString(string) + +let betterDraw = (animal) => + switch animal { + | MyFloat(f) => draw(f) + | MyString(s) => draw(s) + } + +betterDraw(MyFloat(1.5)) +``` +```js +var MyLibrary = require("myLibrary"); + +function betterDraw(animal) { + MyLibrary.draw(animal); +} + +MyLibrary.draw(1.5); ``` -**Try not to do that**, as this generates extra noisy output. Alternatively, define two `external`s that both compile to the same JS call: +Alternatively, define two `external`s that both compile to the same JS call: From 9176a3117da919859b8660b2e377dc3156400c1b Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Sun, 22 Oct 2023 21:52:20 +0200 Subject: [PATCH 2/3] Some better wordings --- pages/docs/manual/latest/record.mdx | 2 +- pages/docs/manual/latest/variant.mdx | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pages/docs/manual/latest/record.mdx b/pages/docs/manual/latest/record.mdx index 53c114a9b..b251d7aa4 100644 --- a/pages/docs/manual/latest/record.mdx +++ b/pages/docs/manual/latest/record.mdx @@ -426,7 +426,7 @@ type c = { } ``` -Record type spreads act as a 'copy-paste' mechanism for fields from one or more records into a new record. This operation inlines the fields from the spread records directly into the new record definition, while preserving their original properties, such as whether they are optional or mandatory. It's important to note that duplicate field names are not allowed across the records being spread, even if the fields share the same type. +Record type spreads act as a 'copy-paste' mechanism for fields from one or more records into a new record. This operation inlines the fields from the spread records directly into the new record definition, while preserving their original properties, such as whether they are optional or mandatory. It's important to note that duplicate field names are not allowed across the records being spread, even if the fields have the same type. ## Record Type Coercion diff --git a/pages/docs/manual/latest/variant.mdx b/pages/docs/manual/latest/variant.mdx index 862b835a1..a0fbb8cab 100644 --- a/pages/docs/manual/latest/variant.mdx +++ b/pages/docs/manual/latest/variant.mdx @@ -346,18 +346,17 @@ let getBestFriendsAge = user => } ``` -> Notice how `@as` allows us to say that an unboxed variant case should map to a specific underlying _primitive_. `Present` has a type variable, so it can hold any type. And since it's an unboxed type, only the payloads `'a` or `null` will be kept at runtime. That's where the magic comes from. +> Notice how `@as` allows us to say that an untagged variant case should map to a specific underlying _primitive_. `Present` has a type variable, so it can hold any type. And since it's an unboxed type, only the payloads `'a` or `null` will be kept at runtime. That's where the magic comes from. ### Decoding and encoding JSON idiomatically -With unboxed variants, we have everything we need to define a JSON type: +With untagged variants, we have everything we need to define a JSON type: ```rescript @unboxed type rec json = - | @as(false) False - | @as(true) True | @as(null) Null + | Boolean(bool) | String(string) | Number(float) | Object(Js.Dict.t) From c0e1d90205c9361ebcc200d729754929c25d3b5a Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Sun, 22 Oct 2023 21:52:48 +0200 Subject: [PATCH 3/3] Variants: Add some TODOs --- pages/docs/manual/latest/variant.mdx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pages/docs/manual/latest/variant.mdx b/pages/docs/manual/latest/variant.mdx index a0fbb8cab..1d4fdcc0e 100644 --- a/pages/docs/manual/latest/variant.mdx +++ b/pages/docs/manual/latest/variant.mdx @@ -432,6 +432,18 @@ let usersToJson = users => Array(users->Array.map(userToJson)) This can be extrapolated to many more cases. +// ### Unboxable types + +// TODO #734: Add a list of what can currently be unboxed (and why), and a note that it's possible that more things could be unboxed in the future. + +// ### Catch all + +// TODO #733: Add a small section on the "catch all" trick, and what kind of things that enable. + +// ## Variant spread + +// TODO #732 + ## Coercion You can convert a variant to a `string` or `int` at no cost: @@ -452,6 +464,12 @@ var message = "Hello " + theCompany; +// TODO #731: expand this section with: +// +// Coercing between variants (and the constraints around that) +// Why you can sometimes coerce from variant to string/int/float, and how to think about that (runtime representation must match) +// The last additions of allowing coercing strings to unboxed variants with catch-all string cases + ## Tips & Tricks **Be careful** not to confuse a constructor carrying 2 arguments with a constructor carrying a single tuple argument: