genkio/blog

Get started with Reason

genkio opened this issue · 0 comments

study notes of Nik Graf's Get Started with Reason course on egghead .

TOC

  • reason-cli and rtop
  • basic data types and operators
  • let binding, type inference and type aliases
  • scope
  • if else and switch
  • records
  • variants and pattern matching variants using switch
  • eliminate illegal states with variants
  • type option
  • declare functions
  • tuple
  • list
  • array
  • reference equality vs structural (deep) equality
  • pattern matching using switch
  • type parameters
  • mutable let binding
  • exceptions
  • imperative loops (for & while)
  • modules
  • first steps using Reason with BuckleScript

reason-cli and rtop

installation
npm install -g reason-cli@latest-macos

start with rtop
$ rtop ctrl r for searching histories.

basic data types and operators

float type

  • +. for adding floats.
  • use utility functions like float_of_int (destinationType_of_sourceType) for type conversion.

string and char type

  • ++ for string concatenation.
  • string must in double quotes, single quote (with single character) is for char type.

unit type

  • (); which is equivalent of undefined in JavaScript.
  • use for functions with side-effects, for example print_int(42); returns type unit.

let binding, type inference and type aliases

let binding

  • similar to variable declaration.
  • let <name>:<type> = <expression>;
  • let binding is immutable, however can be shadowed by binding same name with different expression (expression with different types would also be fine).

type alias

  • type score = int; then let x:score = 100; or type scores = list(score);
  • type alias can also be shadowed.

scope

lexical scoping

  • create local scope using curly braces { 42; };
  • last state will automatically be returned in multi-lines block.
{
  print_endline("hello");
  42;
};
  • let binding defined inside of scoped can't be accessed outside, and it can shadow the outside binding.
  • bind result of a scope to a name
let meaningOfLife = {
  let a = 40;
  let b = 1;
  a + b + 1;
};
let meaningOfLife: score = 42;

if else and switch

if, is an expression, and can be bound to let binding

let isMorning = true;
let greeting = if (isMorning) { "Good Morning"; } else { "Hello"; };
greeting;
- : string = "Good Morning"

every branch of if else must be evaluated to the same type

let greeting = if (isMorning) { "Good Morning"; } else { 42; };
Error: This expression has type score but an expression was expected of type string

if can be used with functions with side-effects.
if (isMorning) { print_endline("Good Morning") }; same as if (isMorning) { print_endline("Good Morning") } else {()};

switch pattern

/*
switch (<value>) {
  | <pattern1> => <case1>
  | <pattern2> => <case2>
};
*/

switch with fall through handling.

let lamp = 
  switch (1) {
  | 0 => "off"
  | 1 => "on"
  | _ => "off"
};
let lamp: string = "on";

alternative of handling fall through.

let lamp = 
switch (1) {
| 0 => "off"
| 1 => "on"
| other => {
    print_endline("Invalid value: " ++ string_of_int(other));
    "off";
  }
};

records

stores various types of data into one, and reference them by name.

type person = { name: string, age: int };
- : person = { name: "Joe", age: 35 }

accessing the records field, same as JavaScript, with dot notation.

let joe = { name: "Joe", age: 35 };
joe.name;

records fields destructing, again, same as JavaScript

let { name: joesname, age: joesage } = joe;
/*and, punning shorthand syntax*/
let { name } = joe;

use punning shorthand to create records (just like es6 :)

let name = "joe";
let age = 35;
let joe = { name, age };

immutable update records with spread operator.

let joe = { ...joe, name: "john" };

mutable update records with = operator.

/*creating mutable field*/
type animal = { species: string, mutable scary: bool };
let lion = { species: "Lion", scary: true }; 
/*mutate field value*/
lion.scary = false;

create a record without declaring its type by declaring its fields using pub keyword.

let anna = { pub name = "Anna", pub eyeColor = "brown" };
/*access fields*/
anna#name;

variants and pattern matching variants using switch

similar to enum yet more powerful.

type answer =
  | Yes /*this is known as constructor, and it has to be capital cased*/
  | No
  | Maybe;

enrich the type system together with switch

let isReasonGreat = Yes;
let message =
  switch (isReasonGreat) {
    | Yes => "Yay"
    | No => "Nope"
    | Maybe => "Maybe" /*if any one of the constructor is missing here, a type error will be shown*/
  };
let message: string = "Yay";

variant constructor can be used to hold data

type item =
  | Note(string)
  | Todo(string, bool);

let newTodo = Todo("new todo", false);

switch(newTodo) {
  | Note(text) => text
  | Todo("new todo", false) => "exact match found";
  | Todo(text, isDone) => text ++ " is done: " ++ string_of_bool(isDone);
};
- : string = "new todo is done: false"

eliminate illegal states with variants

type request = 
  | Loading
  | Error
  | Success(string);

let state = Error;

let ui =
switch(state) {
  | Loading => "loading..."
  | Error => "Something went wrong"
  | Success("") => "Your name is missing"
  | Success(name) => "Your name is " ++ name
};
let ui: string = "Something went wrong";

type option

None and Some

let meaningOfLife = None;
let meaningOfLife = 42;
let message = 
  switch(meaningOfLife) {
    | None => "Sadly I don't know"
    | Some(value) => "The meaning of life is :" ++ value
};

declare functions

starting simple, exact same as JavaScript

let plusOne = x => x + 1;
plusOne(3);
- : int = 4

with type declared

let add = (x: int, y: int): int => x + y;
add(2, 2);
- : int = 4

with block

let add = (x, y) => {
  let z = float_of_int(x);
  y +. z; /*as explained above, this will be returned automatically, AND there's actually no return keyword in Reason*/
};

partial argument, partial application and currying

let add = (x, y) => x + y;
let addTwo = add(2);
addTwo(2);
- : int = 4
/*or*/
add(2)(2);

/*another example*/
let numbers = [4, 11, 5];
let add = (x, y) => x + y;
List.map(x => add(4, x), numbers);
/*or*/
List.map(add(4), numbers);
- : list(int) = [8, 15, 9]

label parameter

let name = (~firstName, ~lastName) => firstName ++ " " ++ lastName;
name(~lastName="doe", ~firstName="joe");
- : string = "joe doe"

/*works really well with partial application*/
name(~lastName="doe")(~firstName="joe");

/*as syntax*/
let name = (~firstName as f, ~lastName as l) => f ++ " " ++ l;

/*with default value*/
let name = (~firstName, ~lastName="doe") => firstName ++ " " ++ lastName;

/*calling function with default parameter to get the output value instead of curried function*/
let name = (~firstName, ~lastName="doe", ()) => firstName ++ " " ++ lastName;
name(~firstName="joe", ());

/*with parameter as option type*/
let name = (~firstName, ~lastName=?, ()) => {
  switch(lastName) {
    | Some(value) => firstName ++ " " ++ value
    | None => firstName
  };
};
name("joe", ());
name(~firstName="joe", ~lastName="doe", ());

chain functions using the pipe/reverse-application operator

/*normally you would do*/
let info = String.capitalize(String.lowercase("ALERT"));

/*with Reason*/
let info = "ALERT" |> String.lowercases |> String.capitalize;

/*even cooler*/
[8,3,6,1]
  |> List.sort(compare)
  |> List.rev
  |> List.find(x => x < 4);

recursive function

tuple

the immutable and fixed position data structure that can hold values of different types.

/*start simple*/
("joe", 42);

/*with different types*/
(42, true, "joe");

/*nested*/
((4, 3), 32);

/*with custom type*/
type point = (int, int);
let myPoint: point = (4, 3);

/*util functions to access first and second element*/
fst(myPoint);
snd(myPoint);

/*destructuring*/
let (x, y, z) = (1, 2, 3);

/*partial destructuring*/
let (_, _, z) = (1, 2, 3);

list

the homogeneous (can't have items with different types) and immutable data structure

/*start simple*/
let myList = [1, 2, 3];

/*create list out of variants*/
type item =
  | Measurement(int)
  | Note(string);
[Measurement(2), Note("hello")];

/*append list with util function*/
List.append([1,2,3], [4,5]);

/*append list with shorthand syntax*/
[1,2] @ [3,4];

/*with spread operator which only can do append not prepend*/
[0, ...[1,2,3]];

/*access the nth item*/
List.nth([2, 3], 0);

/*access item with switch*/
let message = 
  switch(myList) {
    | [] => "This list is empty"
    | [head, ...rest] => "The head of the list is " + string_of_int(head)
};

/*util functions*/
List.map(x => x + 1, [1, 2, 3]);

array

like list but mutable.

/*create array*/
let myArray = [|2, 3|];

/*accessing and mutating the element*/
myArray[0];
myArray[0] = 1;

reference equality vs structural (deep) equality

== structural equality works all data structures, it does deep comparison so it works with nested structure as well.
!= checks if two structures aren't equal.
=== checks if comparators share the same memory locations, which does not use that often, shouldComponentUpdate hook maybe one of the places it could be useful.

{ name: "joe" } === { name: "joe" }
- : bool = false
let joe = { name: "joe" }
joe === joe;
- : bool = true
joe === { name: "joe" }
- : bool = true

pattern matching using switch

start simple with matching structural equality

switch("Hello") {
  | "Hello" => "English"
  | "Bonjour" => "French"
  | _ => "Unknown"
};

/*partial check*/
let myTodo = ("study Reason", false);
  switch(myTodo) {
    | (_, true) => "Congrats"
    | (_, false) => "Too bad"
};

/*or extract part of the structure with name*/
let myTodo = ("study Reason", false);
  switch(myTodo) {
    | (_, true) => "Congrats"
    | (text, false) => "Too bad, " ++ text ++ " is not finished" 
};

/*extract head and tail from list using spread operator*/
switch(["a", "b", "c"]) {
  | [head, ...tail] => print_endline(head) /*note, destrucuring list outside of switch is not recommended, as empty list can lead to runtime error*/
  | _ => print_endline("all other cases")
};

/*matching array*/
switch([|"a", "b", "c"|]) {
  | [|"a", "b", _|] => print_endline("a, b and something")
  | [|_, x, y|] => print_endline("something and " ++ x ++ " " ++ y)
  | _ => print_endline("an array")
};

/*matching records*/
type todo = {
  text: string,
  checked: bool
};
let myTodo = { text: "study Reason", checked: false };
switch(myTodo) {
  | {text, checked: true} => "Congrats, you finished: " ++ text /*panning*/
  | {text, checked: false} => "Too bad, you didn't finish: " ++ text
};

/*matching variant*/
type item = 
 | Note(string)
 | Todo(string, bool);
let myTodo = Todo("study Reason", false);
switch(myTodo) {
  | Note(text) => text
  | Todo(text, checked) => text ++ " is done: " ++ string_of_bool(checked)
};

use pipe to match multiple possible values

type request = 
  | Success(string)
  | Error(int);
switch(Error(502)) {
  | Success(result) => result
  | Error(500 | 501 | 502) => "server error"
  | Error(code) => "unknown error"
};

use custom matching logic with when keyword

let isServerError = code => code >= 500 && code <= 511;
switch(Error(502)) {
  | Success(result) => result
  | Error(code) when isServerError(code) => "server error"
  | Error(code) => "unknown error"
};

when using pattern matching with variants, try apply handling logic for every variant constructor and avoid the fall through case as well as to use when, explicitness is the key to avoid potential bugs in the program.

ternary operator, syntax sugar for boolean switching

let message = isMorning ? "Good morning" : "Hello";
/*is the syntactical sugar for*/
switch(isMorning) {
  | true => "Good morning"
  | false => "Hello"
};

type parameters

type can accept parameters similar to generics

let mylist: list(string) = ["hello", "world"];

type can be treated as function, which take in the parameter and return a new type. It helps to reduce repetition and create more generic types.

type coordinate('a) = ('a, 'a);
let locationOne: coordinate(int) = (10, 20);
let locationOne: coordinate(float) = (10.0, 20.2);

/*create our own option type*/
type ourOption('a) =
  | OurNone
  | OurSome('a);
ourSome(42);
ourSome("Reason");

/*to take multiple parameters*/
type ship('a, 'b) = {
  id: 'a,
  cargo: 'b
};
{ id: 223, cargo: ["Apple"] };

mutable let binding

/*create ref*/
let foo = ref(5);

/*mutate ref*/
foo := 6;

/*retrieve ref*/
foo^;

/*a simple example*/
let game = 
  | Menu
  | Playing
  | Gameover;
let store = ref(Playing);
store := GameOver;
store := Menu;

exceptions

raise exception

/*raise exception*/
raise(Not_found);

create exception

exception InputClosed(string);
raise(InputClosed("the stream has closed"));

handle exception

/*handle exception, exceptions are another types of variants*/
try (raise(Not_found)) {
  | Not_found => ":("
};

/*exception keyword*/
try (List.find(x => x == 42), []) {
  | item => "Found it"
  | exception Not_found => "Not found"
};

imperative loops (for & while)

for loop

for (x in 2 to 8) {
  print_int(x);
  print_string(" ");
};

for (x in 8 downto 2) {
  print_int(x);
  print_string(" ");
};

while

let x = ref(0);
while (x^ < 5) {
  print_int(x^);
  x := x^ + 1;
};

modules

a simple module

module Math = {
  let pi = 3.14;
  let add = (x, y) => x + y;
};
Math.pi;
Math.add(2, 3);

wrap type definition with module

module School = {
  type profession = 
    | Teacher
    | Director;
};
let personOne = School.Teacher;

open module, shorthands for accessing what's inside of an module

let greeting = 
School.(
  switch (personOne) {
    | Teacher => "hello teacher"
    | Director => "hello director"
  };
);

/*local open also work with other data types*/
module Circle = {
  type point = {
    x: float;
    y: float;
  };
};
let center = Circle.{x: 1.2, y: 2.3};

/*use open keyword to open module to bring module's definition to current scope, be careful though*/
open School;
let personTwo = Teacher;

extend module with keyword include

module Game = {
  type states =
    | NotStarted
    | Running
    | Won
    | Lost;
};
module VideoGame = {
  include Game;
  let isWon = state => state == Won;
};

module signature with type keyword, all signatures must be supplied, newly added signature in the child module won't be available to outside cause it's not part of the (parent) signature.

module type EstablishmentType = {
  type profession = 
    | Salesperson
    | Engineer;
  let professionDescription: profession => string;
};
module Company: EstablishmentType = {
  type profession = 
    | Salesperson
    | Engineer;
  let professionDescription = p =>
    switch (p) {
      | Salesperson => "Selling software"
      | Engineer => "Building software"
    };
  let product = "software";
};
Company.professionDescription(Company.Engineer);
Company.product /*Error: Unbound value*/

first steps using Reason with BuckleScript

install BuckleScript platform with npm install -g bs-platform

create project with bsb -init my-new-project -theme basic-reason

create your first file module.

/*Main.re*/
print_endline("Hello World");

build your module with npm run build or npm run start to build your module automatically.

run your module by node Main.bs.js