Reason in a Nutshell — Getting Started Guide

综合技术 2018-05-18 阅读原文

This tutorial intends to provide a comprehensive, but relatively short introduction to Reason.

Reason is a programming language built on top of OCaml . It provides functional and object-oriented features with type-safety and focus on performance. It was created at Facebook. Its syntax is similar to JavaScript. The intention is to make interoperation with JavaScript and adoption by JavaScript programmers easier. Reason can access both JavaScript and OCaml ecosystems. OCaml was created in 1996. It is a functional programming language with infered types.

The Reason website contains an online playground . It allows to play with the language and see how the generated JavaScript looks like. It can also convert from OCaml to Reason.

Why

  1. In JavaScript types annotation, linting or unified formatting is provided as an external depenency such as Flow, TypeScript, ESLint or Prettier. Reason provides these features out-of-the-box. This makes the development process more streamlined and convenient.
  2. Reason offers support for React with ReasonReact . It also supports JSX syntax (HTML-like syntax used in React) out-of-the-box.
  3. Reason has also the ability to generate native binaries. The generated code is performant. There is no VM overhead. It provides one binary which facilites deployment process.

How it works

Reason is compiled to OCaml’s abstract syntax tree. This makes Reason a transpiler. OCaml cannot be run directly in the browser. The AST can be converted to various targets. BuckleScript can be used to compile that AST to JavaScript. It also provides the interop between OCaml and JavaScript ecosystems.

BuckleScript is extremly fast and generates readable JavaScript. It also provides the Foreign Function Interface (FFI) to allow interoperability with the JavaScript existing libraries. Check BuckleScript benchmarks . BuckleScript is used at Facebook by the Messanger team and at Google by WebAssembly spec interpreter. Check the Bucklescript demo here. BuckleScript was created by Hongbo Zhang .

Hello Reason

We will use BuckleScript to generate a Reason project. The tool provides ready-to-use project templates known as themes .

Let’s start by installing bs-platform globally:

npm install -g bs-platform

We can now use bsb binary provided by bs-platform to generate a project scaffold. We will use basic-reason template to start with the most basic Reason project structure.

bsb -init reason-1 -theme basic-reason
Making directory reason-1 
Symlink bs-platform in /Users/zaiste/code/reason-1

Here’s the Reason directory structure generated from basic-reason template via BuckleScript:

. 
├── README.md 
├── bsconfig.json 
├── lib 
├── node_modules 
├── package.json 
└── src 
└── Demo.re

bsconfig.json contains BuckleScript configuration for a Reason project. It allows to specify files to compile via sources , BuckleScript dependencies via bs-dependencies , additional flags for the compiler and more.

Next step is to build the project. This will take Reason code and pass it through BuckleScript to generate JavaScript. By default the compiler will target Node.js.

npm run build
> reason-1@0.1.0 build /Users/zaiste/code/reason-1 
> bsb -make-world ninja: Entering directory `lib/bs' 
[3/3] Building src/Demo.mlast.d 
[1/1] Building src/Demo-MyFirstReasonml.cmj

Finally we can run our application by using node on the files generated by BuckleScript.

node src/Demo.bs.js
Hello, BuckleScript and Reason!

Syntax 101

In this section, I will go over the syntax elements that I found the peculiar, new or just different.

Modules

In Reason files are modules. There are no require or import statements as in JavaScript or similar programming languages. The module definitions must be prefixed with the module name to work externally. This feature comes from OCaml. As a result you can freely move the module files in the filesystem without the need to modify the code.

Functions

Functions are defined using let and => .

let greet = name => 
  Js.log("Hello, " ++ name "!");
greet("Zaiste");

The ++ operator is used to concatenate strings.

Function’s input arguments can be labelled. This makes the function invocation more explicit: passed-in values no longer need to follow the arguments order from the function definition. Prefixing the argument name with ~ makes it labelled.

let greet = (~name, ~location) => 
  Js.log("Hello, " ++ name "! You're in " ++ location);
greet(~location="Vienna", ~name="Zaiste")

Data Structures

Variants

A variant is a data structure that holds a value from a fixed set of values. This is also known as tagged or disjoint union or algebraic data types. Each case in a variant must be capitalised. Optionally, it can receive parameters.

type animal = 
| Dog 
| Cat 
| Bird;

Records

This is a record

let p = { 
  name: "Zaiste", 
  age: 13 
}

Records need explicit type definition.

type person = { 
  name: string, 
  age: int 
};

In the scope of a module, the type will be inherited: the p binding will be recognized as person type. Outside of a module, you can reference the type by just prefixing it with file name.

let p: Person.person = { 
  name: 'Sean', 
  age: 12 
};

There is a convention to create a module per type and name the type as t to avoid the repetition i.e. Person.t instead of Person.person .

Async Programming & Promise

There is a built-in support for Promises via BuckleScript, provided as JS.Promise module. Here's an example of making an API call using Fetch API :

Js.Promise.( 
  Fetch.fetch(endpoint) 
  |> then_(Fetch.Response.json) 
  |> then_(json => doSomethingOnResponse(json) |> resolve) 
)

You need to use then_ as then is reserved word in OCaml.

Pattern Matching

Pattern matching is a dispatch mechanism based on the shape of the provided value. In Reason, pattern matching is implemented with switch statement. It can be used with a variant type or as destructuring mechanism.

switch pet { 
| Dog => "woof" 
| Cat => "meow" 
| Bird => "chirp" 
};

We can use pattern matching for list destructuring:

let numbers = ["1", "2", "3", "4"]; 
switch numbers { 
| [] => "Empty" 
| [n1] => "Only one number: " ++ n1 
| [n1, n2] => "Only two numbers" 
| [n1, _, n3, ...rest] => "At least three numbers" 
};

Or, we can use it for record destructuring

let project = {
  name: "Huncwot", 
  size: 101101, 
  forks: 42, 
  deps: [{name: "axios"}, {name: "sqlite3"}]
}
switch project { 
| {name: "Huncwot", deps} => "Matching by `name`" 
| {location, years: [{name: "axios"}, ...rest]} => "Matching by one of `deps`" 
| project => "Any other situation" }

Optional values

option() is a built-in variant in Reason describing "nullable" values:

type option('a) = None | Some('a);

Varia

  • unit means "nothing"
  • unit => unit is a signature of a function that doesn't accept any input parameters and doesn't return any values; mostly used for callback functions

React in Reason

Hello ReactReason

ReasonReact is a Reason built-in feature for creating React applications.

Let’s create a ReasonReact project using BuckleScript and its react template.

bsb -init reasonreact-1 -theme react

This method is recommended by Reason team for scaffolding ReasonReact projects. It is also possible to use yarn with reason-scripts template for a more complete starting point.

ReasonReact provides two types of components: statelessComponent and reducerComponent . Contrary to stateless components, reducer components are stateful providing Redux-like reducers.

let s = ReasonReact.string
let component = ReasonReact.statelessComponent("App");
let make = (~message, _children) => { 
  ...component, 
  render: _self => 
    

(s(message))

};

As described earlier ~ designates a labelled argument to freely order function's input parameters. _ in the binding name tells the compiler that the argument isn't used in the body of that function. The spread operator ( ... ) alongside of component means that we extend an existing component. In this example we also overwrite the render function.

JSX in Reason is more strict than in React: we need to explicitly wrap strings with ReasonReact.string() . For convenience, I've created a shorter binding called s to use it conveniently inside JSX block.

Building non-trivial ReactReason app

Let’s build a ReactReason application that goes beyond displaying predefined data. We will create a GitHub viewer for trending repositories. The intention is to showcase how to integrate with an external API, how to manage state and how to use React’s lifecycle methods methods.

For the purpose of this example we will use reason-scripts to bootstrap our Reason project.

yarn create react-app reasonreact-github --scripts-version reason-scripts

Install dependencies:

cd reasonreact-github 
yarn

Start it with:

yarn start

Repositoryis the central concept in this application. Let’s start by defining a type to describe that entity. We will put it inside a separate module called Repo .

type t = { 
  name: string, 
  size: int, 
  forks: int 
};

From now on we can refer to this type with Repo.t from any Reason file in our application without the need of requiring it.

Managing State

We’ve already seen a stateless component. Now let’s create a component that has state. In our context we will be using RepoList component managing a list of trending repositories fetched from GitHub's API.

Let’s start by defining the type for the state managed by RepoList component.

type state = { 
  repos: list(Repo.t) 
};

There is, however, a catch. Initially, before the list of trending repositories is fetched from GitHub API, the repos is undefined. Reason type system doesn't allow us to have undefined value though. We could model that initial state with an empty list, but this is not optimal. Empty list could also mean that our query for fetching trending repositories didn't return any results.

Let’s use Reason’s optional values to deal with that situation.

type state = { 
  repos: option(list(Repo.t)) 
}

Next step is to define possible actions for that component. In ReasonReact, actions are represented as variants. For now we will only have one action called ReposFetched .

type action = 
| ReposFetched(list(Repo.t));

In order to create a stateful component in ReasonReact we need to use reducerComponent() function.

let component = ReasonReact.reducerComponent("App");

Such component allows to define a reducer which describes how the state is transformed in response to actions. A reducer takes an action along with the current state as input and returns the new state as output. Reducers must be pure functions.

reducer: (action, _prevState) => { 
  switch action { 
  | ReposFetched(repos) => ReasonReact.Update({repos: Some(repos)}) 
  } 
}

We’re pattern matching action, based on the parameter we receive in the reducer() method. Pattern matching must be exhaustive. All variant values must be matched. reducer definition is placed inside component's main function.

To finish off component’s definition, let’s define its initial state:

initialState: () => {  
  repos: Some([ {name: "Huncwot", size: 11011, forks: 42} ]) 
}

Integrating with API

We will use bs-fetch to fetch data from an external API. It is a BuckleScript library that acts as a thin layer on top of the Fetch API. Once the data is fetched, we will use bs-json to extract fields we are interested in.

Start by installing bs-fetch and bs-json :

npm i bs-fetch @glennsl/bs-json

Add them to bs-dependencies in your bsconfig.json :

{ 
  "bs-dependencies": [ 
    ..., 
    "bs-fetch", 
    "@glennsl/bs-json" 
  ] 
}

We defined our Repo type as a set of three fields: name , size and forks . Once the payload is fetched from GitHub API we parse it to extract those three fields.

let parse = json => Json.Decode.{ 
  name: json |> field("name", string), 
  size: json |> field("size", int), 
  forks: json |> field("forks", int), 
};

field is a method of Json.Decode . The Json.Decode.{ ... } (mind the dot) opens Json.Decode module. Its properties can now be used within these curly brackets without the need of prefixing with Json.Decode .

Since GitHub returns repos under items , let's define another function to get that list.

let extract = (fields, json) => Json.Decode.(
  json |> at(fields, list(parse))
);

Finally we can make a request and pass the returned data through our parsing functions:

let list = () => Js.Promise.( 
  Fetch.fetch(endpoint) 
  |> then_(Fetch.Response.json) 
  |> then_(text => extract(["items"], text) |> resolve) 
);

React Lifecycle Methods

Let’s use didMount lifecycle method to trigger the fetch of repositories from GitHub API.

didMount: self => { 
  let handle = repos => self.send(ReposFetched(repos)); 
  Repo.list() 
  |> Js.Promise.then_(repos => { 
    handle(repos); 
    Js.Promise.resolve(); 
  }) 
  |> ignore; 
}

handle is a method that dispatches ReposFetched action to the reducer. Once the promise resolves, the action will carry fetched repositories to the reducer. This will update our state.

Rendering

Since we distinguish between non initialized state and an empty list of repositories, it is straightforward to handle the initial loading in progress message.

render: self => 
  
( switch self.state.repos { | None => s("Loading repositories..."); | Some([]) => s("Emtpy list") | Some(repos) =>
    ( repos |> List.map((repo: Repo.t) =>
  • (s(repo.name))
  • ) |> Array.of_list |> ReasonReact.array )
} )
};

Error handling

TBW

Types in CSS

Types for CSS with bs-css .

yarn add bs-css
"bs-dependencies": [ 
  ..., 
  "bs-css" 
]
let style = 
  Css.( 
    { 
      "header": style([backgroundColor(rgba(111, 37, 35, 1.0)), display(Flex)]), 
      "title": style([color(white), fontSize(px(28)), fontWeight(Bold)]), 
    } 
  );
let make = _children => { 
  ...component, 
  render: _self => 
    

(s("This is title"))

};

Vocabulary

  • rtop is an interactive command line for Reason.
  • Merlin is an autocompletion service file for OCaml and Reason.
  • [@bs...] Bucklescript annotations for FFI

Additional Resources

TBD

module History = { 
  type h; 
  [@bs.send] external goBack : h => unit = ""; 
  [@bs.send] external goForward : h => unit = ""; 
  [@bs.send] external go : (h, ~jumps: int) => unit = "";  
  [@bs.get] external length : h => int = ""; 
};

BuckleScript allows us to mix raw JavaScript with Reason code.

[%bs.raw {|require('./app.css')|}];
Hacker Noon

责编内容by:Hacker Noon阅读原文】。感谢您的支持!

您可能感兴趣的

(源码)解决Android的WebView加载失败(404,500),显示的自定义视图,... 好多朋友会在Android开发过程中遇到使用WebView加载html页面出现404,500等错误页面,也有好多人想自定义这个错误页面,但是在6.0之前,大家觉得自定义错误页面就不好处理了; 之前一直使用在WebView加载时,根据onReceivedError() 判断网页是否加载成功,然...
Fun with Stamps. Episode 21. Private data in JavaS... A very often question people ask — how to have private (or protected, or just privileged) data or methods in stamps. Answer— same way(s) as it is...
Yarn 1.9.1 发布,Facebook 开源 JavaScript 包管理器... Yarn 1.9.1 发布了,Yarn 是 Facebook 推出的 JavaScript 包管理器,旨在提供 npm 之外的另一种选择方案。Yarn 具有极佳的伸缩性,可以支持成千上万个直接或间接的包依赖。Yarn 的设计初衷是保证稳定性、弹性和高性能。其与 npm 最大的不同在于安装包的方式,Y...
翻译连载 | 附录 A:Transducing(上)-《JavaScript轻量级函数式编程》 |《... Transducing 是我们这本书要讲到的更为高级的技术。它继承了第 8 章数组操作的许多思想。 我不会把 Transducing 严格的称为“轻量级函数式编程”,它更像是一个顶级的技巧。我把这个技术留到附录来讲意味着你现在很可能并不需要关心它,当你确保你已经非常熟悉整本书的主要内容,你可以再...
JavaScript 异常档案 作为前端工程师,看到上面的异常,我们该如何进行排查? 根据异常所在文件及其异常所在行,找到引发异常的相关代码附近, 对代码进行分析? 然后呢?就只能依靠我们以往的经验进行猜测和尝试了。 没有这个异常的相关经验?哪有这么巧...