let is just an expression and akin to a function!
let greeting = hello
let greeting = "hi"; /* shadowed */
let scoped = {
let part1 = "hello";
let part2 = "world";
};
/* part1 & part2 are not accessible here */
- Highlight of Reason
let score = 10; /* inference */
let score: int = 10;
let i = 5;
let i: int = 5;
let i = (5: int) + (4: int);
let add = (x: int, y: int) : int => x + y;
let drawCircle = (~radius as r: int) : unit => ...; /* "as" means this is a labeled argument */
??? I presume (x, y) => x + y
is regular function form
type score = int
let x: score = 10;
- Types can be inferred
- Type coverage always 100%
- Type system is sound. As long as code compiles - every integer is an integer
let greeting = "Hello
world!";
let oneSlash = "\\";
let greetings = "Hi" ++ " and howdy";
let greetingAndOneSlash = {|Hello
World
\
blah..
|};
/* ABOVE: no special chars, hooks for pre-processors */
Useful preprocessors:
let w = {js|世界|js} /* Unicode support */
let w = {j|你好,$world|j} /* Unicode & interpolation char support */
-
Js.String from BuckleScript
-
Don't misuse strings, there are language features for unique ids, identifier into data structure, name of an object field, enum, etc
Though for JS compilation, you'd use [%bs.re] and Js.Re instead, since Str is not available.
??? Do i need to write different code for different targets?
let blah = 'a';
Does NOT support Unicode or UTF-8
- Char compiles to int (0-255)
- You can pattern match on chars
- Convert String->Char "a".[0]
- Convert Char->String `String.make(1, 'a')
(1,2) == (2,1) /* physical equal */
(1,2) !== (2,1) /* Referential equal */
BuckleScript provides bindings to Js true
& false
. Not interchangable (Js.to_bool
and Js.Boolean.to_js_boolean
).
Physical equal:
- handy, but deep
- some comparisons not clear (e.g. foo equal to lazy foo/)
- Modular implicits are on their way
Something something variants????
type bool = True | False
, no need to hard-code a boolean, but constructors are comiled into less readable representation (here)
-
32bits, will truncate
-
Careful when binding to JS numbers. Long ones might be truncated.
-
no Reason Float, Js.Float
-
SERIOUS limitations
Why the heck can't I just use an overloaded + for both int and float? Why is it that each time I find a performant language with great types and interop and community, I find these kind of flaws?
https://reasonml.github.io/docs/en/tuple.html
- immutable
- ordered
- fixed-size
- heterogeneous (w/multiple types)
let ageAndName = (int, string) = (24, "blah");
/* A tuple type alias */
type vector = (float, float, float);
let vel: vector = (1.0, 0.3, 0.0);
Note: no tuple size 1 - just use value
Can use fst
& snd
to get first and second elements docs
Normally use destructuring let (_, y, _) = vel
Generally, use tuple for local conveniance, while records for long-lived data that is passed around.
AKA product type, (string, int)
is written as string * int
in some places. Tuple is really a cartesian product (!!! Thus the exhaustive switch
)
Like js objects but:
- lighter
- immutable by default
- fixed in field names/types
- fast
- more rigidly typed
Type:
type person = {
age: int,
name: string
};
^ This is required
Value:
let me = {
age: 5,
name: "Big reason"
}
Access:
let age = me.age;
let me: School.person = {age: 20, name: "blah"}
/* less preferred...*/
let me = School.{age: 30, name:"blah"};
let me = {School.age: 20, name: "blah"};
Spread operator is:
- immutable
- fast
Use reassignment via =
type horsePower = { power: int, metric: bool}
let metric = true;
let a = {power: 10, metric}
Same field name/Type can be combined
NOTE: cannot do with single field {foo}
(block that returns value foo)
You can convert between JS objects and records. But a better way without conversion overhead is to use Reason Objects
type payload = {. "name": string };
[@bs.module "myAjaxLibrary"] external sendQuery : payload => unit = "sendQuery"; /* ??? is this an import */
sendQuery({"name": "Reaon"});
??? I don't kow what this does
Dot in type definition signifies object type notation - nothing to do with a record
type person = {age: int, name: string};
type monstor = {age: int, tentacles: bool};
let getAge = (entitiy) => entity.age; /* infers entity is of type monstor ??? Why not person ??? */
let kraken = {age: 9999, tentacles: true};
let me = {age: 5, name: "Baby guy"};
getAge(kraken);
getAge(me); /* FAILURE - getAge only works with monstor */
If need "duck typing", use Reason objects ??? double-check
- Data should be fixed most of the time, hence records are relevant
- Variations in data, should be handled by variants
- Very fast, as its 2 assembly instructions (field lookup + access)
- Name-based typing a.k. explicity type declarations mean type error messages are easier to read. Refactoring is easier; changing a record type's fields lets the compiler know its the same record, but with wrong field names sometimes.
type myResponseVariant =
| Yes
| N
| PrettyMuch;
let crushingIt = Yes;
Most data structures express "this AND that", variants allow "this OR that" ??? Does this mean "this OR other, AND that OR another"
Yes
, No
, and PrettyMuch
are constructors (aka tags)
Note: constructors must be capitilised
switch
allows you to exhaustively check every case of a variant:
let message =
switch (crushingIt) {
No => "No worries, keep going"
Yes => "Great"
PrettyMuch => "Nice"
};
/* EDIT: Missing | before constructors
*/
Bring into separate file
/* Zoo.re *
type animal = Dog | Cat;
/* example.re */
let pet: Zoo.animal = Dog;
type account =
| None
| Instagram(string)
| Facebook(string, int)
You can also pattern match
let greeting =
switch (myAccount) {
| None => "Hi!"
| Facebook(name, age) => "Hi " ++ name ++ " you have age of " ++ age
| Instagram(name) => "Hi" ++ name
};
The standard library includes:
- simulate nullable values. Reason can default every value to be non-nullable. An
int
will never beint
ornull
orundefined
. If you want a nullable int, you useoption(int)
- whose values can beNone
orSome(int)
- both must be switched.
type list('a) = Empty | Head('a, list('a));
A list that holds a
is either empty, or it holds that value plus another list
Reason has sugar for this [1,2,3]
aka Head(1, Head(2, Head(3, Empty)))
. All must be switched, including Empty
Variant like types like string, int float array and data structures can be switched over.
??? How is an int, variant-like
foo = int | string; /* INCORRECT */
foo = Int(int) | String(string); /* correct */
Many js functions have many combinations of arguments.
e.g. myLib.draw()
takes either number
or string
You could do this:
/* reserved for internal use */
[@bs "myLib"] external draw : 'a => unit = "draw";
type animal =
| MyFloat(float)
| MyString(string);
let betterDraw = (animal) =>
switch (animal) {
| MyFloat(f) => draw(f)
| MyString(s) => draw(s)
};
But this is better - two externals that compile to the same JS call:
[@bs.module "myLib"] external drawFloat : float => unit = "draw";
[@bs.module "myLib"] external drawString : string => unit = "draw";
BuckleScript provides a few more ways to do this
Same as records - a function can't accept an arbitrary constructor shared by two variants (if you need this, it is a feature called polymorphic variant)
Unlike branching control flow of if else
, which is O(n) (linear), a Reason switch
over a variant
is compiled to a constant-time format O(1) ??? how
- Homogeneous
- Immutable
- Fast at prepending
Reason lists are simple, singly linked lists
Favours using the following attributes:
~following` preceeding EDIT
let myList: list(int) = [1,2,3];
let anotherList = [0, ...myList]; /* List.cons */
The preceeding code does not mutate, and is in constant time (??? one function call of like Head(val, Head(...))
[a, ...b, ...c] /* syntax error, use List.concat instead */
Using an arbitrary item in the middle of a list is discourages, performance & overheard is O(n).
switch
is usually used to access list items:
let message =
switch (myList) {
| [] => "Its empty"
| [a, ...rest] => "The head of the list is" ++ sting_of_int(a)
};
An empty list is a parameter-less variant - which compiles to an integer
Might later create a list data structure with all-around fast perf
Like lists, except:
- mutable
- fast Random-access & updates
- fix-sized on native (flexible on JavaScript)
let myArray = [|"hi", "there"|];
Stand lib Array and ArrayLabel module. JS compilation has JS.Array bindings API.
let myArray: array(string) = [|"hi", "there"|];
let firstItem = myArray[0]; /* "Hello" */
myArray[0] = "hey";
/* overwrote item 0 */
The above is syntactic sugar for Array.get/Array.set
Reason and JavaScript arrays map against each other - even if fix-sized on native.
Simple:
let greet = (name) => "Hello" ++ name;
Usage:
greet("world!");
Multi arguments comma separated, multi expression functions surrounded in {...}
A (mathematical) function always has an argument, but in a program we may have a function only for side effects.
Reason functions always have arguments, ()
is a value called "unit"
let addCoordinates = (~x, ~y) => { ... }
addCoordinates(~x=5, ~y=6);
Curried therefore (??? why does currying provide this) arguments can be in any order
let drawCircle = (~radius as r, ~color as c) => { ... }
Note: ~radius
on its own, is punning shorthand
Reason functions can automatically be partially called:
let add = (x, y) => x + y;
let addFive = add(5);
let eleven = addFive(6);
The preceeding is syntactic sugar for:
let add (x) => (y) => x + y;
OCaml optimizes this to avoid unnecessary function allocation
let drawCircle = (~color, ~radius=?, ()) => {
setColor(color);
switch (radius) {
| None => startAt(1,1)
| Some(r_) => startAt(r_, r)
};
radius is obviously being wrapped in Option
Why is there a unit?
Because drawCircle
has variable arguments, given a single argument its unclear if an invocation is partial and awaiting a radius, or the programmer's intention was to use the default, thus "unit" is a kind of delimiter:
let curried = drawCircle(~color);
let actualResult = drawCircle(~color, ());
OCaml will presume the optional is omitted when the positional argument is provided.
When u don't know the arguments:
let result =
switch (payloadRadius) {
| None => drawCircle(~color, ())
| Some(r) => drawCircle(~color, ~radius=r, ())
};
This is tedius, hence shorthand:
let result = drawCircle(~color, ~radius=?payloadRadius, ());
let drawCircle = (~radius=1, ~color, ()) => {
setColor(color);
startAt(radius, radius);
};
By default, a value can't see a binding that points to it (????), but including the rec keyword in a let binding makes this possible.
rec
lets functions see/call themselves:
let rec neverTerminate = () => neverTerminate();
use rec
with and
:
let rec callSecond = () => callFirst() /* No semi colon */
and callFirst = () => callSecond();
/* anonymous function. Listed for completeness only */
(x) => (y) => 1;
/* sugar for the above */
(x, y) => 1;
/* assign to a name */
let add = (x, y) => 1;
/* labeled */
let add = (~first as x, ~second as y) => x + y;
/* with punning sugar */
let add = (~first, ~second) => first + second;
/* labeled with default value */
let add = (~first as x=1, ~second as y=2) => x + y;
/* with punning */
let add = (~first=1, ~second=2) => first + second;
/* optional */
let add = (~first as x=?, ~second as y=?) => switch (x) {...};
/* with punning */
let add = (~first=?, ~second=?) => switch (first) {...};
With type annotation:
/* anonymous function */
(x: int) => (y: int): int => 1;
/* sugar for the above */
(x: int, y: int): int => 1;
/* assign to a name */
let add = (x: int, y: int): int => 1;
/* labeled */
let add = (~first as x: int, ~second as y: int) : int => x + y;
/* with punning sugar */
let add = (~first: int, ~second: int) : int => first + second;
/* labeled with default value */
let add = (~first as x: int=1, ~second as y: int=2) : int => x + y;
/* with punning sugar */
let add = (~first: int=1, ~second: int=2) : int => first + second;
/* optional */
let add = (~first as x: option(int)=?, ~second as y: option(int)=?) : int => switch (x) {...};
/* with punning sugar */
/* note that the caller would pass an `int`, not `option int` */
/* Inside the function, `first` and `second` are `option int`. */
let add = (~first: option(int)=?, ~second: option(int)=?) : int => switch (first) {...};
/* anonymous application. Listed for completeness only */
add(x)(y);
/* sugar for the above */
add(x, y);
/* labeled */
add(~first=1, ~second=2);
/* with punning sugar */
add(~first, ~second);
/* application with default value. Same as normal application */
add(~first=1, ~second=2);
/* explicit optional application */
add(~first=?Some(1), ~second=?Some(2));
/* with punning */
add(~first?, ~second?);
Application w/type annotation
/* anonymous application */
add(x: int)(y: int);
/* labeled */
add(~first=1: int, ~second=2: int);
/* with punning sugar */
add(~first: int, ~second: int);
/* application with default value. Same as normal application */
add(~first=1: int, ~second=2: int);
/* explicit optional application */
add(~first=?Some(1): option(int), ~second=?Some(2): option(int));
/* with punning sugar */
add(~first: option(int)?, ~second: option(int)?);
/* first arg type, second arg type, return type */
type foo = int => int => int;
/* sugar for the above */
type foo = (int, int) => int;
/* labeled */
type foo = (~first: int, ~second: int) => int;
/* labeled with default value */
type foo = (~first: int=?, ~second: int=?) => int;
Annotate impl. in *.re
file
let add: int => int => int;
/* sugar for above */
let add: (int, int) => int;
Note this is not exporting the type - it is only annotating an exiting value bar
in the implementation file
Are expressions (yay!)
let message = if (isMorning) {
"Good morning!"
} else {
"Hello!"
}
The final else, implicitly gives the value for unit aka (()
)
e.g. this will cause a type error (both int
and unit
):
let result = if (showMenu) { 1 + 2};
Ternaries
let message = isMorning ? "yay" : "nay";
- Pattern matching supercedes much if/else/ternaries
- Use
if-else
if you have 2 branches
Reason ternary is sugar for switch
over bool
Types can accept paramters(~generics in other languages)
Approx. type is a function, that takes args and returns a new type
Parameters start with '
Why? to kill duplications.
Before:
type intVector = (int, int, int);
type floatVector = (float, float, float);
let buddy: intVector = (1,2,3);
After:
type vector('a) = ('a, 'a, 'a);
type intVectorAlias = vector(int);
let buddy: intVectorAlias = (1,2,3);
let buddy: vector(floact) = (0.1, 0.2, 0.3);
With inference:
let buddy = (1,2,3); /* vector(int) */
let greetings = ["a", "b"]; /* list(string) */
If types didn't accept params, the standard library would need to define listOfString
, listOfInt
etc
Types can receive multiple arguments and be composed:
type result('a, 'b) =
| Ok('a)
| Error('b);
type payload = {data: string };
type resultCache('errorType) = list(result(payload, 'errorType));
let payloadResults: resultCache(string) = [
Ok({data: "Nice"}),
Ok({data: "Also nice"}),
Error("Some sort of error")
]
type student = {taughtBy: teacher}
and techer = {students: list(student)};
- Types are pervasive through out Reason - thus unavoidable. Type-level functions allow shorthand expressions (
list(students)
) AND it allows the creation of small boilerplace (listOfStudents
) - It creates a trade-off space between fancy types and simple types
let someInts = (10, 20);
let (first, second) = someInts;
type person ={name: string, age: int};
let guy = {name: "Guy" age: 30};
let {name, age:years} = guy;
/* Created name, and years (aliased age) */
let fn = (~person as {name}) => { /* can use name here */ };
Pre-requisite reading is Variant
Consider a variant:
type payload =
| Bad(int)
| Ok(string)
| None;
While using switch
you can "destructure" data into msg
and errorCode
:
let data = Good("Product shipped");
let message =
switch (data) {
| Good(msg) => "Yay" ++ msg
| Bad(errorCode) => "Error encountered" ++ string_of_int(errorCode)
};
But this is more than desctructing, as the compiler will complain:
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
NoResult
This is pattern matching, conditional and exhaustive.
Note only literals can be a pattern!
The following behaves unexpectedly - it assumes you are matching on any string, and binding that to the name myMessage
let myMessage = "Hello";
switch (greeting) {
| myMessage => print_endline("Hi to you")
};
| _ => /* underscore is a catch all */
if
sugar:
let message =
switch (data) {
| GoodResult(theMessage) => ...
| BadResult(errorCode) when isServerError(errorCode) => ...
| BadResult(errorCode) => ... /* otherwise */
| NoResult => ...
};
switch (List.find((i) => i === theItem, myItems)) {
| item => print_endline(item)
| exception Not_found => print_endline("No such item found!")
};
switch (student) {
| {name: "Jane" | "Joe"} => ...
| {name: "Bob", Job: Progammer({fullTime: Yes | Maybe})} => ...
};
You can put a pattern anywhere you'd put a variable declaration:
let (Left v | Right v) = i;
??? what does this mean? This (<type> <variable>
) doesn't look like previous patterns (<type>
). And I can't guess what is going to be resolved against Left v | Right v
, on which will then be assigned the value ofi
.
Flatten your pattern-match whenever you can
Don't abuse fall-through, try to only use for infinite possibilities
This is tempting:
let optionBoolToJsBoolean = (opt) =>
switch (opt) {
| Some(true) => Js.true_
| _ => Js.false_
};
But it is non-exhuastive - best to be explicit:
let optionBoolToJsBoolean = (opt) =>
switch (opt) {
| Some(true) => Js.true_
| Some(false)
| None => Js.false_
};
Pattern matching corresponds to case analysis in maths - it brings structure to if-else logic.
Use ref
to allow *mutation
let foo = ref(5);
Get value of ref
using ^
:
let five = foo^; /* 5 */
- Not actually mutation - sugar for the mutable record in the standard library
let foo = {contents: 5};
let five = foo.contents;
foo.contents = 6;
Before reaching for ref
, you can "mutate" by overriding let bindings
let startVal = 1;
let endVal = 3;
for (i in startVal to endVal) {
}
for (i in endVal downto startValue) {
}
while (testCondition) {
- Reason JSX is not tied to ReactJS; that translate to function calls
Capitalized tag:
<Comp foo={bar} />
becomes
([@JSX] Comp.make(~foo=bar, ~children=[], ()));
Uncapitalized tag:
<div foo={bar}> child1 child2 </div>
becomes
([@JSX] div(~foo=bar, ~children=[child1, child2], ()));
<Comp> ...foo </Comp>
This passes the value foo without wrapping:
([@JSX] Comp.createElement(~foo=bar, children=foo, ()));
JSX tag that shows most features:
<MyComponent
booleanAttribute={true}
stringAttribute="string"
intAttribute=1
forcedOptional=?{Some("hello")}
onClick={reduce(handleClick)}>
<div> {ReasonReact.stringToElement("hello")} </div>
</MyComponent>
- Attributes and children don't mandate
{}
, but they are shown for easy of learning- refmt nukes some and turns some into parens - No support for prop spread
- Punning
<input checked />
does not desugar to<input checked=true />
but<input checked=checked
/>
[@JSX]
attribute is a hook for potential ppx macros (????) to spot a function wanting to format as jsx. Once you spot the function to you can turn it into any other expression (????). Thus anyone can use JSX syntax without needing a specific librry e.g. ReasonReact.
JSX calls supports the features of labeled functions: optional, explicitly passed option and optional w/default
What’s the point? Hopefully these were nice demonstrations that the discussion around syntax could a bit more advanced than the typical one surrounding whitespace correction! Especially the first and last examples: part of what Reason tries to accomplish is to insert a layer of iteration speed in-between the slow-moving language semantics and the fast-moving userland libraries. We can observe users and capture relevant patterns, upstream some into the macros, then upstream some of that into the syntax, and finally upstream the worthy parts into the language itself and remove the need for certain libraries and tools in the first place. We can also potentially go the other way around: remove deprecated features by downstreaming them into the syntax, then downstream them into macros, then into libraries, then nothing.
-
Macros (???) + lib-agnostic JSX means every library can have JSX without hassle. Add some visual familiaritie to underlying language without compromising on semantics. Reason wants to let more ppl use OCaml, while discarding debates around syntax and formatting (??? by allowing macros to restructure as libs see fit?)
-
How does react handle single vs multiple children? w/runtime logic, syntax transforms and variadic argument detection/marking (see here). Such dynamic usage is complicated. Reason uses children spread to make wrapping (or not) explicit.
aka Foreign function interface aka interop is how Reason communcates with other languages like C or JavaScript.
external myCFunc int => string = "theCFunc";
BuckleScript specific external binding:
[@bs.val] external getElementsByClassname : string => array(Dom.element) = "document.getElementsByClassName";
Use external value/function binding as if it was a normal let binding
Javascript devs need to learn about BuckleScript externals
SMALL ASIDE ON EXTERNAL
external
is like let, except the body is a string. This string usually has specific meanings depending on context:
- native OCAML: usually points to a C function
- BuckleScript: usually decorated with
[@bs.blahblah]
attributes
Once declared, you can use an external
as a normal value
BuckleScript externals
are inlined into their callers during compilation and are competely erased. In practive, when you bind a JavaScript function on the BS side and use it, all trace of such binding disappear from the output (??? compiled file?).
Note Need to use externals
and [bs.blahblah]
(??? typo ??? Why?) attributes in the interface files. Otherwise the inlining won't happen.
Reason takes interop seriously to help gradual conversion. FFI is good to intergrating with dirty existing code.
Used in exceptional cases, throw a variant
let getItem = (theList) =>
if (...) {
/* return the found item here */
} else {
raise(Not_found)
};
let result =
try (getItem([1, 2, 3])) {
| Not_found => 0 /* Default value if getItem throws */
};
- OCaml/Reason throwing is cheap, Javasript is heavy. These days, OCaml/Reason standard lib come with option returning functions instead. (!!! Thus modern == !throw)
Mostly use record for name:values, this is unlike JavaScript objects.
-
Not needed
-
closed version, must have this exact (???public???) shape
type tesla = {
.
color: string
};
- open version, can have additional values & methods
type car('a) = {
..
color: string
} as 'a;
- polymorphic (requires type param)
type tesla = {.
drive: int => int
};
let obj: tesla = {
val hasEnvy = ref(false);
pub drive = (speed) => {
this#enableEnvy(true);
speed
};
pri enableEnvy = (envy) => hasEnvy := envy
};
- In Reason,
this
always works correctly
type tesla('a) = {
..
drive: int => int
} as 'a;
let obj: tesla({. drive: int => int, doYouWant: unit => bool}) = {
val hasEnvy = ref(false); // ??? ref means create on `this`?
pub drive = (speed) => {
this#enableEnvy(true);
speed
};
pub doYouWant = () => hasEnvy^; // bool?
pri enableEnvy = (envy) => hasEnvy := envy
};
obj#doYouWant();
##
field access#=
field mutations- always
type
Js.t
- compile to JS objects
- Like mini inline-files
module School = {
type profession = Teacher | Director;
let person1 = Teacher;
let getProfession = (person) =>
switch(person) => {
| Teacher => "Teach"
| Director => "Direct"
};
};
- contents (values, methods & types) accessed using
let bob: School.profession = School.Teacher
module A = {
module B = {
let c = "hello";
};
};
let message = A.B.c;
#### Local open
let message =
School.(
switch(person1) {
| Teacher => "Hello teacher!"
| Director => "Hellow director!"
}
);
open School
include
statically spreads contents into new module
open
(content into your scope so no prefix) != include
(copies over definition of a module statically, then opens)
- Implicit module creation is expressive:
- Signature = module's type
- Can be explicit in
.rei
file
/* Picking up previous section's example */
module type EstablishmentType = {
type profession;
let getProfession: profession => string;
};
- can have more contents than declared. You can have private enforced contents (implementation details that must exist)
profession
is abstract - doesn't matter what it is, but it must be the same in any module that falls under the same interface.
module Company : EstablishmentType = {
type profession = CEO | Designer | Engie;
let getProfession = (person) => ... ;
let person1 = ...;
let person2 = ...;
};
- Also hides underlying concrete type
- Extend with include(
module type of BaseComponent
)
Modules are different layer, so functors are a special case.
- use
module
(notlet
) - input/output modules
- require annotating arguments
module type Comparable = {
type t;
let equal: (t,t) => bool;
};
/* this is returned */
module MakeSet (Item: Comparable) => {
type backingType = list(Item.t);
let empty = [];
let add = (currentSet: backingType, newItem: Item.t) : backingType =>
if (List.exists((x) => Item.equal(x, newItem), currentSet)) {
currentSet
} else {
[
newItem,
...currentSet
]
};
}
- like module types, functor types control visibility.
- type syntax same for Functors & Function, except types capitalized (as modules)
module type COmparable = ...
module type MakeSetType = (Item: Comparable) => {
type backingType;
let empty: backingType;
let add: (backingType, Item.t) => backingType;
};
module MakeSet: MakeSetType = (Item: Comparable) => {
...
};
- Its different - thus it has non-expected behavior e.g. Cant pass into a tuple
- Use more normal features like record/function
- BuckleScript supports js promises
let doSomethingToAPromise = (somePromise) => {
somePromise
|> Js.Promise.then_(value => {
Js.log(value);
Js.Promise.resolve(value + 2)
})
|> Js.Promise.catch(err => {
Js.log2("Failure!!", err);
Js.Promise.resolve(-2)
})
}
- no async/await yet