Moving from JavaScript to ReasonML
Web / Mobile / VR / AR / IoT / AI
Software architect, consultant, author
Why ReasonML
Reason lets you write simple, fast and quality type-safe code while leveraging both the JavaScript & OCaml ecosystems.
Why OCaml?
- General purpose language (1996) used in critical systems
- Facebook is using it in several projects (Flow)
- Functional programming language with Hindler-Minler type system
- Can be compiled to bytecode, native code or JS
-
Performance and compilation time is blazing fast
- A language for writing React (first React prototypes were in SML)
- While being functional it still has escape hatches
- Amazing community
- compiles === works
- Amazing tooling in the language itself
just take a look at JavaScript world
Show me the Syntax
yarn global add reason-cli@latest-macos
Install Reason CLI
open ReasonML repl
rtop
100% Type inference
Types can be inferred
Tupples
Block scope
Destructuring
Variants
Parameterized variants
Built in variants
We can easily create more complex data structures
Binary tree definition:
Records
record fields can be mutable only if explicitly specified
you can use any types within record. such as tupples or variants
Pattern matching
Looks like switch statement but it's not.
You can destructure into variables when pattern matching
If not covering all use cases it won't compile
Functions
let add = (a,b) => a + b;
There are no nullary functions. Every function return unit type
Destructuring function params
let cross = ((a1, a2, a3), (b1, b2, b3)) => (
a2 * b3 - a3 * b2,
a3 * b1 - a1 * b3,
a1 * b2 - a2 * b1,
);
let computation = (~x, ~y, ~z) => x + y / z;
named params
let cross = (~vector1 as (a1, a2, a3), ~vector2 as (b1, b2, b3)) => (
a2 * b3 - a3 * b2,
a3 * b1 - a1 * b3,
a1 * b2 - a2 * b1,
);
named params destructuring
let add = (~x=0, ~y=0, ()) => x + y;
let add = (~x: int=0, ~y: int=0, ()) => x + y;
defaults
you can use "guards" in pattern matching
functions can be recursive by stating that explicitly
let add = (a,b) => a + b
add(2)(3)
all functions are curried by default
Reverse application operator enables chaining
[4, 2, 1, 3, 5]
|> List.map(x => x + 1)
|> List.filter(x => x < 5)
|> List.sort(compare);
operators are just functions and you can create your own
Under the hood list is self-recursive paramterized variant
type mylist('a) = | Nil; | Cons('a, mylist('a))
let abc = Cons("a", Cons("b", Cons("c", Nil)));
Lists
- homogeneous
- immutable
- fast at prepending items
let abc = ["a", "b", "c"]
Lists
List.nth(someList, 2);
access list item
sort and then reverse
List.rev(
List.sort(
compare,
[8,6,4,3,3,2,6,8,4]
)
);
[8,6,4,3,3,2,6,8,4] |> List.sort(compare) |> List.rev;
Or easier
let test =
switch (someList) {
| [] => "Empty"
| [first, ...last] => "Head of the list is " ++ string_of_int(first)
};
pattern matching on lists
Arrays
- mutable
- fast at random access & updates
- fix-sized on native (flexible on JS)
sort and then reverse
let myArray = [| "a", "b", "c" |];
myArray[2];
let filterArray = (filter, arr) =>
arr
|> Array.to_list
|> List.filter(filter)
|> Array.of_list;
Filtering
Modules
let make = () => "";
let logStr = (str: string, log: string) => log ++ str ++ "\n";
let print = (log: string) => print_string(log);
Every file is a module Everything is exported from module
You can control how much to export via interfaces. Interface of a module is also called its signature
signatures are defined in rei files.
Log.re
Log.rei
type t;
let make: (unit) => t;
let logStr: (string, t) => t;
let print: (t) => unit;
Where is JS and how Reason compiles to it
Reason -> Bucklescript -> JS
Let's get started in VSCode
yarn add bs-platform --dev --exact
yarn add reason-react --exact
Bucklescript
JS primitives: https://rescript-lang.org/apis/latest/js
Installing libraries
{
"dependencies": {
"bs-jest": "^0.1.5"
}
}
{
"bs-dependencies": [
"bs-jest"
],
···
}
Bucklescript(Rescript) objects are like records
- No type declaration needed.
- Structural and more polymorphic, unlike records.
- Doesn't support updates unless the object comes from the JS side.
- Doesn't support pattern matching.
type person = {
"age": int,
"name": string
};
type declaration is optional
let me = {
"age": 5,
"name": "Big ReScript"
}
access
let age = me["age"]
FFI
[%raw {|
console.log('here is some javascript for you')
|}];
raw js
type person = {
name: string,
friends: array<string>,
age: int,
}
[@bs.module("MySchool")] external john: person = "john"
let johnName = john.name
var MySchool = require("MySchool");
var johnName = MySchool.john.name;
convert JS object to Record
Bucklescript Externals
[@bs.val] external bindingToBeCalledInReason: typeSignature = "functionNameOnGlobalScope"
external is a keyword for declaring a value in BuckleScript/OCaml/Reason:
[@bs.val] external setTimeout: (unit => unit, int) => float = "";
[@bs.val] external clearTimeout : float => unit = "";
It's like let but the body is a string
Globals
Abstract type
type timeout;
[@bs.val] external setTimeout: (unit => unit, int) => timeout = "";
[@bs.val] external clearTimeout: timeout => unit = "";
Null, Undefined, Option
If you're receiving, for example, a JS string that can be null and undefined, type it as:
[@bs.module "MyConstant"] external myId: Js.Nullable.t(string) = "myId"
To pass nullable string to a module
[@bs.module "MyIdValidator"] external validate: Js.Nullable.t(string) => bool = "validate";
let personId: Js.Nullable.t(string) = Js.Nullable.return("abc123");
let result = validate(personId);
Import Exports
[@bs.module "path"] external dirname : string => string = "dirname";
default imports
[@bs.module "./student"] external studentName : string = "default";
module Decode {
let planet = planet =>
Json.Decode.{
name: planet |> field("name", string),
rotation_period: planet |> field("rotation_period", string),
orbital_period: planet |> field("orbital_period", string),
diameter: planet |> field("diameter", string),
climate: planet |> field("climate", string),
gravity: planet |> field("gravity", string),
terrain: planet |> field("terrain", string),
surface_water: planet |> field("surface_water", string),
population: planet |> field("population", string),
residents: planet |> field("residents", array(string)),
films: planet |> field("films", array(string)),
created: planet |> field("created", string),
edited: planet |> field("edited", string),
url: planet |> field("url", string)
}
let planets = json => Json.Decode.list(planet, json);
let result = json => Json.Decode.{
count: json |> field("count", int),
previous: json |> field("previous", optional(string)),
next: json |> field("next", optional(string)),
results: json |> field("results", array(planet))
}
}
let print_decoded_planets = _ => {
Js.Promise.(
Fetch.fetch("https://swapi.co/api/planets")
|> then_(Fetch.Response.json)
|> then_(
json => {
let response = Decode.result(json);
response.results |> Array.to_list |> List.iter((planet) => {
switch (planet) {
| plnt => print_endline(plnt.name)
}
})
resolve(json);
}
)
|> then_(
json => json |> Js.Json.stringify
|> resolve
)
|> catch(err => {
Js.log(err)
resolve("")
}
)
);
}
Json encoding and decoding with bs-json
Js.Promise.(
Fetch.fetch("https://swapi.co/api/planets")
|> then_(Fetch.Response.json)
|> then_(
json => Js.Json.stringify(json)
|> print_endline
|> resolve
)
);
Fetching data with bs-fetch
What about React?
Let's convert Create React App to ReasonML
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
switch (ReactDOM.querySelector("#root")) {
| Some(root) => ReactDOM.render(<React.StrictMode><div>{"Hello" |> ReasonReact.string }</div></React.StrictMode>, root)
| None => ()
}
First let's convert rendering to DOM
To bring in our App component from JS we need to type it explicitly for Reason.
module App = {
[@bs.module "./App.js"][@react.component]
external make: (_) => React.element = "default";
}
For that we can use one Bucklescript external
[@bs.module "./serviceWorker.js"]
external unregister: _ => unit = "unregister"
unregister();
Now let's convert App component
[@bs.val] external require: string => string = "";
let logo = require("./logo.svg");
require("./App.css");
The rest is pretty straightforward
[@react.component]
let make = () => {
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
{ "Edit <code>src/App.js</code> and save to reload." |> ReasonReact.string }
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
{"Learn React" |> ReasonReact.string}
</a>
</header>
</div>
}
Let's convert create-react-app to ReasonML
Thank You
@VladimirNovick
OdessaJS Moving from JS to ReasonML
By Vladimir Novick
OdessaJS Moving from JS to ReasonML
- 1,183