More Related Content Similar to Roman Schejbal: From Madness To Reason (20) Roman Schejbal: From Madness To Reason10. Compiler Errors
We've found a bug for you!
/workspace/reason/src/Student.re 20:30
19 │ let age = 2;
20 │ let greeting = greet(Student(1));
21 │
22 │ Js.log(greeting);
This has type:
int
But somewhere wanted:
string
You can convert a int to a string with string_of_int.
15. let name = "Reason";
let age = 2;
let greet = () => "Hello World!";
Language Basics
16. Algebraic data types
type name = option(string); /* None | Some(“Reason”) */
type state = { items: array(string), length: number };
type schoolPerson =
| Teacher
| Director
| Student(string);
17. type schoolPerson = Teacher | Director | Student(string);
let greet = stranger =>
switch (stranger) {
| Teacher => "Hey professor!"
| Director => "Hello director."
| Student("Richard") => "Still here Ricky?"
| Student(anyOtherName) => "Hey, " ++ anyOtherName ++ "."
};
greet(Teacher); /* Hey professor! */
greet(Student("OCaml")); /* Hey, OCaml. */
Variants & Pattern Matching
18. Pattern Matching Exhaustiveness
Warning number 8
/workspace/reason/src/Student.re 7:3-12:3
5 │
6 │ let greet = stranger =>
7 │ switch (stranger) {
8 │ | Teacher => "Hey professor!"
. │ ...
11 │ /* | Student(anyOtherName) => "Hey " ++ anyOtherName ++ "." */
12 │ };
13 │
14 │ print_endline(greet(Teacher));
You forgot to handle a possible value here, for example:
Student ""
20. type userT = option(string);
let userOne = Some("X");
let userTwo = None;
let printPlayerName = player =>
switch (player) {
| Some(name) => "Logged in as " ++ name
| None => "Not logged in"
};
Option type
21. Why Reason then? 🤔
“We want people to be
able to use powerful, well-
typed languages at work,
not just in their free time.”
- Jordan Walke
27. flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| package.json
| bsconfig.json
| yarn.lock
28. flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| package.json
| bsconfig.json
| yarn.lock
yarn add bs-platform reason-react
29. bsconfig.json
{
"name": "my-project",
"namespace": true,
"sources": [
{
"dir": "src",
"subdirs": true
}
],
"package-specs": {
"module": "es6-global",
"in-source": true
},
"suffix": ".bs.js",
"bs-dependencies": ["reason-react"],
"reason": {
"react-jsx": 2
}
}
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| package.json
| bsconfig.json
| yarn.lock
30. package.json
{
"name": "from-react-to-reason",
"version": "0.1.0",
"private": true,
"dependencies": {
"bs-platform": "^3.0.0",
"flow-bin": "^0.72.0",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-redux": "^5.0.7",
"react-scripts": "1.1.4",
"reason-react": "^0.4.1",
"redux": "^4.0.0",
"redux-thunk": "^2.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"bsb": "bsb -make-world -w"
}
}
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| package.json
| bsconfig.json
| yarn.lock
"bsb": "bsb -make-world -w"
31. src/Types.re
[@bs.deriving jsConverter]
type flightInfo = {
airline: string,
price: int,
duration: float,
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
Record Type
32. src/Types.bs.js
// Generated by BUCKLESCRIPT VERSION 3.0.0, PLEASE EDIT WITH CARE
function flightInfoToJs(param) {
return {
airline: param[ /* airline */0],
price: param[ /* price */1],
duration: param[ /* duration */2]
};
}
function flightInfoFromJs(param) {
return /* record */[
/* airline */param.airline,
/* price */param.price,
/* duration */param.duration
];
}
export {
flightInfoToJs ,
flightInfoFromJs ,
}
/* No side effect */
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
33. src/state/reducer/index.js
import {
type actionT,
REQUEST_FLIGHTS,
RECEIVE_FLIGHTS,
FAILURE_FLIGHTS,
} from ' ../action';
const initialState = {
loading: false,
flights: [],
error: null,
};
export default (state = initialState, action) => {
switch (action.type) {
case REQUEST_FLIGHTS:
return { ...state, loading: true };
case RECEIVE_FLIGHTS:
return { ...state, loading: false, flights: action.payload.flights };
case FAILURE_FLIGHTS:
return { ...state, error: action.payload.error };
default:
return state;
}
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
34. src/state/reducer/Reducer.re
type flightsData = array(Types.flightInfo);
[@bs.deriving jsConverter]
type state = {
loading: bool,
flights: flightsData,
error: option(Js.Exn.t)
};
let initialState = {
loading: false,
flights: [ ||],
error: None
};
let default = (state, action) =>
switch (action ##_type) {
| "REQUEST_FLIGHTS" => { ...state, loading: true}
| "RECEIVE_FLIGHTS" => {
...state,
flights: Array.map(Types.flightInfoFromJs, action ##payload ##flights),
loading: false,
}
| "FAILURE_FLIGHTS" => { ...state, error: action ##payload ##error}
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
36. src/state/reducer/Reducer.re
type flightsData = array(Types.flightInfo);
type state = {
loading: bool,
flights: flightsData,
error: option(Js.Exn.t)
};
let initialState = {
loading: false,
flights: [ ||],
error: None
};
let default = (state, action) =>
switch (action ##_type) {
| "REQUEST_FLIGHTS" => { ...state, loading: true}
| "RECEIVE_FLIGHTS" => {
...state,
flights: Array.map(Types.flightInfoFromJs, action ##payload ##flights),
loading: false,
}
| "FAILURE_FLIGHTS" => { ...state, error: action ##payload ##error}
| _ => state
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
| _ => state
37. src/state/action/index.js
export const REQUEST_FLIGHTS = 'REQUEST_FLIGHTS';
export const RECEIVE_FLIGHTS = 'RECEIVE_FLIGHTS';
export const FAILURE_FLIGHTS = 'FAILURE_FLIGHTS';
export const fetchFlights = (where, when) => (
dispatch
) => {
dispatch(requestFlights());
setTimeout(
() =>
dispatch(
receiveFlights([
{ airline: 'British Airways', price: 390, duration: 3.5 },
{ airline: 'KLM', price: 340, duration: 2.2 }
])
),
3000
);
};
const requestFlights = () => ({
type: REQUEST_FLIGHTS
});
const receiveFlights = (flights: Array<Object>) => ({
type: RECEIVE_FLIGHTS,
payload: { flights }
});
const failureFlights = (error: Error) => ({
type: FAILURE_FLIGHTS,
payload: { error }
});
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
38. src/state/action/Actions.re
let fetchFlights = (from, where, when) =>
(. dispatch) => {
dispatch(requestFlights(from, where, when));
Js.Global.setTimeout(
() =>
dispatch(
receiveFlights( [|
{airline: "British Airways", price: 390, duration: 3.5},
{airline: "KLM", price: 340, duration: 2.2},
|]),
),
3000,
);
};
let requestFlights = (from, where, when) => {
"type": "REQUEST_FLIGHTS",
"payload": {
"from": from,
"where": where,
"when": when,
},
};
let receiveFlights = flights => {
"type": "RECEIVE_FLIGHTS",
"payload": {
"flights": flights,
},
};
...
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
39. src/state/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducer/Reducer.bs';
const initialState = undefined;
export default () =>
createStore(
rootReducer,
applyMiddleware(thunk),
initialState
);
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
40. What have we gained?
1. Can’t mess up our state shape
Can we get more?
41. src/state/action/Actions.re
let fetchFlights = (from, where, when) =>
(. dispatch) => {
dispatch(requestFlights(from, where, when));
Js.Global.setTimeout(
() =>
dispatch(
receiveFlights( [|
{airline: "British Airways", price: 390, duration: 3.5},
{airline: "KLM", price: 340, duration: 2.2},
|]),
),
3000,
);
};
let requestFlights = (from, where, when) => {
"type": "REQUEST_FLIGHTS",
"payload": {
"from": from,
"where": where,
"when": when,
},
};
let receiveFlights = flights => {
"type": "RECEIVE_FLIGHTS",
"payload": {
"flights": flights,
},
};
let failureFlights = error => {
"type": "FAILURE_FLIGHTS",
"payload": {
"error": error,
},
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
42. src/state/action/Actions.re
type from = string;
type where = string;
type when = string;
[@bs.deriving accessors]
type action =
| RequestFlights(from, where, when)
| ReceiveFlights(array(Types.flightInfo))
| FailureFlights(Js.Exn.t);
let fetchFlights = (from, where, when) =>
(. dispatch) => {
dispatch(RequestFlights(from, where, when));
Js.Global.setTimeout(
() =>
dispatch(
ReceiveFlights( [|
{airline: "British Airways", price: 390, duration: 3.5},
{airline: "KLM", price: 340, duration: 2.2},
|]),
),
3000,
);
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
43. src/state/action/Reducer.re
type flightsData = array(Types.flightInfo);
[@bs.deriving jsConverter]
type state = {
loading: bool,
flights: flightsData,
error: option(Js.Exn.t),
};
let initialState = {
loading: false,
flights: [ ||],
error: None,
};
let default = (state, action) => {
Actions.(
switch (action) {
| RequestFlights(_, _, _) => { ...state, loading: true}
| ReceiveFlights(flights) => { ...state, flights}
| FailureFlights(error) => { ...state, error: Some(error)}
}
)
};
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
44. src/state/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducer/Reducer.bs';
const fromReasonToJs = store => next => action => {
if (action.tag !== undefined) {
// reason action
const { tag } = action;
action = {
type: `REASON_ACTION_${tag}`,
tag,
reasonAction: action
};
}
next(action);
};
const reasonReducer = reducer => (state, action) => {
if (action.reasonAction) return reducer(state, action.reasonAction);
return reducer(state, { ...action, tag: -1 });
};
const initialState = undefined;
export default () =>
createStore(
reasonReducer(rootReducer),
applyMiddleware(thunk, fromReasonToJs),
initialState
);
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
45. src/components/FlightList.re
open ReasonReact;
let component = statelessComponent("FlightsList");
let make = (~data, _children) => {
...component,
render: _self =>
<div>
(
Array.map(
flightInfo =>
<div>
<h2> (string(flightInfo.airline)) </h2>
<p>
(string("Price: $" ++ string_of_int(flightInfo.price)))
(
string(
" | Duration: "
++ string_of_float(flightInfo.duration)
++ "hours",
)
)
</p>
</div>,
data,
)
|> array
)
</div>,
};
let default =
wrapReasonForJs(~component, jsProps =>
make(~data=jsProps ##data, jsProps ##children)
);
flight-search/
|- public/
|- node_modules/
|- public/
|- src/
| |- components/
| | |- flight-list.js
| | | |- FlightList.js
| |- state/
| | |- action/
| | | |- index.js
| | | |- Actions.re
| | |- reducer/
| | | |- index.js
| | | |- Reducer.re
| | |- store.js
| |- App.js
| |- index.js
| |- Types.re
| package.json
| bsconfig.json
| yarn.lock
46. What have we gained?
1. Can’t mess up our state shape
2. Can’t forget to handle actions
3. Can’t forget to handle edge cases
47. Not covered…
• Rendering a JS component from a Reason component
• Using Variants for async stuff i.e.
[@bs.module]
external myJSComponent : ReasonReact.reactClass = “./my-js-component“;
let make = (~name: string, ~age: option(int)=?, children) =>
ReasonReact.wrapJsForReason(
~reactClass=myJSComponent,
~props={"name": name, "age": Js.Nullable.fromOption(age)},
children,
);
type async('a) =
| Idle
| Loading
| Success('a)
| Error(Js.Exn.t);
type action =
| Tick(unit)
| Flights(Types.(async(array(flightInfo))));
http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html