Express-helloworld-API

Express API

之前聊了很多基本的型態和使用

開始來做一些實際的東西(不然有點無聊)

Nodejs 中似乎想到 API

都會先想到 Express

今天先用 Reason 寫一個 Express 的 hello world API

但是在這之前

我們先用最簡單的 nodejs 寫一個 Express hello world API

然後再慢慢轉為 Reason

Hello world

安裝 express 和開始 watch re 檔案

1
2
$ npm install express
$ yarn start

app.js

1
2
3
4
5
6
7
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port);

Image

步驟

在上面的範例中 require 了一個 express 他是一個函式 它會回傳一個 app 的物件

然和對 app 的這個物件 有使用到兩個函式

listenget

listen 會丟入一個參數(int)

get 會丟入兩個參數(string, function)

而在 get 中丟入的 function 會有兩個物件 req, res

因為只有使用到 res.send

大致上我們可以先宣告剛剛有提到函式 和參數的類型

再寫上 express 的程式

結果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type res = {.
[@bs.meth] "send": string => string
};
type handler = (string, res) => string;
type expressApp = {.
[@bs.meth] "listen": (int) => unit,
[@bs.meth] "get": (string, handler) => string
};

[@bs.module] external express: unit => expressApp = "express";

let app = express();

app##get("/", (_, res) => res##send("Hello World!"));

app##listen(3000);

有一些東西看不懂

BuckleScript - Class

裡面有些東西和 BuckleScriptClass 有關係

先來聊聊 在 Reason 中如何使用 class new 出一個物件

@bs.new

Date 為範例

1
2
3
4
type t;
[@bs.new] external createDate: unit => t = "Date";

let date = createDate();

上面的程式碼會編譯為

1
var date = new Date();

如果你想要用 moment

可以利用 @bs.new@bs.module

並且設定回傳可以使用的 method

1
2
3
4
5
6
7
8
type momentResult = {.
[@bs.meth] "format": (string) => string
};

[@bs.new] [@bs.module] external moment: unit => momentResult = "";

let date = moment();
Js.log(date##format("YYYY"));

上面的程式碼會轉為

1
2
3
4
5
var Moment = require("moment");

var date = new Moment();

console.log(date.format("YYYY")); /* 2018 */

主要是因為在 moment 增加一個回傳的 momentResult 的類型

代表回傳了一個 Javascript 物件 裡面有一個 format 的函式可以使用

需要傳入一個 string 會回傳一個 string

綁定 JS Class

JS Class 其實就是 Object 和 Class 利用一些方式連接起來

OCaml 通常會增加 [@bs] 將 class 轉為 Js.t 類型

1
2
3
4
5
6
7
8
9
10
class type _moment =
[@bs]
{
pub format: (string) => string;
};
type momentResult = Js.t(_moment);

[@bs.new] [@bs.module] external moment: unit => momentResult = "";
let date = moment();
Js.log(date##format("YYYY")); /* 2018 */

這個做法也是可以

結果是同樣的

但是這個做法可以使用 [@bs.meth] 來宣告非 arrow function

這些值會被視為 properties

[@bs.set] 則會將這些參數視為可以更動的 (mutable)

之後要修改值的時候要使用 #=

刪除 [@bs.set] 則會變為 不可變動的(immutable)

這部分之後在 JS package 中會做更詳細的討論

Middleware

先實做一個 簡單的 Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type res = {.
[@bs.meth] "send": string => string
};

type nextFun = unit => string;
type handler = (string, res, nextFun) => string;

type expressApp = {.
[@bs.meth] "listen": (int) => unit,
[@bs.meth] "get": (string, handler, handler) => string
};

[@bs.module] external express: unit => expressApp = "express";

let app = express();

app##get("/", (_, _, next) => {
Js.log("hello next");
next();
}, (_, res, _) => res##send("Hello World!"));

app##listen(3000);

但是每次多一個 middleware 都要對一個 type 做一次修改

感覺很麻煩啊

今天先到這邊吧

之後再來處理 Middleware 的問題了

BasicType-OptionsAndSomeAndNone

Null 和 Undefined 和 Option

Reason 並沒有 nullundefined 的概念

這是很棒的事情,因為可以減少很多型態的 bugs

再也看不到 undefined is not a function 或是 cannot access foo of undefined

但是 這些值其實依舊存在但是在 Reason 可以安全的存在

因為是通過 Option 來表示值是否存在

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

這代表這個值得類型是 None (無) 或是實際值包含在 Some

範例

1
2
3
4
5
6
7
let personHasCar = false;

if(personHasCar) {
Some(5);
} else {
None;
}

如果要表示一個數可能會是 None

但是可以利用另一種寫法

1
2
3
4
5
6
7
8

let licenseNumber = Some(5);

switch (licenseNumber) {
| None => print_endline("The person doesn't have a car")
| Some(number) =>
print_endline("The person's license number is " ++ string_of_int(number))
};

透過將數字串為選項

強迫您列舉出所有可能的選項和回應

杜絕錯誤處理或忘記處理的情況

Option

Option 其實就是等於 Some('a)None

這在 Javascript 中十分常見

警告1

1
let aaa = Some(Some(Some(5)))

這個依舊會被編譯為 let aaa = 5;

但是下面的狀況會比較麻煩

1
let bbb = Some(None);

上面那一行會被編譯為

1
var bbb = Js_primitive.some(undefined);

什麼是 Js_primitive ?

怎麼不直接編譯為 var bbb = undefined; 這樣就好了?

在處理多種 Option 的型態的時候

如果沒有用一些特殊註解來標記這些值的話

很多操作上都會變得棘手

以下有幾個原則可以遵守

  • 永遠不要內嵌 Option (Ex: Some(Some(Some(5))))
  • 不要指定 Option('a) 給來源是 Javascript 的值,給予準確的型態

警告2

但是很多時候在 Javascript 總是會有 null 或是未定義

在這種情況下我們無法輸入這種值

因為 option 只會檢查 undefined 而不會檢查 null

解法: Js.Nullable

有提供 Js.Nullable 這個模組來檢查 undefinednull

要建立 undefined 可以使用 Js.Nullable.undefined

當然你也可以使用 None 這並不衝突

範例

1
2
3
4
5
6
7
[@bs.module "MyIdValidator"] external myId: Js.Nullable.t(string) = "MyID";

[@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);

Js.Nullable.return 可以回傳的是一個允許他是 null 的字串

而不僅僅是單純的字串格式

NoneJs.Nullable 是很重要的一部分

因為他補足了強形態中處理 nullundefined 的問題

More on Type

型別是可以接受參數的

類似別種語言的(generics)

參數必須使用 ' 開頭

1
2
3
4
type intCoordinates = (int, int, int);
type floatCoordinates = (float, float, float);

let buddy: intCoordinates = (10, 20, 20);

這是基本的使用方法

可以改為

1
2
3
4
5
type coordidate('a) = ('a, 'a, 'a);

type coordidateAlias = coordidate(int);

let buddy: coordidateAlias = (10, 20, 20);

其實型別會自動幫你推導

所以更簡潔的寫法是

1
let buddy = (10, 20, 20);

這時候會自動推導它為 (int, int, int) 型別

1
let greetings = ["hello", "world", "how are you"];

會自動推導為 list(string)

型別也可以接受多個參數

1
2
3
4
5
6
7
8
9
10
11
12
type result('a, 'b) =
| Ok('a)
| Error('b);
type myPayload = {data: string};

type myPayloadResults('errorType) = list(result(myPayload, 'errorType));

let payloadResults: myPayloadResults(string) = [
Ok({data: "hi"}),
Ok({data: "bye"}),
Error("Something wrong happened!")
];

相互遞迴型別

1
2
type student = {taughtBy: teacher}
and teacher = {student: list(student)};

第一行沒有分號

第二行開頭沒有 type

設計決策

型別系統基本上允許階層

例如: list(int)

實際是 list 型別 中接受 int 型別

其他語言有的會稱之為泛型

遵循 The principle of least power原則

在解決方法中選擇最不複雜的一個來使用

BuckleScript-Object

Bukle Script Object

Javascript 中的物件有兩個主要的目的

  • 像是一個雜湊表(或是字典), Keys 可以動態的 新增/刪除 ,值是同樣的型態
  • 像是一個紀錄(Record),欄位是固定的(但是也允許是選擇性的),他的值可以是不同的形態

這部分 BuckleScript 的物件也是一樣的

雜湊表模式

直到最近 Javascript 才終於有正確的支援 Map

物件使用上和 Map 可以是一樣的

前提是

  • 可能會或是不會 新增/刪除 參數的鰎值
  • 值可以被 動態/計算 鍵值 瀏覽
  • 值得型態都是一樣

這時候可以利用 Js.Dict  來綁定這個 Javascript 物件

你可以就像 Javascript 中的 Object 來做操作

Js.Dict.keys 來取得所有的鍵值

Js.Divt.values 來取得所有的值

範例

1
2
3
4
5
6
7
8
9
10
let myMap = Js.Dict.empty();

Js.Dict.set(myMap, "Allison", 10);

[@bs.val] external studentAge: Js.Dict.t(int) = "student";

switch (Js.Dict.get(studentAge, "Joe")) {
| None => Js.log("Joe can't be found")
| Some(age) => Js.log("Joe is " ++ string_of_int(age));
};

轉譯後的結果

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict';

var myMap = { };

myMap["Allison"] = 10;

var match = student["Joe"];

if (match !== undefined) {
console.log("Joe is " + String(match));
} else {
console.log("Joe can't be found");
}

你可以看到其實 Js.Dict 是基於 Javascript 的物件

整個 API 都沒有使用到 external 所以在編譯之後 整個 API 都會消失

編譯結果中你也找不到 Dict 的相關字

這很便於你將 Js 檔案轉為 BuckleScript 檔案

Record 模式

如果你的 Js 物件:

  • 有固定並且已知的欄位
  • 包含的值有不同的型別

這時候大多數的程式語言中 你都可以使用 Record

例如:

1
2
3
4
5
{
"John": 10,
"Allison": 20,
"Jimmy": 15
}

1
2
3
4
5
{
name: "John",
age: 10,
job: "CEO"
}

的不同

前者會使用 雜湊表 的型態來處理

後者會使用 Record 來處理

而在 BuckleScript 中要使用 bs.deriving abstract 功能

後者則會這樣寫

1
2
3
4
5
6
7
8
[@bs.deriving abstract]
type person = {
name: string,
age: int,
job: string
};

[@bs.val] external john: person = "john";

note person 並不是一個 Record 的型別

他只是看起來像是 Record 的型別

而且可以使用 Record 的形態檢查

bs.deriving abstract 就是在註解將 person 轉為 抽象型別

建立

上述的抽象型別因為不是 Record 型別

所以不能直接使用型態的方式來建立

1
2
3
4
5
6
7
8
[@bs.deriving abstract]
type person = {
name: string,
age: int,
job: string,
};

let joe = person(~name="Joe", ~age=20, ~job="teacher")

轉譯後的結果

1
2
3
4
5
6
7
8
9
10
11
12
// Generated by BUCKLESCRIPT VERSION 4.0.5, PLEASE EDIT WITH CARE
'use strict';


var joe = {
name: "Joe",
age: 20,
job: "teacher"
};

exports.joe = joe;
/* No side effect */

結果對於運行並不會有成本

欄位重新命名

有時候在綁定 JS object 的時候,欄位名稱在 BuckleScript/Reason 中是無效的名稱

有兩種範例 一個是 {type: "foo"} (BS/Reason 中的保留字) 和 {"aria-checked": true}

可以選擇另一個有效的名稱 使用 @bs.as 來規避這個問題

1
2
3
4
5
6
7
[@bs.deriving abstract]
type data = {
[@bs.as "type"] type_: string,
[@bs.as "aria-checked"] ariaLabel: string
};

let d = data(~type_="message", ~ariaLabel="hello");

輸出會是

1
2
3
4
5
6
7
'use strict';


var d = {
type: "message",
"aria-checked": "hello"
};
可選的標籤

可以建立可忽略的 欄位

1
2
3
4
5
6
7
8
9
[[@bs.deriving abstract]
type person = {
[@bs.optional] name: string,
age: int,
job: string
};

let joe = person(~age=19, ~job="sleep", ());
Js.log(joe);

編譯後會是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Js_primitive = require("bs-platform/lib/js/js_primitive.js");

function joe(param) {
return (function (param$1) {
var prim = param;
var prim$1 = 19;
var prim$2 = "sleep";
var tmp = {
age: prim$1,
job: prim$2
};
if (prim !== undefined) {
tmp.name = Js_primitive.valFromOption(prim);
}
return tmp;
});
}

console.log(joe);

可以看到 name 會是可選的,若是沒有也不會造成錯誤

note:bs.optional 只是將 name 改為可選,但是如果輸入 Option(string) 不會有作用

Accessors

當你使用 bs.deriving abstract 隱含了 recode 的型態,你無法使用 joe.age 這樣的方式來取得值

原生會提供 gettersetter 來完成這個

取值

每一個 bs.deriving abstract 欄位 都會有一個 getter function

在上面的範例中會有三個 getter functionnameGet, ageGet, jobGet

他們會取得 person 的值並分別回傳 string, int, string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[@bs.deriving abstract]
type person = {
[@bs.optional] name: string,
age: int,
job: string,
};

let joe = person(~age=20, ~job="teacher", ());

let twenty = ageGet(joe);
Js.log(twenty);

/**
也可以寫成這樣
joe-> ageGet-> Js.log
**/

至於改值則是

1
2
3
4
5
6
7
8
9
10
11
12
[@bs.deriving abstract]
type person = {
[@bs.optional] name: string,
mutable age: int,
job: string,
};

let joe = person(~age=20, ~job="teacher", ());

joe-> ageGet-> Js.log; /* 20 */
joe-> ageSet(21)-> Js.log;
joe-> ageGet-> Js.log; /* 21 */

note: 要記得將需要修改的參數前面加上 mutable

Object Methods

可以附加任何function 在類型上(任何類型的 record 不僅止於 @bs.deriving abtract)

可以參閱 Object Method

這部分之後會再討埨

可變性

Test.re

1
2
3
4
5
[@bs.deriving abstract]
type cord = {
[@bs.optional] mutable x: int,
y: int,
};

Test.rei

1
2
3
4
5
[@bs.deriving abstract]
type cord = {
[@bs.optional] x: int,
y: int,
};

Object2

如果上述的 Object 並不符合您的需求

也有另外一種方式可以綁定 Javascript Object

有幾個前提

  • 你不想要預先宣告 type
  • 你希望你的 Object 是有結構性的

Ex: 你的類型希望可以接受所有含有 age 這個參數的物件,而不是只有特定參數的物件

陷阱

note: 不能使用普通的 Reason/OCaml 的物件類型,如下

1
2
3
4
5
6
type person = {
.
name: string,
age: int,
job: string
};

你仍然可以宣告這個類型

但是無法編譯成功

因為 Reason/OCaml 的對象工作方式是不一樣的

解決方案

BuckleScript 利用 Js.t 來做包裝類型

以便控制和追蹤可以編譯成 Javascript 的對象子集合

1
2
3
4
5
6
7
8
9
10
type person = Js.t({
.
name: string,
age: int,
job: string
});

let john = {"name": "john", "age": 11};

john##name -> Js.log;

從現在開始 BuckleScript 都會以 Js.t 來消除跟 一般物件 和 Javascript 物件的歧義

因為 Reason 有包覆一層語法糖 會將 Js.t({. name: string}) 轉為 {. "name": string}

訪問 與 修改

取值

要取得值的話方法為 ##

範例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type person = Js.t({
.
name: string,
age: int,
job: string
});

let john = {
"name": "john",
"age": 11,
"job": "development"
};

let johnName = john##name;

明天再來談談 SomeOption 的問題

BuckleScript-Best-Friend

BuckleScript

OCaml

BuckleScript 的基礎

如果需要對 OCaml 有興趣可以參考 官方文件

如果對 Reason 有興趣可以參考 官方文件

Reason

OCaml 的另一種語法,對 Javascript 的開發者比較親切

OPAM

官方的套件管理, 自從 BuckleScript 支援 NPM/YARN 之後就不需要使用他了

External/Interop/FFI

都是 BuckleScript <-> JavaScript 交互編譯的術語

  • External - 在 BuckleScript 中使用 JS 值得一種方式
  • Interop - interoperability 的縮寫
  • FFI - Foreign Function Interface 的縮寫, “external”, “wrapper” 和 “interop” 的通用術語, 基本上就是在一個語言中呼叫另一個語言的值
bs 意義
val [@bs.val] global value
scope [@bs.scope] use names as a namespace
new [@bs.new] new constructor
module [@bs.module] 從某一個 mobule 綁定其中一個值
send @bs.send function chaining
send.pipe @bs.send.pipe function chaining includes parameters
splice [@bs.splice] 具有不定長度參數的函式

note:這只是一小部分

使用場景

  • Reason 中直接寫 Javascript
  • 引用 js lib 使用 (global, default, other libs)
  • 使用現有 Bucklescript 所提供的lib

原生 Javascript

1
2
3
4
5

[%%raw "var a = 1, b = 2"];
let add = [%raw "a + b"];
let myFunction = [%raw "(a, b) => a + b"]
Js.log(myFunction(1)(2));

字串 Unicode 和 template string

1
2
3
4
5
Js.log({js|你好,
世界|js});

let world = "world";
let helloWorld = {j|hello, $world|j};

全域變數

note: 先看看官方是否有先幫你完成的 API 再決定是否要自己處理

1
2
3
4
[@bs.val] external setTimeout/* 在 Reason 中使用的模組名稱 */: (unit => unit, int) => float = "setTimeout";/* 對應到 Javascript 中的模組名稱 */

let eventId = setTimeout(() => Js.log("hello setTimeout"), 1000);
Js.log(eventId);

function chaining

亦或是利用 mapfilter 做一些陣列處理的時候

1
2
3
4
5
6
[@bs.send] external map: (array('a), 'a => 'b) => array('b) = "";
[@bs.send] external filter: (array('a), 'a => 'b) => array('b) = "";

let mapResult = map([|1, 2, 3|], a => a + 1);
let result = filter(mapResult, a => a mod 2 === 0);
Js.log(result);

上面的範例 mapfilter 為什麼要使用 send 呢?

也有另外一個比較漂亮的做法

1
2
3
4
5
6
7
[@bs.send] external map: (array('a), 'a => 'b) => array('b) = "";
[@bs.send] external filter: (array('a), 'a => 'b) => array('b) = "";

[|1, 2, 3|]
->map(a => a+1)
->filter(a => a mod 2 === 0)
->Js.log

但是這部分之後再探討

全域模組

1
2
3
4
5
6
[@bs.val] [@bs.scope "Math"] external random: unit => float = "random";
let someNumber = random();
/**/

/* front end demo */
[@bs.val] [@bs.scope ("window", "location", "ancestorOrigins")] external length: int = "length";

可空值

1
2
let a = Some(5);
let b = None;
1
2
3
4
5
6
let JsNullable = Js.Nullable.null;
let JsUndefined = Js.Nullable.undefined;

let result1: Js.Nullable.t(string) = Js.Nullable.return("hello"); /* hello */
let result2: Js.Nullable.t(int) = Js.Nullable.fromOption(Some(10)); /* 10 */
let result3: option(int) = Js.Nullable.toOption(Js.Nullable.return(10)); /* 10 */

這一部份在後續章節也會做更詳細的討論

模組

預設輸出 ES6

LeftPad.re

1
let default = (name: string, age: int) => Js.log({j|$name 年紀 $age|j});
Import Module

ImportModule.re

1
2
3
[@bs.module "./LeftPad.bs"] external leftPad : (string, int) => string = "default";

leftPad("hello", 123);

因為 bsconfig.js 中的 suffix 設定為 .bs.js

所以當我們要 import 的時候需要加上 .bs

他編譯後的結果會是

1
2
3
var LeftPadBs = require("./LeftPad.bs");

LeftPadBs.default("hello", 123);

否則會找不到這個檔案

Global Module
1
2
3
[@bs.module "path"] external dirname: string => string = "";

Js.log(dirname("/foo/bar/baz/asdf/quux"));
1
2
3
var Path = require("path");

console.log(Path.dirname("/foo/bar/baz/asdf/quux"));

@bs.module 會將模組引入

external 則是指定輸出的名稱 (在Reason 使用的名稱)

最後則是要引入的名稱,但是如果同名則可以設為空字串

參數長度可變

使用 module path 中的 join 來做 demo

1
2
3
4
[@bs.module "path"] [@bs.splice]
external join : array(string) => string = "";

Js.log(join([|"/", "hello", "/", "world"|]));

bs.module 先指定來源 path

再加上 bs.splice 來告訴 Reason 他是一個具有不固定長度參數的 function

利用 array 的 長度是彈性的優點來做彈性的多參數處理

Reason-BasicTypeIII

Basic Type

今天討論的東西會也很多(每天啃到天荒地老)

  • Reason - Record
  • Reason - Object
  • Reason - Destructuring

Reason - Record

Reason - Record 很像 Javascript 的 Object

但是他有幾個特性

  • 更輕量
  • 預設是不可改變的 (Immutable)
  • 欄位名稱和型別都是固定的
  • 很快
  • 是較嚴格的類別

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
type person = {
age: int,
name: string
};

let tomas = {
age: 35,
name: "Tomas Lin"
};

let tomasName = tomas.name;
print_endline(tomasName); /* Tomas Lin */

需要明確的定義類型跟結構

Record 需要先宣告一個基本的結構 (Type)

否則會報錯

如果符合則會自動型別推導

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type person = {
age: int,
name: string
};

let tomas: person = {
age: 35,
name: "Tomas Lin"
};

let tomasName = tomas.name;
print_endline(tomasName);
let simon = {
age: 22,
name: "Simon"
};

上面的範例中 tomas 有指定型別是 person

但是 simon 並未指定,可是因為他的結構和 person 一樣

所以 Reason 會自動做型別推導為 person

note:若是有兩個同樣型別結構的話則會依據較近的型別

你也可以將型別宣告在別的檔案中

但是需要明確的指定名稱

AnimalType.re

1
type cat = {color: string, call: string};

Animal.re

1
2
3
4
5
6
let whiteCat: AnimalType.cat = {
color: "white",
call: "喵!"
};

print_endline(whiteCat.call);

Spread 修改 (immutable update)

因為 Record 是不可以直接修改的

可以利用 Spread 來做修改的實作

範例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type person = {
age: int,
name: string
};

let tomas: person = {
age: 35,
name: "Tomas Lin"
};

let tomasName = tomas.name;
print_endline(tomasName);
let simon = {
age: 22,
name: "Simon"
};

let tomasNextYear = {...tomas, age: tomas.age + 1};

Js.log(tomasNextYear.age);/* 36 */

注意:Spread修改不能夠增加新的欄位

mutable update

Reason 也保有直接修改的彈性

範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type person = {
age: int,
mutable name: string
};

let tomas: person = {
age: 25,
name: "Tomas Lin"
};

let name = "Simon";

let simon = {
age: 25,
name
};

tomas.age = tomas.age + 1;

Js.log(tomas.age); /* 26 */
Js.log(simon.name); /* Simon */

也支援 punning 簡化程式碼

不能用 Record 處理的問題

Reason 中你無法宣告一個 function 他的參數傳入一個物件

這個物件中必要參數是 age

而其他參數都是可以變動的

範例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type person = {age: int, name: string};
type monster = {age: int, hasTentacles: bool};

let getAge = (entity) => entity.age;
let kraken = {age: 9999, hasTentacles: true};
let me = {age: 5, name: "Baby Reason"};

getAge(kraken);
getAge(me);
/* get Error:
This has type:
person
But somewhere wanted:
monster */

Type system 不能定義一個類型是 personmonster

當你有這類型需求的話

接下來介紹的 Reason - Object 可以滿足

Reason - Object

有些時候 Reason - Record 無法滿足一些特殊情境的需求

所以便有了 Reason - Object 增加一些彈性

型別

物件不一定要宣告型別

看起來和 Reason - Record 十分相似

除了 ...

範例如下

1
2
3
4
type tesla = {
.
color: string
};

開頭 . 代表這是一個閉鎖的物件

任何基於這個型別的物件都必須完全符合結構

1
2
3
4
type car(`a) = {
..
color: string
} as `a;

開頭 .. 則代表這是開放型的物件,所以可以包含其他值或 function

開放型的物件代表是多型

所以需要一個參數

Example

Simple

1
2
3
4
5
6
7
8
9
10
11
type tesla = {
.
color: string,
};

let obj: tesla = {
val red = "Red";
pub color = red;
};

Js.log(obj#color) /* "Red" */

在這個物件中含有兩個屬性

私有屬性 redcolor method

因為 color 是一個公開的 method

所以我們可以利用 object notation 來標記取得值

物件只導出Method,所有屬性都是Private的

Advanced

1
2
3
4
5
6
7
8
9
10
11
12
type tesla = {.
drive: (int) => int
};

let obj: tesla = {
val hasEnvy = ref(false);
pub drive = (speed) => {
this#enableEnvy(true);
speed;
};
pri enableEnvy = (envy) => hasEnvy := envy
};

在這個範例中 tesla 中一個公開的 drive 也有一個私有的 enableEnvy method

私有的 method 只可以在物件內部取用

Reason 也有 this 和 Javascript 的 this 不一樣的地方在於

Reason 的 this 永遠都會指向物件本身

開放式物件的範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type tesla('a) = {
..
drive: int => int
} as 'a;

type teslaParams = {.
drive: int => int,
doYouWant: unit => bool
};

let obj: tesla(teslaParams) = {
val hasEnvy = ref (false);
pub drive = (speed) => {
this#enableEnvy(true);
speed;
};
pub doYouWant = () => hasEnvy^;
pri enableEnvy = (envy) => hasEnvy := envy
};

Js.log(obj#doYouWant());
Js.log(obj#drive(11));
Js.log(obj#doYouWant());

上面的 tesla 中為開放型物件

但是需要有一個參數 teslaParams 定義所有開放的的 method

但是如果你要找由 Javascript 的物件,你需要的不是 Reason - Object

而是特別的 BukleScript - Record special Record

Reason - Destructuring (解構)

解構是很清楚簡單的方式來取得物件或陣列中的變數

用法

下面綁定 ten = 10twenty = 20

1
2
3
4
let someInts = (10, 20);
let (ten, twenty) = someInts;
Js.log(ten);
Js.log(twenty);

下列綁定變數 name = "Guy"age = 30

1
2
3
4
5
type person = {name: string, age: int};
let somePerson: person = {name: "Tomas", age: 30};
let {name, age} = somePerson;
Js.log(name);
Js.log(age);

取回變數後可以重新命名

1
2
3
4
5
type person = {name: string, age: int};
let somePerson: person = {name: "Tomas", age: 30};
let {name: n, age: a} = somePerson;
Js.log(n);
Js.log(a);

也可以定義型態

1
2
3
4
5
type person = {name: string, age: int};
let somePerson: person = {name: "Tomas", age: 30};
let {name: (n: string), age: (a: int)} = somePerson;
Js.log(n);
Js.log(a);

當然函式的標籤式參數也可以解構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type person = {
name: string,
age: int
};

let tomas = {name: "Tomas", age: 25};

let someFunction = (~person as {name}) => {
Js.log(name);
};

let otherFunction = (~person as {name} as thePerson) => {
Js.log(thePerson);
Js.log(name);
};

someFunction(~person=tomas);
otherFunction(~person=tomas);

Mutable

有時候我們會需要可以變更的變數

Reason 也有保留這部分的彈性

1
2
3
4
type total = ref(int);
let x: total = ref(5);
x := x^ + 1;
Js.log(x); // 6

ref 只是一個語法糖

透過 := 修改

^ 代表取出他的值

這樣就可以簡單地在 Reason 中使用 Mutation 的技巧

Reason-BasicType Part-II

Basic Type

上一章節已經有討論了幾種基本類型

這是今天要了解的幾種型態

  • Function
  • Array
  • Tuple
  • List

Function

Function 在日常 coding 是相當常見與重要的部分

Reason 中是以箭頭以及回傳表達式

以逗號隔開參數

1
2
let greet = (hello, name) => hello ++ " " ++ name;
Js.log(greet("Hello", "Tomas"));

無參數

有時候 Function 中不需要傳入參數

這裡稱為 unit

1
2
let sayHello = () => "Hello";
Js.log(sayHello()); /* Hello */

參數的標籤

1
2
3
4
5
let subNumber = (x, y) => {
return x - y;
};

Js.log(subNumber(1, 2)); /* 1 */

這裡其實很難知道哪個是 x, 哪個是 y

我們可以加上 ~ 這樣的話會變成

1
2
3
4
5
let subNumber = (~x, ~y) => {
return x - y;
};

Js.log(subNumber(~x=1, ~y=2));

也可以利用 as 加上別名

1
2
3
4
5
6
7
let drawCircle = (~radius as r, ~color as c) => {
setColor(c);
startAt(r, r);
/* ... */
};

drawCircle(~radius=10, ~color="red");

事實上,(radius) 只是 (radius as radius) 的速寫法(稱之為 雙關(punning))

Optional 參數

1
2
3
4
5
6
7
8
let saySomeThing = (~name, ~title) => {
switch title {
| None => "Hello " ++ name;
| Some(t_) => t_ ++ name;
};
}

Js.log(saySomeThing(~name="Tomas"));

title 並不是一定要輸入的參數

如果為空預設值是 None

利用 switch 來涵蓋相對的 回傳值

Tuple

Javascript 中並沒有 Tuple 的概念 而被整合在 Array 之中

Reason 中的 Tuple 需要符合幾種特性

  • 不可變(immutable)
  • 有順序性的
  • 一開始就已經固定長度
  • 允許包含不同的值

tuple 長度不會是 1。若長度為 1 就是值本身。

Pair operations

官方提供 fstsnd 來快速取得 Tuple中第一 二個值

若是要取得其他更多的值,可以多多利用解構

1
2
3
4
5
6
7
8
9
type testTuple = (int, int, int);
let test: testTuple = (1, 2, 3);

let (_, second, _) = test;

Js.log(second);/* 2 */

/* Js.log(fst(test)); get compiler Error */
type test2Tuple = (int, int);

盡量在 區域性 使用 Tuple,若是有經常傳遞的需求則可以考慮使用 record

而且 Tuple + Switch 是十分強大的應用,可以解決 category bug

因為 Reason 會強迫你列出 2*2=4的所有可能情況而不用使用 if else 來做處理

1
2
3
4
5
6
switch (isWindowOpen, isDoorOpen) {
| (true, true) => "window and door are open!"
| (true, false) => "just window open"
| (false, true) => "just door open"
| (false, false) => "window and door are not open"
}

但是總會有些時候必須要修改某個 Tuple 中的一個值

對 Reason 來說這就是一個新的 Tuple 範例如下:

1
2
3
let tuple1 = (1, 2);
let tuple2 = (fst(tuple1), 4);
Js.log(tuple2);

List

特性

  • 包含的參數需要同型態
  • 不可變的(immutable)
  • 在陣列前面 shift 元素會效率很好
1
2
let myList: list(int) = [1, 2, 3];
let anotherList = [0, ...myList];

myList 的值不會被改變, antherList現在是 [0, 1, 2, 3]

為什麼他的效能式常數而不是線性的?

因為在 anotherList 中的 myList 是 shared 的

note: 但是並不允許 [0, ...aList, ...bList]

你可以使用 ListLabels.concat 來實現

Reason 提供了一些基本的 API

但是在使用的時候名稱是 ListLabels

1
2
3
4
let myList: list(int) = [1,2,3];
let anotherList = [0, ...myList];
Js.log(ListLabels.length(myList)); /* 3 */
Js.log(ListLabels.length(anotherList)); /* 4 */

Switch

也可以搭配 pattern matching

1
2
3
4
5
6
7
8
let myList: list(int) = [1, 2];
let message =
switch (myList) {
| [] => "This list is Empty"
| [a, ...rest] => "a: " ++ string_of_int(a) ++ " rest.length is " ++ string_of_int(ListLabels.length(rest))
};

Js.log(message); /* a: 1 rest.length is 1 */

Array

特性

  • 可變的
  • 快速的任意存取和修改
  • 原生固定長度 (Javascript 彈性長度)

前後需要使用 [||]

可以使用 ArrayArrayLabels 而針對 JS compiler 也可以使用 Js.Array API

1
2
3
4
5
6
7
let arrayTest = [|"hello", "eeee", "yo"|];

Js.log(arrayTest[0]);

arrayTest[1] = "gggg";

Js.log(arrayTest[1]); /* gggg */

如果要編譯 Javascript Reason 中的 Array 可以直接對應 Javascript 的 Array

雖然在 Reason 中無法直接修改長度,可以利用 Js.Array 來修改

範例如下

1
2
3
4
5
6
let array1 = [|0,2,3,4,5,6|];
let array2 = Js.Array.copyWithin(~to_=3, array1);
Js.log(array1);/* [ 0, 2, 3, 0, 2, 3 ] */
Js.log(array2);/* [ 0, 2, 3, 0, 2, 3 ] */
let array3 = Js.Array.push(1, array1);
Js.log(array3);/* [ 0, 2, 3, 0, 2, 3, 1 ] */

陣列在一般寫程式過程中扮演相當重要的一個角色

篇幅會太多

明天再來聊聊另一種基本型到 RecordObject

Reason-BasicType

Basic Type

在每個程式語言中都會有一些基本的型態

Reason 屬於強型態的程式語言

這裡介紹的基本型態的變數型態有

  • Boolean
  • 數字
  • 字串
  • 字元

Type

TypeReason 的亮點之一

let 不一定需要宣告 類型

1
let score = 10;

Reason 知道 score 是個 int 型別,透過其值 10 判斷出來。這個稱之為 推斷(inference)

變數宣告(Let binding)

在介紹變數型態之前要先介紹一下如何宣告變數

Reason 中只有使用 let 來宣告變數

1
let a: string = "hello";

雖然在 Reason 中已經宣告,但是在編譯成功後依舊不會看到這個變數

因為當尚未使用到之前是不會產生在結果中

1
2
let a: string = "hello";
Js.log(a);

但是當我們開始使用之後,編譯結果就會出現

Result:

1
2
3
4
5
6
7
8
9
// Generated by BUCKLESCRIPT VERSION 4.0.5, PLEASE EDIT WITH CARE
'use strict';

var a = "hello";

console.log(a);

exports.a = a;
/* Not a pure module */

另外 Reason 是屬於 Block Scope

1
2
3
4
5
if (displayGreeting) {
let message = "Enjoying the docs so far?";
print_endline(message)
};
/*這裡不能呼叫 message*/

也可以這樣處理

1
2
3
4
5
6
7
let message = {
let part1 = "hello";
let part2 = "world";
part1 ++ " " ++ part2
};

Js.log(message);

這樣在外層會無法呼叫 part1, part2

message 會等於 part1 ++ " " ++ part2 的字串組合

編譯後的結果會是

1
2
3
4
5
6
7
'use strict';

var message = "hello world";

console.log(message);

exports.message = message;

而且預設使用 let 宣告的變數都是 Immutable

1
2
let a: string = "hello";
a = "hello world";

這邊會得到一個錯誤訊息

1
2
3
4
5
6
7
8
9
10
11
12
>>>> Start compiling
[1/1] Building src/Demo-HelloWorld.cmj

We've found a bug for you!
/Users/tomas/Documents/iThome/2018/hello-world/src/Demo.re 2:1-17

1 │ let a: string = "hello";
2 │ a = "hello world";

The value a is not an instance variable

>>>> Finish compiling(exit: 1)

但是可以重複使用 let 宣告同一個變數

1
2
3
4
let message = "hello";
Js.log(message); /* Prints "hello" */
let message = "bye";
Js.log(message); /* Prints "bye" */

Type system

雖然 Reason 是屬於強型態,但是也有留一些彈性

若你沒有宣告變數型態的時候,他會自動進行型態推導

1
2
3
4
5
let a = "1";

let b = a ++ "hello";

Js.log(b); /* 1hello */

但是如果使用數字的運算的話

1
2
3
4
5
let a = 1;

let b = a ++ "hello";

Js.log(b); /* Error: */

會得到了一個錯誤

型態錯誤

訊息會相當清楚地告知是哪一個部分錯誤

對於 debug 會相當有利

Boolean

布林值在各個程式語言中佔有相當重要的基本判斷依據

而且在 Javascript 中也是一項令人相當頭痛的變數型態

常常會有很多不小心造成了很多 bug

1
2
3
let testNum = 1;
let isZero: bool = testNum === 0;
Js.log(isZero);

有趣的是編譯結果會是

1
2
3
4
5
6
7
8
9
10
11
12
// Generated by BUCKLESCRIPT VERSION 4.0.5, PLEASE EDIT WITH CARE
'use strict';


var isZero = false;

console.log(isZero);

var testNum = 1;

exports.testNum = testNum;
exports.isZero = isZero;

他並未依據我的順序來做編譯

而是做了一些優化

因為 isZero 比較的是一個常數

所以不論怎樣執行,最後的結果一定是 false

所以他優先宣告了 var isZero = false;

然後在宣告 testNum

基本的布林運算子

  • && - and
  • || - or
  • ! - not
  • <=, >=, <, >
  • == - structural equal
  • === - referential equal
  • != - structural not equal
  • !== - referential equal

運算子中比較有趣的是 =====

1
2
3
4
5
6
7
8
9
let tuple1 = (1, 2, 3);
let tuple2 = (1, 2, 3);
let tuple3 = tuple2;

Js.log(tuple1 == tuple2); /* true */
Js.log(tuple1 == tuple3); /* true */
Js.log(tuple1 === tuple2); /* false */
Js.log(tuple1 === tuple3); /* false */
Js.log(tuple2 === tuple3); /* true */

== 是比較結構是否相等
=== 是比較來源是否相同

數字

1
2
3
4
5
6
7
let zero: int = 0;
let floatNumber: float = 0.1;

Js.log(zero); /* 0 */
Js.log(floatNumber); /* 0.1 */

/* zero === floatNumber will got Error by compiler*/
  • int - 整數
  • float - 浮點數

數字的相加

1
2
3
4
5
let addResult = 1 + 1;
Js.log(addresult);/* 2 */

let floatAddResult = 1.1 + 1.2; /* get compiler Error (WTF) */
Js.log(floatAddResult);

Float 的相加要使用另外一個符號 +.

1
2
3
4
5
let addResult = 1 + 1;
Js.log(addresult);/* 2 */

let floatAddResult = 1.1 +. 1.2; /* 2.3 */
Js.log(floatAddResult);

int 與 float 的比大小

因為 Reason 是強型別的語言

1
2
let twoBiggerThanOne = 2 > 1;
Js.log(twoBiggerThanOne);

intfloat 是不同的型別

無法直接地做比較

所以要先使用 float_of_intint 轉成 float

1
float_of_int(2) > 1.2; /* true */

string

" 使用雙引號宣告的為字串

' 使用單引號宣告的為字元

字元只能有一個字母

1
2
3
4
5
6
7
let name: string = "Tomas";
let yourName: string = "哈囉世界!";
let a: char = 'a';

Js.log(name);
Js.log(a);
Js.log(yourName); /* \xe5\x93\x88\xe5\x9b\x89\xe4\xb8\x96\xe7\x95\x8c! */

在編譯的時候要注意的是如果是中文的話預設是 Unicode

在宣告的時候要先進行處理

而使用 j 除了處理 Unicode 也可以用變數 (類似 ES6 Template)

1
2
3
4
5
6
7
8
9
let name: string = "Tomas";
let yourName: string = {js|哈囉世界!|js};
let world = {js|世界|js};
let helloWorld = {j|你好,$world|j};
let a: char = 'a';

Js.log(name);
Js.log(a);
Js.log(yourName); /* 哈囉世界! */

BuckleScript 編譯前會先尋找 jsj 進行處理

另外字串的相加使用 ++

1
2
3
let hello = "Hello";
let world = "World"
Js.log(hello ++ " " ++ world); /* console.log("Hello World"); */

也可此參考 Reason 的 string API文件

Reason簡介與Helloworld

What is Reason

Reason 並不是一個新的語言,而是一種新的語法和工具鍊(toolchain),Ocaml支援。並支援既有的 NPM/YARN。

藉由 BuckleScriptReason 編譯為可閱讀的 Javascript

Reason 的優勢

  • 型別系統: Ocaml 型別測試具有 100% 的覆蓋率,而且保有 Javascript 的型別推導,一但編譯過型別保證正確
  • 保有簡單和實用性:
    • 允許 side-effect, mutable
    • 也可以使用 immutable functional
  • 重視效能和大小: Reason 的建置系統 bsb 建置時間小於 100ms(遞增),產生的結果也會很小
  • 漸進式學習 & 程式碼庫轉換: 也可以在 Reason 中貼上 Javascript 的程式片段再慢慢調整為 Reason 的程式碼
  • 基本使用 immutable and functional 但是也提供 side-effectmutation 的彈性
  • Reason 的 build system (bsb) 建構精簡可閱讀的 Javascript code
  • 完整的生態圈和工具鍊: 編輯器, NPM 套件, Reason-React, webpack

compiler 流程

當你完成一個簡單的 .re 檔案 (這是基本的 reason檔案)

會經由下圖的過程幫你編譯成 Javascript

流程

Install Reason on Mac

1
2
$ npm install -g reason-cli@latest-macos
$ npm install -g bs-platform
  • reason-cli - Reason 的環境套件
  • bs-platform - BuckleScript 和 Reason 基本套件

initial First Reason Project

1
$ bsb -init hello-world -theme  basic-reason

第一次初始化之後會得到這樣的檔案結構

1
2
3
4
5
6
7
8
.
├── README.md
├── bsconfig.json
├── node_modules
│   └── bs-platform -> /usr/local/lib/node_modules/bs-platform
├── package.json
└── src
└── Demo.re

bsconfig.json

BuckleScript 的設定 json 檔案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "hello-world",
"version": "0.1.0",
"sources": {
"dir" : "src",
"subdirs" : true
},
"package-specs": {
"module": "commonjs",
"in-source": true
},
"suffix": ".bs.js",
"bs-dependencies": [
// add your dependencies here. You'd usually install them normally through `npm install my-dependency`. If my-dependency has a bsconfig.json too, then everything will work seamlessly.
],
"warnings": {
"error" : "+101"
},
"namespace": true,
"refmt": 3
}
  • name - 專案名稱
  • version - 版本
  • sources
    • dir - source 的資料夾
    • subdirs - 是否要編譯子資料夾內的 re (Boolean or Array)
  • package-specs
    • module - 編譯後使用哪種 Javascript 模組 (default: commonjs)
    • in-source - 編譯的時候是否也要輸出
  • suffix - 編譯後的 js 的 後綴
  • bs-dependencies - 列出你使用 NPM(Yarn) 安裝的第三方 套件
  • bs-dev-dependencies- 列出你使用 NPM(Yarn) 安裝的第三方 開發套件
  • namespace - name 是 package 名稱,可以選擇是否開啟命名空間 (default: false)
    • 例如您有一個 Util.re 的檔案,如果沒有開啟命名空間,你的第三方套件也有一個 Util 的套件,他們會造成衝突,,這個參數影響的是這個 lib 的使用者,而不是自己本身
  • refmt - 當你使用 Reason V3 syntax 則明確指定為 3
  • reason - 預設是打開的,但是若有使用 ReasonReact, 設定則為
1
2
3
4
{
"reason": {"react-jsx": 2},
"refmt": 3
}

merlin

在你的專案中還隱藏了一個小小的檔案 .merlin

這個檔案雖然只有短短幾行

但是扮演相當重要的角色

他會協助你的 格式檢查, autocompleate

執行您的第一個 Hello world

1
$ npm run start

然後會開始編譯

編譯完成

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

Welcom Reason’s World

truffle pet demo

設定步驟

  1. 設定開發者相關環境變數
  2. 建立一個 Truffle 專案
  3. 寫一個基本的合約
  4. 編譯與部屬你的合約
  5. 測試你的合約
  6. 建立一個介面來跟你的合約溝通
  7. 放到瀏覽器開工囉

後端

1. 設定開發者相關環境變數

安裝基本環境

Nodejs

Git

Ganache

個人的以太區塊鏈的測試環境

1
2
3
$ npm install -g truffle
$ truffle --version // 檢查是否安裝成功以及版本
$ mkdir pet-shop-tutorial && cd pet-shop-tutorial

2. 建立一個 Truffle 專案

Truffle 有提供一些套件協助建立 DAPP

Truffle Boxes

我常用 React 開發

Truffle React Box

所以這裡使用 React 作為範例

建立一個 pet-shop 的 basic project

1
2
3
$ npx truffle unbox react
$ npm install -g truffle
$ truffle unbox react
1
Truffle initial 有很多方式, `truffle init` 會幫你建立一個空的 Truffle 專案

Truffle init

檔案結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  .
├── LICENSE
├── box-img-lg.png
├── box-img-sm.png
├── bs-config.json
├── build
│   └── contracts
│   ├── Adoption.json
│   └── Migrations.json
├── contracts
│   ├── Adoption.sol
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── package-lock.json
├── package.json
├── src
│   ├── css
│   │   ├── bootstrap.min.css
│   │   └── bootstrap.min.css.map
│   ├── fonts
│   │   ├── glyphicons-halflings-regular.eot
│   │   ├── glyphicons-halflings-regular.svg
│   │   ├── glyphicons-halflings-regular.ttf
│   │   ├── glyphicons-halflings-regular.woff
│   │   └── glyphicons-halflings-regular.woff2
│   ├── images
│   │   ├── boxer.jpeg
│   │   ├── french-bulldog.jpeg
│   │   ├── golden-retriever.jpeg
│   │   └── scottish-terrier.jpeg
│   ├── index.html
│   ├── js
│   │   ├── app.js
│   │   ├── bootstrap.min.js
│   │   ├── truffle-contract.js
│   │   └── web3.min.js
│   └── pets.json
├── test
└── truffle-config.js

* contracts : Solidity 的檔案,在這個範例中有一個 Migrations.sol 的範例檔案

  • migrations : Truffle 利用 migrations system 來處理開發環境,A migration is an additional special smart contract that keeps track of changes.
  • test : 測試檔案
  • truffle.js : Truffle 的設定檔案

3. 寫一個基本的合約

constracts 中建立一個檔案 Adoption.sol

Adoption.sol

1
2
3
4
5
pragma solidity ^0.5.0;

contract Adoption {

}
  • pragma 定義使用 solidity 編譯器的版本

變數

Solidity 有一種特別的變數 adress,這是代表 Ethereum 的位址

儲存了 20 個 byte 得值, 每一個帳號和合約在 Ethereum block chain 都擁有一個 adress

這個變數是 唯一的

Adoption.sol 中宣告一個 adress

1
2
3
4
5
pragma solidity ^0.5.0;

contract Adoption {
address[16] public adopters;
}
  • 宣告一個變數 adopters 這是一個陣列儲存 Ethereum 的 address
    Array 內有一種類型,在這裡的長度設定為 16

  • adopters 設定是 public, public 有 getter method 回傳一個值
    但是因為在這個範例 adopters 是一個陣列,所以等等會寫一個 function 回傳整個陣列

First Function: Adopting a pet

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.5.0;

contract Adoption {
address[16] public adopters;
// Adopting a pet
function adopt(uint petId) public returns (uint) {
require(petId >= 0 && petId <= 15);

adopters[petId] = msg.sender;

return petId;
}
}
  • Solidity 中必須要定義函式的 輸入值和回傳值的型態,在這個範例中會接收一個 petId (整數)也會回傳一個整數

  • 在函式要保證 petId 的範圍值必須在 adopters 陣列 範圍內, Solidity 中的陣列 index 是從 0 開始,所以這個 ID 會在 0~15 之間, 我們利用 require() 來定義這個範圍

  • 如果這個 ID 在允許的範圍內 (0 ~ 15) 之間,則新增這個人的位址到採用者的陣列 adopters 而這個人的位址則是利用 msg.sender 來取得

  • 最後回傳 petId 提供確認

Second Function: Retrieving the adopters

array getter 只能回傳一個值,但是但是 16 個 API 不是很實際,所以我們需要一個 API 來回傳所有的寵物列表

  1. adopt() 後增加一個 getAdopters() 這個 function
1
2
3
function getAdopters() public view returns (address[16] memory) {
return adopters;
}
  1. adopters已經宣告過了,可以直接回傳,但是要確認加上 memory 這個關鍵字 確認給出的是變量的位置
  2. view 這個關鍵字則代表這個 function 部會修改這個合約的任何狀態值,更詳細的資訊可以查看這

memory 表示 adopters 的值存在記憶體裡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.5.0;

contract Adoption {
address[16] public adopters;
function adopt(uint petId) public returns (uint) {
require(petId >= 0 && petId <= 15);
adopters[petId] = msg.sender;
return petId;
}

function getAdopters() public view returns (address[16] memory) {
return adopters;
}
}

在這邊我們將回傳之前宣告的 adopters 定義類型為 address[16]

編譯並部署智能合約

Truffle 有一個開發者控制台, 他會生成一個開發區塊鏈,可以利用他測試部署合同

他可以直接在控制台運行,本範例大部分將會使用它完成

編譯

Solidity 是一個編譯語言,所以需要一個編譯器先編譯之後才能執行,

1
$ truffle compile

這時候應該會看到這些資訊

1
2
3
Compiling ./contracts/Adoption.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts

部署

資料夾內會看到 migrations/1_initial_migration.js 這個檔案,,他是一個更改 contract 狀態的部屬腳本,會避免未來重複部署同樣的 Migrations.sol 合約

現在我們需要新增一個屬於我們自己的部署 script

  1. migrations 資料夾中新增一個檔案 2_deploy_contracts.js
  2. 2_deploy_contracts.js 內容
1
2
3
4
5
const Adoption = artifacts.require("Adoption");

module.exports = function (deployer){
deployer.deploy(Adoption);
}

在部署之前要先執行 Ganache 他會有一個本地的區塊鏈在 port 7575

若您尚未下載 download Ganache

image

然後回到終端機

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  $ truffle migrate
Compiling ./contracts/Adoption.sol...
Writing artifacts to ./build/contracts

⚠️ Important ⚠️
If you're using an HDWalletProvider, it must be Web3 1.0 enabled or your migration will hang.


Starting migrations...
======================
> Network name: 'development'
> Network id: 5777
> Block gas limit: 6721975


1_initial_migration.js
======================

Deploying 'Migrations'
----------------------
> transaction hash: 0x9965bb63687936396ef9db5830b9e0a9ff36f10108b775abf944fc86f061454c
> Blocks: 0 Seconds: 0
> contract address: 0x3216882738b0ca58BD4a2a3125Fa4bC651100C7e
> account: 0x10D045570AD2a69921Dc4e6b55148e071fC7484D
> balance: 99.99430184
> gas used: 284908
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00569816 ETH


> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00569816 ETH


Summary
=======
> Total deployments: 1
> Final cost: 0.00569816 ETH

在 Ganache 中 blockchain 的狀態改變了從原本的 0 改變為 4 ,之後會再討論到交易成本

寫好第一個合約並且部署上了區塊鏈,接下來要開始測試一下你的合約

transaction hash 代表這個合約的序號

你可以透過這個序號來搜尋這個合約

回到 Terminal migrate 合約到鍊上

1
$ truffle migrate

結果會是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Starting migrations...
======================
> Network name: 'development'
> Network id: 5777
> Block gas limit: 6721975


2_deploy_contracts.js
=====================

Deploying 'Adoption'
--------------------
> transaction hash: 0xac113a702da3ab7a8fde7ec8941143ff854cb8ae3f2457e1cc9251a0fa62a2b8
> Blocks: 0 Seconds: 0
> contract address: 0xAc30aaD46a83f8E8De3f452B0d4C175a1173a54b
> account: 0x10D045570AD2a69921Dc4e6b55148e071fC7484D
> balance: 99.98838348
> gas used: 253884
> gas price: 20 gwei
> value sent: 0 ETH
> total cost: 0.00507768 ETH


> Saving migration to chain.
> Saving artifacts
-------------------------------------
> Total cost: 0.00507768 ETH


Summary
=======
> Total deployments: 1
> Final cost: 0.00507768 ETH

image

  1. 可以打開 Ganache 之前的數值是 0 現在會變成 4,也可以看到第一個帳號原本是 100但是現在不到 100 (我的顯示是 99.99),因為這次的 migration 花費了乙太幣,等等會討論到更多關於這個花費的問題

測試智能合約

Truffle 如何測試你的合約呢?

  1. 建立一個 TestAdoption.soltest 的資料夾下
  2. 內容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.5.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/Adoption.sol";

contract TestAdoption {
Adoption adoption = Adoption(DeployedAddresses.Adoption());

uint expectedPetId = 8;

address expectedAdopter = address(this);
}
  • Assert.sol: 提供 assertions 使用,判斷是否相等,大於小於等等的判斷
  • DeployedAddresses.sol: 側是的時後 Truffle 會在鍊上部署一個新的實例,會取得那一個的位址來使用模擬
  • Adoption.sol: 要測試的智能合約內容

然後再定義其他的變數

  • DeployedAddresses 模擬部署一個智能合約取得他的位址
  • expectedPetId 提供測試的寵物 ID
  • 因為預計 TestAdoption 合約會發送交易,預期的 sneder 位址設為此,取得現在合約的 address
測試 adopt() 函式

測試 adopt() 使用這個函式成功後回傳 petId

可以判斷這個 petId 的直是否正確

  1. TestAdoption.sol 中的 Adoption 中增加下面的程式碼
1
2
3
4
5
function testUserCanAdoptPet() public {
uint returnedId = adoption.adopt(expectedPetId);

Assert.equal(returnedId, expectedPetId, "Adoption of the expected pet should match what is returned.");
}
  • expectedPetId 是我們要認養的 寵物 ID 跟回傳的 returnedId是否相等
測試單個寵物主人的主人

public 變數會有一個 getter 的 function 來取得,測試的過程中數據會持續存在

所以可以沿用 expectedPetId 在其他測試中

  1. 增加一個 function 在 TestAdoption.sol
1
2
3
4
5
function testGetAdopterAddressByPetId() public {
address adopter = adoption.adopters(expectedPetId);

Assert.equal(adopter, expectedAdopter, "Owner of the expected pet should be this contract");
}

取得 adopter 的位址 存在合約中,利用 Assert 判斷是否一致

測試所有寵物主人
1
2
3
4
5
function testGetAdopterAddressByPetIdInArray() public {
address[16] memory adopters = adoption.getAdopters();

Assert.equal(adopters[expectedPetId], expectedAdopter, "Owner of the expected pet should be this contract");
}

注意 adopters 屬性,因為有 memory 關鍵字代表存在記憶體中,不是存在合約的 storage 中,當 adopters 在一個陣列中,比較了陣列中的 expectedAdopter做比較

Running test

1
2
3
4
5
6
7
8
9
10
11
12
$ truffle test
Using network 'development'.

Compiling ./contracts/Adoption.sol...
Compiling ./test/TestAdoption.sol...
Compiling truffle/Assert.sol...
Compiling truffle/DeployedAddresses.sol...

TestAdoption
✓ testUserCanAdoptPet (75ms)
✓ testGetAdopterAddressByPetId (64ms)
✓ testGetAdopterAdderssByPetIdInArray (138ms)

參考資料

Tutorial

Javascript-High Order Function

High Order Function

是一種 接受 Funciton 回傳 Function 的 Function

Map, Filter, Reduce 都是

Function 回傳一個 Function

範例一

1
2
3
4
5
6
7
8
9
//基本款
const sum = x => {
return y => {
return x + y;
};
};

//簡潔款
const sum2 = x => y => x + y;

applyTwice 就是一個 High Order Function

1
2
3
const applyTwice = func => (...args) => func(func.apply(null, args));

console.log(applyTwice(x => "yo!" + x)("world"));

func 中回傳兩次呼叫的自己,

所以會收到 yo!yo!world

Map

實作 Map

先來看看 Array.prototype.Map 是如何使用的

1
2
3
const array = [1, 2, 3, 4];
array.map(item => console.log(item)); // 1,2,3,4
console.log(array.map(item => item + 1)); //[2,3,4,5]

不希望使用到 prototype

實作的時候會偏向 lodash 的 Map

1
2
3
4
import _ from "lodash";
const array = [1, 2, 3, 4];
_.map(array, item => console.log(item)); //1,2,3,4
console.log(_.map(array, item => item + 1)); //[2,3,4,5]

真正來實作

1
2
3
4
5
6
7
8
9
10
11
12
13
const map = (array, callback) => {
if (array.length == 0) {
return [];
} else {
const x = array.pop();
map(array, callback) || [];
callback(x);
}
};

const arr = [1, 2, 3, 4];
map(array, item => console.log(item)); //1,2,3,4
console.log(map(array, item => item + 1)); //[]

似乎需要回傳一些東西

1
2
3
4
5
6
7
8
9
10
11
12
13
const map = (array, callback) => {
if (array.length == 0) {
return [];
} else {
const x = array.pop();
const result = map(array, callback) || [];
result.push(callback(x));
return result;
}
};
const arr = [1, 2, 3, 4];
map(array, item => console.log(item)); //1,2,3,4
console.log(map(array, item => item + 1)); //[1,2,3,4]

Filter

Filter 的使用範例

1
2
3
const array = [1, 2, 3, 4, 5];
console.log(array.filter(x => x > 2)); // [3,4,5]
console.log(array.filter(x => x > 4)); // [ 5 ]

針對 Filter 的實作

1
2
3
4
5
6
7
8
9
10
11
const array = [1, 2, 3, 4, 5];
const filter = func => array => {
if (array.length === 0) {
return [];
} else {
const [x, ...xs] = array;
const filterResult = func(x) ? [x] : [];
return [...filterResult, ...filter(func)(xs)];
}
};
console.log(filter(x => x > 2)(array));

Zip

Zip 就是將兩個陣列合成一個二維陣列大小以較小的陣列為基準

輸出結果

1
2
zip([1, 2, 3])([0, 0, 0]) = [[1, 0], [2, 0], [3, 0]]
zip([1, 2, 3, 4, 5])([87]) = [[1, 87]]
1
2
3
4
5
6
7
8
9
10
11
12
const zip = array1 => array2 => {
if (array1.length === 0 || array2.lenght === 0) {
return [];
} else {
const [x1, ...xs1] = array1;
const [y1, ...ys1] = array2;
const result = zip(xs1)(ys1);
return [...result, [x1, y1]];
}
};

console.log(zip([1, 2, 3])([0, 0, 0]));

Quick Sort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const quickSort = array => {
if (array.length < 2) {
return array;
} else {
const [basic, ...xs] = array;
const left = xs.filter(item => item < basic);
const right = xs.filter(item => item >= basic);
return [...quickSort(left), basic, ...quickSort(right)];
}
};

const array = [77, 1, 2, 5, 3, 2, 33, 34, 44, 66, 22];

console.log(quickSort(array)); //[ 1, 2, 2, 3, 5, 22, 33, 34, 44, 66, 77 ]

Reduce

參數

Reduce 又稱為 Fold

Reduce 就與 Map 跟 Filter 不同了

Redcue 須傳入 function 與 初始值

且 Reduce 是依序接到四個變數︰

  • accumulator

    • 累積值
  • element 當前元素

  • index - index

  • array 正在執行 reduce 的陣列

Reduce 的遞迴實作

1
2
3
4
5
6
7
8
9
10
const reduce = func => acc => array => {
if (array.length == 0) {
return acc;
} else {
const [x, ...xs] = array;
return reduce(func)(func(acc, x))(xs);
}
};
const reverse = reduce((acc, el) => [el, ...acc])([])([1, 2, 3]);
console.log(reverse);

functional

1
2
3
4
5
6
7
8
9
10
const reduce = func => acc => array => {
if (array.length == 0) {
return acc;
} else {
const [x, ...xs] = array;
return reduce(func)(func(acc, x))(xs);
}
};
const reverse = reduce((acc, el) => [el, ...acc])([])([1, 2, 3]);
console.log(reverse);

參考資料

關於 JS 中的淺拷貝和深拷貝

|