mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-11-22 21:52:31 +03:00
[rescript/en] add ReScript (#4394)
This commit is contained in:
parent
f3fc404374
commit
fadb281798
536
rescript.html.markdown
Normal file
536
rescript.html.markdown
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
---
|
||||||
|
language: ReScript
|
||||||
|
filename: rescript.res
|
||||||
|
contributors:
|
||||||
|
- ["Seth Corker", "https://sethcorker.com"]
|
||||||
|
- ["Danny Yang", "https://yangdanny97.github.io"]
|
||||||
|
---
|
||||||
|
|
||||||
|
ReScript is a robustly typed language that compiles to efficient and human-readable JavaScript. It comes with a lightning fast compiler toolchain that scales to any codebase size. ReScript is descended from OCaml and Reason, with nice features like type inference and pattern matching, along with beginner-friendly syntax and a focus on the JavaScript ecosystem.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/* Comments start with slash-star, and end with star-slash */
|
||||||
|
// Single line comments start with double slash
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Variable and function declaration
|
||||||
|
*----------------------------------------------
|
||||||
|
* Variables and functions use the let keyword and end with a semi-colon
|
||||||
|
* `let` bindings are immutable
|
||||||
|
*/
|
||||||
|
|
||||||
|
let x = 5
|
||||||
|
/* - Notice we didn't add a type, ReScript will infer x is an int */
|
||||||
|
|
||||||
|
/* A function like this, take two arguments and add them together */
|
||||||
|
let add = (a, b) => a + b
|
||||||
|
/* - This doesn't need a type annotation either! */
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Type annotation
|
||||||
|
*----------------------------------------------
|
||||||
|
* Types don't need to be explicitly annotated in most cases but when you need
|
||||||
|
* to, you can add the type after the name
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* A type can be explicitly written like so */
|
||||||
|
let x: int = 5
|
||||||
|
|
||||||
|
/* The add function from before could be explicitly annotated too */
|
||||||
|
let add2 = (a: int, b: int): int => a + b
|
||||||
|
|
||||||
|
/* A type can be aliased using the type keyword */
|
||||||
|
type companyId = int
|
||||||
|
let myId: companyId = 101
|
||||||
|
|
||||||
|
/* Mutation is not encouraged in ReScript but it's there if you need it
|
||||||
|
If you need to mutate a let binding, the value must be wrapped in a `ref()`*/
|
||||||
|
let myMutableNumber = ref(120)
|
||||||
|
|
||||||
|
/* To access the value (and not the ref container), use `.contents` */
|
||||||
|
let copyOfMyMutableNumber = myMutableNumber.contents
|
||||||
|
|
||||||
|
/* To assign a new value, use the `:=` operator */
|
||||||
|
myMutableNumber := 240
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Basic types and operators
|
||||||
|
*----------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* > String */
|
||||||
|
|
||||||
|
/* Use double quotes for strings */
|
||||||
|
let greeting = "Hello world!"
|
||||||
|
|
||||||
|
/* A string can span multiple lines */
|
||||||
|
let aLongerGreeting = "Look at me,
|
||||||
|
I'm a multi-line string
|
||||||
|
"
|
||||||
|
|
||||||
|
/* Use ` for unicode */
|
||||||
|
let world = `🌍`
|
||||||
|
|
||||||
|
/* The ` annotation is also used for string interpolation */
|
||||||
|
let helloWorld = `hello, ${world}`
|
||||||
|
/* Bindings must be converted to strings */
|
||||||
|
let age = 10
|
||||||
|
let ageMsg = `I am ${Js.Int.toString(age)} years old`
|
||||||
|
|
||||||
|
/* Using `j` annotation in interpolation will implicitly convert bindings to strings */
|
||||||
|
let ageMsg = j`I am $age years old`
|
||||||
|
|
||||||
|
|
||||||
|
/* Concatenate strings with ++ */
|
||||||
|
let name = "John " ++ "Wayne"
|
||||||
|
let emailSubject = "Hi " ++ name ++ ", you're a valued customer"
|
||||||
|
|
||||||
|
/* > Char */
|
||||||
|
|
||||||
|
/* Use a single character for the char type */
|
||||||
|
let lastLetter = 'z'
|
||||||
|
/* - Char doesn't support Unicode or UTF-8 */
|
||||||
|
|
||||||
|
/* > Boolean */
|
||||||
|
|
||||||
|
/* A boolean can be either true or false */
|
||||||
|
let isLearning = true
|
||||||
|
|
||||||
|
true && false /* - : bool = false Logical and */
|
||||||
|
true || true /* - : bool = true Logical or */
|
||||||
|
!true /* - : bool = false Logical not */
|
||||||
|
|
||||||
|
/* Greater than `>`, or greater than or equal to `>=` */
|
||||||
|
'a' > 'b' /* - bool : false */
|
||||||
|
|
||||||
|
/* Less than `<`, or less than or equal to `<=` */
|
||||||
|
1 < 5 /* - : bool = true */
|
||||||
|
|
||||||
|
/* Structural equal */
|
||||||
|
"hello" == "hello" /* - : bool = true */
|
||||||
|
|
||||||
|
/* Referential equal */
|
||||||
|
"hello" === "hello" /* - : bool = false */
|
||||||
|
/* - This is false because they are two different "hello" string literals */
|
||||||
|
|
||||||
|
/* Structural unequal */
|
||||||
|
lastLetter != 'a' /* -: bool = true */
|
||||||
|
|
||||||
|
/* Referential unequal */
|
||||||
|
lastLetter !== lastLetter /* - : bool = false */
|
||||||
|
|
||||||
|
/* > Integer */
|
||||||
|
/* Perform math operations on integers */
|
||||||
|
|
||||||
|
1 + 1 /* - : int = 2 */
|
||||||
|
25 - 11 /* - : int = 11 */
|
||||||
|
5 * 2 * 3 /* - : int = 30 */
|
||||||
|
8 / 2 /* - : int = 4 */
|
||||||
|
|
||||||
|
/* > Float */
|
||||||
|
/* Operators on floats have a dot after them */
|
||||||
|
|
||||||
|
1.1 +. 1.5 /* - : float = 2.6 */
|
||||||
|
18.0 -. 24.5 /* - : float = -6.5 */
|
||||||
|
2.5 *. 2.0 /* - : float = 5. */
|
||||||
|
16.0 /. 4.0 /* - : float = 4. */
|
||||||
|
|
||||||
|
/* > Tuple
|
||||||
|
* Tuples have the following attributes
|
||||||
|
- immutable
|
||||||
|
- ordered
|
||||||
|
- fix-sized at creation time
|
||||||
|
- heterogeneous (can contain different types of values)
|
||||||
|
A tuple is 2 or more values */
|
||||||
|
|
||||||
|
let teamMember = ("John", 25)
|
||||||
|
|
||||||
|
/* Type annotation matches the values */
|
||||||
|
let position2d: (float, float) = (9.0, 12.0)
|
||||||
|
|
||||||
|
/* Pattern matching is a great tool to retrieve just the values you care about
|
||||||
|
If we only want the y value, let's use `_` to ignore the value */
|
||||||
|
let (_, y) = position2d
|
||||||
|
y +. 1.0 /* - : float = 13. */
|
||||||
|
|
||||||
|
/* > Record */
|
||||||
|
|
||||||
|
/* A record has to have an explicit type */
|
||||||
|
type trainJourney = {
|
||||||
|
destination: string,
|
||||||
|
capacity: int,
|
||||||
|
averageSpeed: float,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Once the type is declared, ReScript can infer it whenever it comes up */
|
||||||
|
let firstTrip = {destination: "London", capacity: 45, averageSpeed: 120.0}
|
||||||
|
|
||||||
|
/* Access a property using dot notation */
|
||||||
|
let maxPassengers = firstTrip.capacity
|
||||||
|
|
||||||
|
/* If you define the record type in a different file, you have to reference the
|
||||||
|
filename, if trainJourney was in a file called Trips.re */
|
||||||
|
let secondTrip: Trips.trainJourney = {
|
||||||
|
destination: "Paris",
|
||||||
|
capacity: 50,
|
||||||
|
averageSpeed: 150.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Records are immutable by default */
|
||||||
|
/* But the contents of a record can be copied using the spread operator */
|
||||||
|
let newTrip = {...secondTrip, averageSpeed: 120.0}
|
||||||
|
|
||||||
|
/* A record property can be mutated explicitly with the `mutable` keyword */
|
||||||
|
type breakfastCereal = {
|
||||||
|
name: string,
|
||||||
|
mutable amount: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
let tastyMuesli = {name: "Tasty Muesli TM", amount: 500}
|
||||||
|
|
||||||
|
tastyMuesli.amount = 200
|
||||||
|
/* - tastyMuesli now has an amount of 200 */
|
||||||
|
|
||||||
|
/* Punning is used to avoid redundant typing */
|
||||||
|
let name = "Just As Good Muesli"
|
||||||
|
let justAsGoodMuesli = {name, amount: 500}
|
||||||
|
/* - justAsGoodMuesli.name is now "Just As Good Muesli", it's equivalent
|
||||||
|
to { name: name, amount: 500 } */
|
||||||
|
|
||||||
|
/* > Variant
|
||||||
|
Mutually exclusive states can be expressed with variants */
|
||||||
|
|
||||||
|
type authType =
|
||||||
|
| GitHub
|
||||||
|
| Facebook
|
||||||
|
| Google
|
||||||
|
| Password
|
||||||
|
/* - The constructors must be capitalized like so */
|
||||||
|
/* - Like records, variants should be named if declared in a different file */
|
||||||
|
|
||||||
|
let userPreferredAuth = GitHub
|
||||||
|
|
||||||
|
/* Variants work great with a switch statement */
|
||||||
|
let loginMessage =
|
||||||
|
switch (userPreferredAuth) {
|
||||||
|
| GitHub => "Login with GitHub credentials."
|
||||||
|
| Facebook => "Login with your Facebook account."
|
||||||
|
| Google => "Login with your Google account"
|
||||||
|
| Password => "Login with email and password."
|
||||||
|
}
|
||||||
|
|
||||||
|
/* > Option
|
||||||
|
An option can be None or Some('a) where 'a is the type */
|
||||||
|
|
||||||
|
let userId = Some(23)
|
||||||
|
|
||||||
|
/* A switch handles the two cases */
|
||||||
|
let alertMessage =
|
||||||
|
switch (userId) {
|
||||||
|
| Some(id) => "Welcome, your ID is" ++ string_of_int(id)
|
||||||
|
| None => "You don't have an account!"
|
||||||
|
}
|
||||||
|
/* - Missing a case, `None` or `Some`, would cause an error */
|
||||||
|
|
||||||
|
/* > List
|
||||||
|
* Lists have the following attributes
|
||||||
|
- immutable
|
||||||
|
- ordered
|
||||||
|
- fast at prepending items
|
||||||
|
- fast at splitting
|
||||||
|
|
||||||
|
* Lists in ReScript are linked lists
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* A list is declared with the `list` keyword and initialized with values wrapped in curly braces */
|
||||||
|
let userIds = list{1, 4, 8}
|
||||||
|
|
||||||
|
/* The type can be explicitly set with list<'a> where 'a is the type */
|
||||||
|
type idList = list<int>
|
||||||
|
type attendanceList = list<string>
|
||||||
|
|
||||||
|
/* Lists are immutable */
|
||||||
|
/* But you can create a new list with additional prepended elements by using the spread operator on an existing list */
|
||||||
|
let newUserIds = list{101, 102, ...userIds}
|
||||||
|
|
||||||
|
/* > Array
|
||||||
|
* Arrays have the following attributes
|
||||||
|
- mutable
|
||||||
|
- fast at random access & updates */
|
||||||
|
|
||||||
|
/* An array is declared with `[` and ends with `]` */
|
||||||
|
let languages = ["ReScript", "JavaScript", "OCaml"]
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Function
|
||||||
|
*----------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ReScript functions use the arrow syntax, the expression is returned */
|
||||||
|
let signUpToNewsletter = email => "Thanks for signing up " ++ email
|
||||||
|
|
||||||
|
/* Call a function like this */
|
||||||
|
signUpToNewsletter("hello@ReScript.org")
|
||||||
|
|
||||||
|
/* For longer functions, use a block */
|
||||||
|
let getEmailPrefs = email => {
|
||||||
|
let message = "Update settings for " ++ email
|
||||||
|
let prefs = ["Weekly News", "Daily Notifications"]
|
||||||
|
|
||||||
|
(message, prefs)
|
||||||
|
}
|
||||||
|
/* - the final tuple is implicitly returned */
|
||||||
|
|
||||||
|
/* > Labeled Arguments */
|
||||||
|
|
||||||
|
/* Arguments can be labeled with the ~ symbol */
|
||||||
|
let moveTo = (~x, ~y) => {
|
||||||
|
/* Move to x,y */
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
moveTo(~x=7.0, ~y=3.5)
|
||||||
|
|
||||||
|
/* Labeled arguments can also have a name used within the function */
|
||||||
|
let getMessage = (~message as msg) => "==" ++ msg ++ "=="
|
||||||
|
|
||||||
|
getMessage(~message="You have a message!")
|
||||||
|
/* - The caller specifies ~message but internally the function can make use */
|
||||||
|
|
||||||
|
/* The following function also has explicit types declared */
|
||||||
|
let showDialog = (~message: string): unit => {
|
||||||
|
() /* Show the dialog */
|
||||||
|
}
|
||||||
|
/* - The return type is `unit`, this is a special type that is equivalent to
|
||||||
|
specifying that this function doesn't return a value
|
||||||
|
the `unit` type can also be represented as `()` */
|
||||||
|
|
||||||
|
/* > Currying
|
||||||
|
Functions can be curried and are partially called, allowing for easy reuse */
|
||||||
|
|
||||||
|
let div = (denom, numr) => numr / denom
|
||||||
|
let divBySix = div(6)
|
||||||
|
let divByTwo = div(2)
|
||||||
|
|
||||||
|
div(3, 24) /* - : int = 8 */
|
||||||
|
divBySix(128) /* - : int = 21 */
|
||||||
|
divByTwo(10) /* - : int = 5 */
|
||||||
|
|
||||||
|
/* > Optional Labeled Arguments */
|
||||||
|
|
||||||
|
/* Use `=?` syntax for optional labeled arguments */
|
||||||
|
let greetPerson = (~name, ~greeting=?, ()) => {
|
||||||
|
switch (greeting) {
|
||||||
|
| Some(greet) => greet ++ " " ++ name
|
||||||
|
| None => "Hi " ++ name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* - The third argument, `unit` or `()` is required because if we omitted it,
|
||||||
|
the function would be curried so greetPerson(~name="Kate") would create
|
||||||
|
a partial function, to fix this we add `unit` when we declare and call it */
|
||||||
|
|
||||||
|
/* Call greetPerson without the optional labeled argument */
|
||||||
|
greetPerson(~name="Kate", ())
|
||||||
|
|
||||||
|
/* Call greetPerson with all arguments */
|
||||||
|
greetPerson(~name="Marco", ~greeting="How are you today,")
|
||||||
|
|
||||||
|
/* > Pipe */
|
||||||
|
/* Functions can be called with the pipeline operator */
|
||||||
|
|
||||||
|
/* Use `->` to pass in the first argument (pipe-first) */
|
||||||
|
3->div(24) /* - : int = 8 */
|
||||||
|
/* - This is equivalent to div(3, 24) */
|
||||||
|
|
||||||
|
36->divBySix /* - : int = 6 */
|
||||||
|
/* - This is equivalent to divBySix(36) */
|
||||||
|
|
||||||
|
/* Pipes make it easier to chain code together */
|
||||||
|
let addOne = a => a + 1
|
||||||
|
let divByTwo = a => a / 2
|
||||||
|
let multByThree = a => a * 3
|
||||||
|
|
||||||
|
let pipedValue = 3->addOne->divByTwo->multByThree /* - : int = 6 */
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Control Flow & Pattern Matching
|
||||||
|
*----------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* > If-else */
|
||||||
|
/* In ReScript, `If` is an expression when evaluate will return the result */
|
||||||
|
|
||||||
|
/* greeting will be "Good morning!" */
|
||||||
|
let greeting = if (true) {"Good morning!"} else {"Hello!"}
|
||||||
|
|
||||||
|
/* Without an else branch the expression will return `unit` or `()` */
|
||||||
|
if (false) {
|
||||||
|
showDialog(~message="Are you sure you want to leave?")
|
||||||
|
}
|
||||||
|
/* - Because the result will be of type `unit`, both return types should be of
|
||||||
|
the same type if you want to assign the result. */
|
||||||
|
|
||||||
|
/* > Destructuring */
|
||||||
|
/* Extract properties from data structures easily */
|
||||||
|
|
||||||
|
let aTuple = ("Teacher", 101)
|
||||||
|
|
||||||
|
/* We can extract the values of a tuple */
|
||||||
|
let (name, classNum) = aTuple
|
||||||
|
|
||||||
|
/* The properties of a record can be extracted too */
|
||||||
|
type person = {
|
||||||
|
firstName: string,
|
||||||
|
age: int,
|
||||||
|
}
|
||||||
|
let bjorn = {firstName: "Bjorn", age: 28}
|
||||||
|
|
||||||
|
/* The variable names have to match with the record property names */
|
||||||
|
let {firstName, age} = bjorn
|
||||||
|
|
||||||
|
/* But we can rename them like so */
|
||||||
|
let {firstName: bName, age: bAge} = bjorn
|
||||||
|
|
||||||
|
let {firstName: cName, age: _} = bjorn
|
||||||
|
|
||||||
|
/* > Switch
|
||||||
|
Pattern matching with switches is an important tool in ReScript
|
||||||
|
It can be used in combination with destructuring for an expressive and
|
||||||
|
concise tool */
|
||||||
|
|
||||||
|
/* Lets take a simple list */
|
||||||
|
let firstNames = ["James", "Jean", "Geoff"]
|
||||||
|
|
||||||
|
/* We can pattern match on the names for each case we want to handle */
|
||||||
|
switch (firstNames) {
|
||||||
|
| [] => "No names"
|
||||||
|
| [first] => "Only " ++ first
|
||||||
|
| [first, second] => "A couple of names " ++ first ++ "," ++ second
|
||||||
|
| [first, second, third] =>
|
||||||
|
"Three names, " ++ first ++ ", " ++ second ++ ", " ++ third
|
||||||
|
| _ => "Lots of names"
|
||||||
|
}
|
||||||
|
/* - The `_` is a catch all at the end, it signifies that we don't care what
|
||||||
|
the value is so it will match every other case */
|
||||||
|
|
||||||
|
/* > When clause */
|
||||||
|
|
||||||
|
let isJohn = a => a == "John"
|
||||||
|
let maybeName = Some("John")
|
||||||
|
|
||||||
|
/* When can add more complex logic to a simple switch */
|
||||||
|
let aGreeting =
|
||||||
|
switch (maybeName) {
|
||||||
|
| Some(name) when isJohn(name) => "Hi John! How's it going?"
|
||||||
|
| Some(name) => "Hi " ++ name ++ ", welcome."
|
||||||
|
| None => "No one to greet."
|
||||||
|
}
|
||||||
|
|
||||||
|
/* > Exception */
|
||||||
|
|
||||||
|
/* Define a custom exception */
|
||||||
|
exception Under_Age
|
||||||
|
|
||||||
|
/* Raise an exception within a function */
|
||||||
|
let driveToTown = (driver: person) =>
|
||||||
|
if (driver.age >= 15) {
|
||||||
|
"We're in town"
|
||||||
|
} else {
|
||||||
|
raise(Under_Age)
|
||||||
|
}
|
||||||
|
|
||||||
|
let evan = {firstName: "Evan", age: 14}
|
||||||
|
|
||||||
|
/* Pattern match on the exception Under_Age */
|
||||||
|
switch (driveToTown(evan)) {
|
||||||
|
| status => print_endline(status)
|
||||||
|
| exception Under_Age =>
|
||||||
|
print_endline(evan.firstName ++ " is too young to drive!")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alternatively, a try block can be used */
|
||||||
|
/* - With ReScript exceptions can be avoided with optionals and are seldom used */
|
||||||
|
let messageToEvan =
|
||||||
|
try {
|
||||||
|
driveToTown(evan)
|
||||||
|
} catch {
|
||||||
|
| Under_Age => evan.firstName ++ " is too young to drive!"
|
||||||
|
}
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Object
|
||||||
|
*----------------------------------------------
|
||||||
|
* Objects are similar to Record types, but are less rigid
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* An object may be typed like a record but the property names are quoted */
|
||||||
|
type surfaceComputer = {
|
||||||
|
"color": string,
|
||||||
|
"capacity": int,
|
||||||
|
}
|
||||||
|
let surfaceBook: surfaceComputer = { "color": "blue", "capacity": 512 }
|
||||||
|
|
||||||
|
/* Objects don't require types */
|
||||||
|
let hamster = { "color": "brown", "age": 2 }
|
||||||
|
|
||||||
|
/* Object typing is structural, so you can have functions that accept any object with the required fields */
|
||||||
|
let getAge = animal => animal["age"]
|
||||||
|
getAge(hamster)
|
||||||
|
getAge({ "name": "Fido", "color": "silver", "age": 3 })
|
||||||
|
getAge({ "age": 5 })
|
||||||
|
|
||||||
|
/*----------------------------------------------
|
||||||
|
* Module
|
||||||
|
*----------------------------------------------
|
||||||
|
* Modules are used to organize your code and provide namespacing.
|
||||||
|
* Each file is a module by default
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Create a module */
|
||||||
|
module Staff = {
|
||||||
|
type role =
|
||||||
|
| Delivery
|
||||||
|
| Sales
|
||||||
|
| Other
|
||||||
|
type member = {
|
||||||
|
name: string,
|
||||||
|
role,
|
||||||
|
}
|
||||||
|
|
||||||
|
let getRoleDirectionMessage = staff =>
|
||||||
|
switch (staff.role) {
|
||||||
|
| Delivery => "Deliver it like you mean it!"
|
||||||
|
| Sales => "Sell it like only you can!"
|
||||||
|
| Other => "You're an important part of the team!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A module can be accessed with dot notation */
|
||||||
|
let newEmployee: Staff.member = {name: "Laura", role: Staff.Delivery}
|
||||||
|
|
||||||
|
/* Using the module name can be tiresome so the module's contents can be opened
|
||||||
|
into the current scope with `open` */
|
||||||
|
open Staff
|
||||||
|
|
||||||
|
let otherNewEmployee: member = {name: "Fred", role: Other}
|
||||||
|
|
||||||
|
/* A module can be extended using the `include` keyword, include copies
|
||||||
|
the contents of the module into the scope of the new module */
|
||||||
|
module SpecializedStaff = {
|
||||||
|
include Staff
|
||||||
|
|
||||||
|
/* `member` is included so there's no need to reference it explicitly */
|
||||||
|
let ceo: member = {name: "Reggie", role: Other}
|
||||||
|
|
||||||
|
let getMeetingTime = staff =>
|
||||||
|
switch (staff) {
|
||||||
|
| Other => 11_15 /* - : int = 1115 Underscores are for formatting only */
|
||||||
|
| _ => 9_30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [Official ReScript Docs](https://rescript-lang.org/)
|
||||||
|
- [Try ReScript - Online Playground](https://rescript-lang.org/try)
|
Loading…
Reference in New Issue
Block a user