SOVMedCare Tech

@sovmedcare


  • 首頁

  • 關於

  • 作者群

  • 標籤

  • 分類

  • 歸檔

初識Haskell

Tu-Szu-Chi 發表於 2018-03-19

Haskell

簡介

在之前我們介紹了些Functional Programming的概念(ex. Currying/High-Order-Function…)
Haskell就是最具有代表性的函數式語言,整個應用就是透過定義函數而組成,大致上函數皆沒有副作用(Haskell用特定類型表達副作用,後續會提及)
不再像命令式語言命令電腦要做什麼,而是用函數來描述出問題是什麼
其本身也有社群 - Hackage 持續發佈許多工具讓我們更方便建立應用
而多數理工人都碰過的GHC編譯器其實就是Glasgow Haskell Compiler的縮寫
接着我會從安裝到語法來介紹這門特殊的程式語言
logo

安裝

在Haskell官方頁面中有三種方式讓我們建設基本環境,在此我選擇功能較齊全的Haskell-Platform來安裝,並用Docker新建了純淨的Ubuntu環境來操作
有碰上ERROR: Unable to locate package,先下一個apt-get update再apt-get install haskell-platform就可以了
安裝後如果發現ERROR: stack command not found,可參考stack 官方的步驟

ghci

ghci這指令可讓我們直接進入終端機來嘗試Haskell,就像Python的python指令一樣

1
2
3
4
5
root@758b279035c4:~# ghci
GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help
Prelude> :q
Leaving GHCi.
root@758b279035c4:~#

stack

stack指令主要是用來管理Haskell Project,stack new可以快速地幫我們建立一個基礎的專案架構
在此用的版本是1.6.5,截至今日,建議升級到1.6以上

1
2
3
4
5
6
7
# stack --version
# stack upgrade
stack new my-project
cd my-project
stack setup
stack build
stack exec my-project-exe

cabal

cabal主要用來管理package,stack背後也就是cabal在運作

解釋

現在/my-project內會有stack.yaml & my-project.cabal兩個檔案,在此要稍微解釋一下
這和我們在寫Javascript專案時不一樣,不單單只有一個package.json幫我們管理第三方套件
首先,打開my-project.cabal,會看到cabal定義的package主要會有:

  • Name & Version
  • 0 or 1 libraries
  • 0 or more executables
  • A cabal file (在此是my-project.cabal)

而打開stack.yaml,stack定義的project會有:

  • A resolver
  • Extra dependencies (extra-deps)
  • 0 or more local Cabal packages

stack.yaml中的resolver除了定義要用什麼來編譯外,也決定了.cabal中build-depends的package版本
各位可以試試看,當resolver = lts-10.5 or resolver = lts-9.4兩種狀況時,當stack build過程,network的版本分別是多少

1
2
3
4
5
6
7
8
executable my-project-exe
hs-source-dirs: app
main-is: Main.hs
ghc-options: -threaded -rtsopts -with-rtsopts=-N
build-depends: base >= 4.7 && < 5
, network
, my-project
default-language: Haskell2010

對於各個resolver對應的package version都可以在這網站查到
那當你想用Hackage上的某個package,卻沒在當前的resolver找到呢?
這時只要在stack.yaml的extra-deps添加即可(版本號必備)

1
2
extra-deps:
- accelerate-cublas-0.0

而專案的架構也可以從Github上發現,在package的hs-source-dirs資料夾,除了主要的Main檔外,不會有多餘的檔案
自行定義的諸多Function都整理爲Library,並在.cabal的library: Exposed-Modules載入
可以參考 begriffs/postgrest & uber/queryparser這兩個repo

語法

首先,有兩種方式可以讓我們進入GHC的互動模式

1
2
3
4
5
6
# 1
stack ghci
# 2
cd app/
ghci
:l Main

第一種方式會幫我們自動載入所有檔案,不需要一個個引入;而第二種就要個別引入檔案,在此預設使用第一種方式
當指令下完後,你的終端機會出現這些文字

1
2
3
...
Loaded GHCi configuration from /private/tmp/ghci94387/ghci-script
*Main Lib>

但前綴有點長(引入的檔名都顯示出來),我們再直接下個指令更改

1
:set prompt "ghci>"

然後跑stack project預設的程式試試

1
2
3
ghci> main
someFunc
ghci>

Operator

一般常用的數學運算符用法都差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ghci> 1 + 1
2
ghci> 1 - 1
0
ghci> 2 * 2
4
ghci> 2 / 2
1.0
ghci> 1 == 1
True
ghci> 1 == 2
False
ghci> 1 /= 1
False
ghci> 1 /= 2
True

其實這每一個運算符也都是Function,這些被兩個參數夾在一起呼叫的我們稱爲中綴函數,其餘則是前綴函數(大部分皆這種)

1
2
3
4
ghci> max 5 1
5
ghci> mod 10 5
0

Haskell的Function在傳遞參數並不用逗號、括號來分割,純粹用空格來表示;當然也有括號的優先順序

1
2
3
4
ghci> max 5 (max 10 1)
10
ghci> (succ 9) + (max 5 4) + 1
16

但這些前綴函數有時會讓人覺得不易讀,所以我們也可以將它們改成用中綴的方式寫,只要這樣加上`即可

1
2
3
4
ghci> 10 `mod` 5
0
ghci> 10 `div` 5
2

函數

接下來的Code會直接寫在Main.hs裡頭,每當存檔後,只要下:r這指令就會重新幫我們載入
來寫一個addTwo的Function

1
2
addTwo :: Num a => a -> a
addTwo n = n + 2

第一行是定義這個Function的Type,代表它接收一個型別爲Num的參數並同樣回傳Num(你也可以先不定義);第二行就是這Function主要做的事情
:r重新載入後,如果剛剛你沒定義這Function的Type,可以用:t addTwo看見Haskell自動幫你定義了,:i addTwo也可試試看

1
2
addBoth :: Num a => a -> a -> a
addBoth x y = x + y

if/else

用法也大同小異,只是在Haskell中,else不可省略(爲了確保一定會回傳一個值)
不論是5/x+y/if...then...else都只是個表達式(就是返回一個值的程式碼),所以可以輕鬆的在函數中使用

1
2
3
4
5
doubleSmallNumber x = if x > 100
then x
else x*2
doubleSmallAndAddOne x = (if x > 100 then x else x*2) + 1

List

這是我們很熟悉的資料結構,許多資料都用List來操作,但在Haskell中List是一種單型別資料結構,List內的元素皆要是同型別
GHCi模式中可以用 let a = [1,2,3] 定義常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- let 定義
ghci> let temp = [1,2,3]
ghci> temp
[1,2,3]
-- 合併List
ghci> [1,2,3] ++ [3,4,5]
[1,2,3,4,5]
-- String其實就是一組字元的List而已
ghci> ['h','a'] ++ ['O','O']
"haOO"
ghci> "ha" ++ "OO"
"haOO"

在用++要特別注意,Haskell會遍歷整個List(符號左邊那個),如果List長度大時會跑一陣子
單單插入一個元素可以使用:往List前端插入,這樣較高效

1
2
3
4
ghci> 1:[2,3,4]
[1,2,3,4]
ghci> 1:2:[3,4]
[1,2,3,4]

一些List的函數

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
-- 取List的值
ghci> [1,2,3,4] !! 2
3
ghci> head [1,2,3,4]
1
ghci> tail [1,2,3,4]
[2,3,4]
ghci> last [1,2,3,4]
4
ghci> init [1,2,3,4]
[1,2,3]
ghci> length [1,2,3,4]
4
ghci> null [1,2,3,4]
False
ghci> null []
True
ghci> reverse [1,2,3,4]
[4,3,2,1]
ghci> take 3 [1,2,3,4]
[1,2,3]
ghci> drop 2 [1,2,3,4]
[3,4]
ghci> minium [1,2,3,4]
1
ghci> maximum [1,2,3,4]
4
ghci> sum [1,2,3,4]
10
ghci> product [1,2,3,4]
24
-- 檢查元素是否在List裡
ghci> 4 `elem` [1,2,3,4]
True
ghci> 5 `elem` [1,2,3,4]

再來介紹List中一個好用的功能 - Range
欲表達一個1~20的List我們可以這樣做

1
2
3
4
5
6
7
8
9
10
11
ghci> [1..20]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
-- 字元也行
ghci> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
-- 也可以給它間距
ghci> [1,3..20]
[1,3,5,7,9,11,13,15,17,19]
-- 但間距只能給一次
ghci> [1,3,5..20]
error: parse error on input ‘..’

我們也可以不標註界限,定義個無限長度的List - [1,3..]
由於Haskell是惰性的,所以並不會去取一個無限長度的值,它會等到你真的要取的時候,看你要多少才給多少

1
2
3
4
5
6
ghci> take 10 [1,3..]
[1,3,5,7,9,11,13,15,17,19]
ghci> take 14 (cycle "SOV! ")
"SOV! SOV! SOV!"
ghci> take 10 (repeat 5)
[5,5,5,5,5,5,5,5,5,5]

接着也是個厲害的語法 - List Comprehension
這其實就是數學中Set Comprehension的概念,從既有的集合按照規則產生一個新集合

1
2
3
4
ghci> [x*2 | x <- [1..10]]
[2,4,6,8,10,12,14,16,18,20]
ghci> [x + y | x <- [1..5], y <- [2,3]]
[3,4,4,5,5,6,6,7,7,8]

如果我們自己動手實現一個length'函數可以這樣

1
length' xs = sum [1 | _ <- xs]

_代表我們不在乎它是什麼值

Tuple

Tuple很像List - 將多個值存入一個容器中,我們剛剛有提到,List內的元素皆是同型別,且它的Type不會因爲內部的元素數目而有異
可是Tuple要求明確的內部數據數目,它的Type會取決於內部數據的數目&Type

1
2
3
4
5
6
7
8
9
10
-- 可以發現Type的異處 --
ghci> :t [1,2,3]
[1,2,3] :: Num a => [a]
ghci> :t (1,2,3)
(1,2,3) :: (Num c, Num b, Num a) => (a, b, c)
ghci> :t ([1,2], 3)
([1,2], 3) :: (Num b, Num a) => ([a], b)
-- 不會有單元素的Tuple --
ghci> :t (1)
(1) :: Num p => p

我們先介紹針對序對Tuple的Function(後續會提及其他長度的Tuple)

1
2
3
4
ghci> fst (1,2)
1
ghci> snd (1,2)
2

用List產生Tuple

1
2
3
4
5
6
7
8
9
10
ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]
-- 如果兩個List長度不一,取小的
ghci> zip [1,2,3] [4,5,6,7,8]
[(1,4),(2,5),(3,6)]
-- 所以也可以處理無限的List
ghci>zip [1,3..] ["one", "two"]
[(1,"one"),(3,"two")]

實際的應用上,我們以三角形爲例

1
let threeEdge = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]

我們必須再多給些條件才能讓它是三角形的集合(假設我要直角三角形)

1
2
-- c是斜邊, b是長邊, a是短邊
let triangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2 ]

最後我只想要周長爲24的三角形,所以再修改一下

1
let finalTriangle = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24 ]

進階函數

語法提及的差不多,接着要持續深入Haskell中Function的精髓

模式匹配 (Pattern matching)

這是一個很實用的功能,真心推薦每個語言都帶有除了能幫我們Code寫的更簡潔外,取參數也變得更加容易
來個簡單的範例

1
2
3
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY SEVEN!"
lucky x = "Sorry, you're out of luck..."

Pattern Matching會從上到下開始匹配,如果把最後一個匹配一切的模式移到最前面,那永遠會回傳Sorry, you’re out of luck…
我們來實作個階乘函數,可以計算10!,7!之類的,而條件就是0!回傳1

1
2
3
4
-- 還結合了遞迴在裏頭~
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)

Pattern Matching的順序很重要,如果將兩個模式對調,那就是無窮盡的一直跑了…
我們再來把Tuple融合到模式匹配裡,以相加兩個Vector向量爲例

1
2
3
4
5
6
7
--- 一般Function寫法
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
-- Pattern Matching寫法
addVectors' :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors' (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

這跟Javascript的解構賦值有點像,我們再舉個三元組的Tuple來試試

1
2
3
4
5
6
7
8
first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z

_就跟剛剛一樣,我們不在乎它是什麼~
再來要以List爲例子,這也可能是往後很常用到的技巧
[1,2]就是1:2:[]的結果,也可以是1:[2],用變數表達來看即爲a:b:c or x:xs,這也是我們在模式匹配中針對List的寫法

1
2
3
4
-- 改寫head,List的匹配至少要有一個元素,所以空的就拋出錯誤
head' :: [a] -> a
head' [] = error "Empty !"
head' (x:_) = x

再來改寫length試試

1
2
3
4
-- 遞迴又用上了
length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs

總之在用Pattern Matching搭配遞迴的時後要記得給予邊界條件,就像我們針對空List或是值等於0那樣,避免無窮下去

Guards

Pattern Matching用來檢查一個值是否適合從中取值,而guards是用來檢查一個值的某項屬性是否爲真就是比較簡潔的if/else啦
先用個簡單的Bmi來試試

1
2
3
4
5
6
bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight"
| bmi <= 25.0 = "You're supposedly normal"
| bmi <= 30.0 = "You're fat!"
| otherwise = "You're a whale, congratulations!"

guard利用豎線來表示,並且要記得縮排(不然會報錯)
他跟Pattern Matching一樣,由上到下開始撮合,最後要給一個otherwise來處理未列舉的狀況
Haskell定義Function後面不需要加上=,且對縮排有要求,當然也可以用之前提過的中綴方式來寫

1
2
3
4
5
6
bmiTell' :: (RealFloat a) => a -> a -> String
weight `bmiTell'` height
| weight / height ^ 2 <= 18.5 = "You're underweight"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal"
| weight / height ^ 2 <= 30.0 = "You're fat!"
| otherwise = "You're a whale, congratulations!"

Where

在bmiTell'裡我們重複了三次weight / height ^ 2,照理說應該要讓它有個常量可以暫存著,所以我們將它改成

1
2
3
4
5
6
7
bmiTell' :: (RealFloat a) => a -> a -> String
weight `bmiTell'` height
| bmi <= 18.5 = "You're underweight"
| bmi <= 25.0 = "You're supposedly normal"
| bmi <= 30.0 = "You're fat!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2

要定義多個常量只要縮進就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
weight `bmiTell'` height
| bmi <= skinny = "You're underweight"
| bmi <= normal = "You're supposedly normal"
| bmi <= fat = "You're fat!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
-- 模式匹配版
...省略
where bmi = weight / height ^ 2
(skinny, normal, fat) = (18.5, 25.0, 30.0)

let

let & where很相似,差在於where綁定是在函數底部,綁定語法結構,且所有guard在內的整個函數皆可看見
let綁定則是個表達式,像是定義局部變數,所以對不同guard不可見
但我們也可以想成,一個是放在前面,一個後面這樣~
既然let是綁定表達式,所以可以這樣寫

1
2
3
4
5
6
7
8
9
10
ghci> 4 * (let a = 9 in a + 1)
40
-- 在其他結構裡
ghci> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]
-- 用在List Comprehension
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]

case of

在剛剛介紹了Pattern Matching後,你一定覺得莫名其妙,爲何還要有switch的功能?
其實case才是正宗的! Pattern Matching是語法糖來着~
給個範例看看就好

1
2
3
4
5
6
7
head' :: [a] -> a
head' [] = error "Empty !"
head' (x:_) = x
head'' :: [a] -> a
head'' xs = case xs of [] -> error "Empty !"
(x:_) -> x

lamda function

就是匿名函數,那些一次性的Function通常不會特地去定義它,我們可以這樣寫

1
2
mapAndPlusOne :: Num a => [a] -> [a]
mapAndPlusOne arr = map (\x -> x+1) arr

$ 函數呼叫符

1
2
ghci> :t ($)
($) :: (a -> b) -> a -> b

這只是個呼叫函數的符號而已,以往都是用空格直接呼叫函數,了不起再加個括號決定優先權
用空格呼叫的函數是左結合,f a b c等於(((f a) b) c),$是右結合的
假設今天有個sum (map sqrt [1..10]),我們可以改寫成sum $ map sqrt [1..10],少了括號也清楚多了
附上個需要想想的例子,可以用:t來看看位什麼

1
2
ghci> map ($ 3) [(4+),(10*),(^2)]
[7.0,30.0,9.0]

實例

到入門的最後一關總要練習實例,這邊主要以簡單的leetcode、Hackrank題型來講

快速排序(quick sort)

因爲我們要做排序,所以這邊要規定a爲Ord Typeclass的成員
主要邏輯就是:先取得比頭部小的數,然後quick sort,比頭部大的數,也是要quick sort
所以要怎取得比頭部大&小的數呢? List Comprehension!

1
2
3
4
5
6
quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
let smallerSorted = quicksort [a | a <- xs, a <= x]
biggerSorted = quicksort [a | a <- xs, a > x]
in smallerSorted ++ [x] ++ biggerSorted

簡潔有力,這就是Functional Programming的厲害之處

回文 (Palindrome Number)

給予一個Int並判斷它是不是回文的格式,例如, 1,121,4444,12321
先別想著用[Char]來做,單純用數值去操作
首先,我們要判斷是不是回文,至少要比較數字位數 除以 2次 - length / 2
假設數字是12321,在知道總位數&數值的前提下,我只要遍歷個/十位數,就可以知道千/萬位數是否相等
但因爲Int並沒有實作length,所以我們要用Function算 - calLength
並且再實作一個比較的Function,這Function已知我們的數值n & 總位數len,我們傳入位數i讓它比較相對的位數是否相等 - compare

  • calLength: 初始長度爲1,利用遞迴將長度算出來
  • compare: 已知的數值n & 長度len(這裡利用Currying簡化),取相對應的位數來比較,並回傳Bool
  • all: 可以利用:t來看它的Type,在此它會將檢查是否爲True的Function,map到一個List上,全爲True即True,否則False
1
2
3
4
5
6
7
8
9
10
11
12
palindrome :: Integer -> Bool
palindrome n
| n < 0 = False -- 不考慮負數
| otherwise = all (== True) $ map (compare n len') [1..(len' `div` 2)]
where len' = calLength 1 n
calLength len n
| n >= 10 = calLength (len+1) (n `div` 10)
| otherwise = len
compare n len i =
let high = (n `div` 10 ^ (len - i)) `mod` 10
low = (n `div` 10 ^ (i - 1)) `mod` 10
in high == low

再來是偷工減料轉爲字串版…

1
2
3
4
5
6
7
palindrome' :: Integer -> Bool
palindrome' n
| n < 0 = False
| str_n == re_n = True
| otherwise = False
where str_n = show n
re_n = reverse str_n

移動0 (Move Zeroes)

最後來題比較輕鬆的,給予一個[Int],將數字0都移到List最後面
例如: [1,4,0,3,0,1]轉成[1,4,3,1,0,0]
用Pattern Matching加上遞迴能讓我們寫的很精簡(記得給予遞迴邊界)

1
2
3
4
5
moveZeroes :: [Integer] -> [Integer]
moveZeroes (x:xs)
| null xs = [x]
| x == 0 = moveZeroes xs ++ [x]
| otherwise = x : moveZeroes xs

總結

以上提到的只是Haskell最初的入門知識點,還有很多像是Module、TypeClass、Monad之類的學問,會在之後的文章有個說明
Haskell可以利用其他第三方Module、pakage建構出一個完整的服務(I/O, Network…),並不單只是解些運算題而已
也推薦大家可以從Haskell趣學指南當做入門點
未來還有很多驚喜挫折等著你呢~

Reference

Haskell趣學指南
Hackage

Docker基礎架設(以Python Scrapy為例)

Tu-Szu-Chi 發表於 2017-11-15

簡介

Docker的介紹和教學已有相當多的資源,但當我以為可以快速地建立環境、連上DB,卻不盡然
許多資源很棒,寫得很詳細,但正因對每一項解釋太詳細以致我無法很快找到我要的知識點,故在此提供這一回摸索後,我認為能較快建立環境的流程。

環境

因一直想寫寫爬蟲,較著名的Framework是基於Python的 - Scrapy,但其實Scrapy之外還要安裝蠻多東西的,故能用Docker幫我們整合當然再好不過

安裝

  1. Docker
    • Mac
    • Windows
  2. Kitematic
    這是一個Docker的GUI工具,個人很推薦剛入門者使用

都安裝好後記得確認下Docker是否正常運行著,並打開Kitematic,會要求輸入Docker帳號密碼,沒有的可以申請一組

Mac使用者可看上方工具列小鯨魚點開,會亮綠燈且Docker is running

部署

總共會有三個Container來做示範,Scrapy/Postgres/Adminer

Scrapy

接著要建立Scrapy的Container,在搜尋框輸入「Scrapy」第一個就會是了(from scrapinghub)
點一下三個點點的More button會看到可以選擇Tag/Network,先選擇1.1-py3-latest的Tag讓後續我們的版本一致,選好後就按下Create即可

因我在run流程第二遍時有發生HTTP 500 error在這步驟,可以參考這篇,登出Kitematic的Docker Account

載好後Kitematic就會自動run這個Container,可以在Setting看到些資訊
Index

接著點擊上方的EXEC會將Container啟動在我們的終端機上,輸入scrapy並按下Enter確認Container正常運作

可以先點左下方齒輪(設定),將Exec command shell選擇bash會比較習慣

Postgres

再來按New去搜尋Postgres並選擇第一個官方的Image(在此是用10.1版本),一樣好了後利用EXEC在終端機上開啟並輸入psql
會有以下錯誤訊息

1
psql: FATAL: role "root" does not exist

改成輸入預設的帳號就可以了

1
psql -h localhost -U postgres

要離開psql就輸入\q

Adminer

接著New一個adminer的Container(官方的Image),選擇latest版,方便我們之後檢查是否有連上DB
好了後可以到Settings -> Hostname/Ports 確認下這Container的Published IP (有可能顯示的是localhost)
Adminer
直接點藍色IP字段就會將Adminer打開在瀏覽器了
再來要輸入Server位置,我們要在Postgres的command line輸入

1
ip address

會得到類似以下資訊
Ip address
第二段的172.17.0.3就是Postgres在Docker內網的位置
為了要管理Container的Network,可以到Settings -> Network檢查該Container是屬於哪一組網路(預設是bridge)
要在同一組網路下連內網IP才有用
所以我們的三個Container都是在bridge情況下,要連對方用內網IP即可
故在Adminer介面只要輸入剛剛得到的Postgres內網IP & Username,按下Login就會連上了
Adminer
環境架設就先到這裡!

專案

再來就拿實際專案來做示範,可以先clone我個人抓證交所資料的專案來測試

branch 選擇 docker-test

要讓Container讀取本機端的檔案,要設定Volumes,而在Kitematic上不是每個Image都可以直接到Settings -> Volumes 去設定

本次的Scrapy就不行,Postgres可以

所以要教大家利用指令來啟動Container & 指定Volumes(原有的Scrapy Container不需要了)

Step 1

先在終端機上輸入指令,找到我們的scrapinghub/scrapinghub-stack-scrapy Image,並記住Image ID

1
docker images

Step 2

在終端機輸入

1
docker run -it -v ~/stockParser/twse/:/twse [Image ID] bash

-i是讓Container的標準輸入保持打開;-t是讓Docker分配一個虛擬終端(pseudo-tty)並綁定到Container標準輸入上
-v即是Volumes的設定,冒號左邊是本機端位置,要用絕對路徑,冒號右邊是Container內的路徑
按下Enter後我們就建立了一個新的Container,並可在Docker內讀取到我們的專案,於Kitematic中可以重整Containers List看我們剛剛建立的容器(Mac就按 Cmd+R)

這裡也有個插曲,當按重整沒有任何新的容器出現時,我要用管理員權限打開Kitematic才看得到剛剛建立的Container,但Postgres/Adminer就看不到了
New Container

Step 3

接著在Scrapy的Container輸入ls就看得到twse資料夾,我們先cd進去,然後先安裝要用的Module

1
pip install -r requirements.txt

再到專案底下的/twse/twse/settings.py 底部的DATABASE填入我們Postgres的設定

如果發現你的DB和Scrapy是不同的Published IP
那你Scrapy要連DB就輸入Postgres的Published IP:Port
如果都一樣者只要先用ip address找Postgres內網ip再輸入即可
IP:Port

記得到adminer去新建一個database,在此預設名字為twse

Step 4

最後輸入以下指令,就會開始抓2017/10/18 電子工業的資料了

1
scrapy crawl stock

結語

以上是個人認為剛開始用Docker會想嘗試的地方,Docker還有很多強大的用途直得我們去學習
也歡迎有興趣的人可以一起完成這twse專案

參考連結

Docker Gitbook

husky + git hook

tancc 發表於 2017-10-24

git中,許多的操作指令都有所謂的hook,提供給我們先做一些前置作業再去真正執行git的指令。較常用hook的像是pre-commit, pre-push等等,通常會用這些hook搭配其他套件來做程式碼的規範檢查和自動化測試,來避免不好的程式碼推上repository。

git所提供的hook放在<project>/.git/hooks裡面,可以自行寫shell或是其他腳本語言去修改。不過這樣的方式比較麻煩,所以我們是透過husky,將指令定義在package.json來達成的。

husky

husky的這套件的描述就是

Git hooks made easy

有多簡單呢?假設現在我們要在commit之前先通過lint檢查,push之前通過測試,只要兩個步驟

  1. 安裝 husky

yarn add husky --dev

  1. 修改package.json,定義precommit hook
1
2
3
4
5
6
7
8
{
"scripts": {
"test": "jest",
"lint": "standard",
"precommit": "yarn lint",
"prepush": "yarn test"
},
}

lint-staged

不過通常我們不會希望每次commit之前,都對所有的js檔案做lint,只要對這次修改的檔案去檢查即可。

這時候就可以搭配lint-staged來使用。

  1. 安裝 lint-staged

yarn add lint-staged --dev

  1. 修改package.json,加入lint-staged設定
1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"test": "jest",
"lint": "standard",
+ "precommit": "lint-staged",
"prepush": "yarn test"
},
+ "lint-staged": {
+ "*.js": ["standard --fix", "git add"]
+ }
}

lint-staded可以不只做一件事,我們可以再順便加上css檔案的lint, format

1
2
3
4
5
6
7
8
9
10
{
"lint-staged": {
+ "*.js": ["standard --fix", "git add"],
+ "*.{css,less,scss,sss}": [
+ "stylefmt",
+ "stylelint",
+ "git add"
+ ]
}
}

參考資料

  • husky
  • lint-staged

使用Immutable.js

tancc 發表於 2017-10-17

解決的問題

  • mutable state
    JavaScript中,變數的值預設是mutable的,所以在程式中的數值、狀態等等都是可變的。mutable這性質常會導致非預期的結果,而通常導致某數值或是狀態改變的地方太多了難以掌握,所以在追蹤和維護那些可變的狀態時,就是非常費工的任務了。

  • 物件比對不便
    JavaScript中在做相等判斷時通常會用===,但若比較的對象是物件,這只有做到shallow compare,無法知道其內層是否相等,需要自己再去做deep compare。但這過程就比較複雜且效能很差。

  • Object.assign的效能問題
    為了解決上述的問題,一個方法就是想辦法擁有immutability。為了達到此目的,原生JavaScript能使用的方法有幾個:Object.freeze,但會使得許多操作變得更加複雜麻煩;
    在做有關物件改變的操作時使用Object.assign或是spread operator來返回全新的物件,間接得到物件的不可變性。但是Object.assign等方法需要做許多複製的行為,當物件龐大時效能問題就會出現了。

特色及優點

  • persistent data structures

對資料做改動時不會修改到資料本身,會保留舊資料回傳新資料。也就是FP中常提及的immutability的概念,所以少了side-effect所造成的困擾。

  • 使用Trie結構存放資料

Trie資料結構的介紹。因為使用了Trie所以在資料比對上十分便利,只需檢查hashcode是否相同即可,不用做如deepCompare這類耗資源的方式。

  • structural sharing

structural_sharing

不像純JS的Object.assign的方式,將修改的物件整份複製。使用Immutable.js時,只有修改到的地方的節點會重新產生,而沒有變動的節點會共用。因此可以節省許多記憶體

  • 方便的API
    提供了許多API如updateIn、setIn、getIn等等,讓我們在對巢狀資料的操作上方便許多

shouldComponentUpdate

在開發React專案時,immutable.js是非常好用的工具。由於re-render是很耗效能的行為,所以常需要去撰寫shouldComponentUpdate中的比對邏輯,減少re-render的次數。
使用純JavaScript的話,須去寫許多客制的比較邏輯,難以避免的還有deep compare行為,這會使得光是判斷物件相等與否,就耗掉了需多運算資源。
而使用immutable.js的話,使用shallow compare就可以滿足所有需求了。因為在immutable.js中,若是物件彼此內部不同,必定會是不同的物件。

參考資料

Immutable.js, persistent data structures and structural sharing

Currying介紹

tancc 發表於 2017-10-16

節錄自wiki

currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

舉例來說,若是有一函式gn2,他的input為某一需要多個參數的函式fn,output為需要一個參數的函式fn2而此函式亦回傳需要一個參數的函式…,直到原函式fn的參數皆被滿足。則gn2所做的就是currying函式fn。

1
2
3
4
5
6
7
8
9
10
11
12
13
const curry = fn => {
const getCurriedFn = prev => arg => {
const args = [...prev, arg]
return args.length < fn.length ? getCurriedFn(args) : fn(...args)
}
return getCurriedFn([])
}
const add = (a, b, c) => a + b + c
const curriedAdd = curry(add)
const add2 = curriedAdd(2)
const add2And3 = add2(3)
console.log(add2And3(4)) // 9

優點

  • 搭配partial application,寫出更泛用、復用性高的函數
    若函數支援currying的話,我們可以只先放入某幾個參數(partial application)來製造出可以被其他情境使用的函數。而不用每次都把函數寫死,傳入完整的參數。

    1
    2
    3
    4
    const add2 = add(2) // 用在需要加2的地方
    const splitAt5 = splitAt(5) // 將字串從第5個字元處切開
    const doubleList = map(x => x * 2) // 將陣列中數字都兩倍
    ...
  • 更為簡潔易讀的函數
    因為搭配了partial application,只需傳入幾個特定的參數,可以寫出較為語易化且簡短的函數。
    由上述可以看出,這種方式得到的函數,我們可以很清楚從字面上知道他想做什麼。

  • 更方便去做 function composition
    我們寫程式時,會只針對問題的各個小流程去定義函數。由於每個函數可被定義的很語易化,能清楚了解每個步驟的意圖,再藉由函數組合就能達到想要的結果

1
2
3
4
5
6
7
8
9
10
11
12
// 簡單範例 沒太大意義...
const joinWithDash = join('-')
const strEq = eqBy(String);
const uniqByStr = uniqWith(strEq)
const transStr = compose(
joinWithDash,
uniqByStr,
reverse
) // 先reverse,將重複的字刪除再用'-'連結
map(transStr)(['apple', 'banana', 'cat', 'door']) // ["e-l-p-a","a-n-b","t-a-c","r-o-d"]

使用關鍵

  • 撰寫函數時,欲操作的資料要放在最後一個參數
    這也是Hey Underscore, You’re Doing It Wrong!中提到的Underscore不好的地方,他所提供的函數將要操作的資料放在第一個參數,使得無法將其currying後去做composition。
    所以較好的方式是,撰寫函數時能把要操作的那個變數,放在最後參數的最後一個。所以較方便我們去撰寫pointfree style的函數,然後去做function composition。

相關資料

Hey Underscore, You’re Doing It Wrong!

使用HOC

tancc 發表於 2017-10-16

以往當我們要對component進行功能上的擴充時,以前常見的方法如mixin、inheritance等等,但現在都不推薦使用這些方式了。

用inheritance的話,會使得複用性變低,且當你只想要簡單的功能時,所繼承的物件背後可能相依著一大串東西。如一句名言“You wanted a banana but you got a gorilla holding the banana”。

mixin較大的問題在於不具有immutability,會去修改到原有的component。當在測試或是發生問題時,我們難以確認當前的狀態是component本身所導致的抑或是mixin的override所造成的。

什麼是 Higher-Order Component

是從 Higher-order function 這名字而來的

而 Higher-Order Component 可簡單解釋為:

a higher-order component is a function that accepts a component, and returns a new component that wraps the original

也就是一個function,他的輸入是一個component,輸出一個新的component。

簡單的形式大概會長這樣

1
2
3
4
5
6
const hoc = WrappedComponent => class extends Component {
// do something...
render() {
return <WrappedComponent {...this.state} {...this.props} />
}
}

例子

我們常會有一些頁面是有鎖權限的,例如只有登入的使用者才能進入,若沒有登入則跳到登入畫面

假設現在有兩個頁面(Account, Setting)是必須登入才可看見,在沒有用HOC時,最直覺的方法會是這樣寫

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
// Account
import React, {Component} from 'react';
import Login from 'components/Login'
export default class Account extends Component {
render() {
const {user} = this.props
if (user.isLogin) {
return <div>Account</div>
} else {
return <Login />
}
}
}
// Setting
import React, {Component} from 'react';
import Login from 'components/Login'
export default class Setting extends Component {
render() {
const {user} = this.props
if (user.isLogin) {
return <div>Setting</div>
} else {
return <Login />
}
}
}

可以發現明顯會有兩個問題:

  • 檢查是否登入的邏輯重複了。這使得之後如果要改變驗證邏輯時,必須去更改每個component做了許多重複的步驟,也使得產生bug的機會變多了。

  • component除了畫面呈現的樣子外,還需額外關心”何時要呈現”這件事。可以想像到的是,若是邏輯變多且沒有抽出時,component的程式碼會變得相當龐大,會難以維護、debug。比較好的做法是component只關注在畫面上,其餘邏輯部分抽出。

使用 HOC

同樣實作上述例子,但這次用HOC去改寫,將確認是否登入的邏輯抽出。

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
// authorized.js
import React, {Component} from 'react';
import Login from 'components/Login'
const authorized = WrappedComponent => class extends Component {
render() {
const {user} = this.props
if (user.isLogin) {
return <WrappedComponent {...this.props} />
} else {
return <Login />
}
}
}
export default authorized
// Account.js
import React, {Component} from 'react';
export default class Account extends Component {
render() {
return <div>Account</div>
}
}
// Setting.js
import React, {Component} from 'react';
export default class Setting extends Component {
render() {
return <div>Setting</div>
}
}
// 使用方法
const authorizedAccount = authorized(Account)
const authorizedSetting = authorized(Setting)
// ...

優點

從上面例子可以看到幾個優點

  • 共用邏輯抽出,增加復用性、減少重複代碼
    當把邏輯抽出時,可以省掉許多重複的代碼。且當我們的認證方法改變或是出問題時,只須去更動或檢查authorized.js這檔案即可,在維護以及修改上方便許多。

  • component專注在畫面呈現上

  • 使用composition的方式,返回全新的component,具有immutability

  • 較易測試
    測試component時就只需測試他的畫面正確性,而不需考慮其他如商務邏輯的部分。而測商務邏輯就直接測其邏輯的正確性,不需考慮畫面的呈現。
    且因為具有immutability的特性,測試時不會有如文章開頭所寫到的使用mixin,因為mutable state所造成的問題。

  • 程式較具有彈性
    當有其他的功能需要加上去時,我們只需要去定義各種對應的HOC,藉由函數的組合就能達到想要的效果,如compose(hocA, hocB, hocC)(Comp),也就是FP中常提及的function composition。

何時使用

當在撰寫component時,發現了許多重複的程式碼,如重複的邏輯條件判斷或是重複的前置動作等等,或是夾雜了畫面以外的商業邏輯時,就代表是可以嘗試使用HOC做進一步抽象的時機了!

許多library也都有用到HOC這技巧,常見如react-redux中的connect、react-router中的withRouter,以及recompose等等。

參考資料

Higher Order Components in React

React PureComponent and Performance

Tu-Szu-Chi 發表於 2017-10-12

React在15.3.0新增了PureComponent的類別,我們基本上可以直接無痛從extends Component轉到extends PureComponent
大部分情況下直接使用PureComponent會是較方便的做法,省去了自己寫shouldComponentUpdate (自己寫的話還會報錯)
但也不代表用了PureComponent整個效能就會提升,根本的療法還是要將Component寫的pure-render,意指假如傳給Component的prop & state都是一樣,那vDOM回傳的結果也該是一樣

Non-pure

最常見的non-pure寫法就像是以下

1
2
3
4
5
6
7
// 每次render傳進去的都是新的Object
<Button style={{color: 'red'}} />
<Children data={{x: 1, y: 2}} />
// inline-function, 每次render傳進去的都是新的Function
<Button2 onClick={e => console.log(e)} />

這樣的inline寫法,雖然會造成每次一有變動,每個Children Component都會去檢查一次,很多東西要檢查「理論上」耗費的時間也會變多,但我們真的就該改掉這樣的寫法嗎?這篇文章給了我們些值得探討的問題,程式變慢真的是這些non-pure造成的嗎?

實驗

我在Github上開了一個用來實作PureComponent的專案,有興趣的人也可以直接到這打開log看看
其中的一個結論是

當傳給Children Component的props「是」pure,更新children會觸發children的ComponentDidUpdate & render,但畫面上不會有任何改變,真的被skip掉了;而Parent Component的更新,並不會觸發children的ComponentDidUpdate & render

結語

「不要過早開始優化」,這是文章中作者很強調的一點,當你要做優化時,要好好做實測;是inline-style造成的嗎?還是有更大的問題在Code裡?
優化沒辦法一次到位,從使用的過程中逐漸察覺出可加強的部分,首要關注的還是專案本身的產出
官方也有建議到,因PureComponent做的是shallow compare,所以較複雜的data structures可以用immutable objects來加快比對速度(ex. Immutable.js)

Reference

React, Inline Functions, and Performance
React PureComponent 源码解析
我的實驗

webpack upgrade to 16

jackypan1989 發表於 2017-09-29

Facebook just announced the release of React v16.0.

New features

  1. fragments
  2. error boundaries
  3. portals
  4. custom DOM attributes
  5. improved server-side rendering
  6. small lib size
  7. better render perf (with react-fiber)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    https://github.com/facebook/react/issues/10686
    v15
    render functional component tree x 115,932 ops/sec ±4.10% (55 runs sampled)
    render class based component tree x 255,407 ops/sec ±4.46% (58 runs sampled)
    render class that renders functional components x 252,045 ops/sec ±5.47% (56 runs sampled)
    Fastest is render class based component tree,render class that renders functional components
    v16
    render functional component tree x 204,931 ops/sec ±2.21% (59 runs sampled)
    render class based component tree x 339,215 ops/sec ±2.98% (58 runs sampled)
    render class that renders functional components x 326,880 ops/sec ±4.51% (56 runs sampled)
    Fastest is render class based component tree,render class that renders functional components

Install

Just update your package.json
No other configurations, and no migration issues :D
(It’s true !!!!!)

1
yarn add react react-dom

Pratical performance (in chrome devtool)

v16
img

v15
img

You maybe also need to update (optional)

1
yarn add prop-types recompose redux

Reference

  1. React v16.0

Webpack production bundle 優化 (72x)

jackypan1989 發表於 2017-09-28

本篇將會介紹 webpack 3 中常見的優化方式
包含問題分析,plugin介紹等等

優化成效

  1. 大部分的情境下,使用者用量(專案為內部使用者為主)
    從 3.6 MB 降到 51 KB (app)
    降為原本的 1.38 % (縮減 98.62 %)

  2. 在完全沒有 cache 機制,必須重拉 vendor 情況下
    也是從 3.6 MB 降到 726 KB (app + vendor)
    降為原本的 19.6 % (縮減 80.4 %)

優化流程

1. 觀察現有狀況與問題

僅使用 DefinePlugin(with production), UglifyJsPlugin(with minimize)
bundle 出來的大小為 3.6MB, 存在很大改善空間(3G網路讀取時間過長)
因為不是所有 module 都在每次 build 都改動到

imgur

2. 利用 BundleAnalyzerPlugin 分析 bundle 成分

BundleAnalyzerPlugin 會協助解析 bundle 中有哪些單元
他會生成一個網頁,透過該網頁可以看到用到的 module 以及它的大小

3. 利用 CommonsChunkPlugin 抽出 lib

透過分析發現,第三方 lib 佔了大部分的成分(例如 react, rxjs, redux, moment, antd …etc)
但這些 lib 的改動(例如升級 react 版本),在 production build cycle 中次數是很少的
因此透過 CommonsChunkPlugin 把所有第三方都抽出來,與我們自己的 code 分開
(例如產生出 app.js, vendor.js 這樣一來就每次只更新 app.js 即可)

ps: 也可以在 entry 中,指定那些比較大的 lib

4. Uglyify 優化

除了調整 minify, souceMap 外,compress選項也可以另外調整
(例如不要在生成支援 ie8 的 code, 去掉 dead code, comment等)

5. IgnorePlugin

如果有使用 momentJS 可以發現,他會 bundle 所有的語言包
這時就可以利用 IgnorePlugin 去掉沒用到的語言包

6. ModuleConcatenationPlugin & HashedModuleIdsPlugin

  1. ModuleConcatenationPlugin
    參考 RollupJS 將有相關的 module 放在同一個閉包裡面,所以會減少閉包數量

  2. HashedModuleIdsPlugin
    給打包的 module 一個穩定 hash 值 (如果沒啟用,抽出第三方 lib 就會大打折扣)

7. Gzip (非常有效降低大小)

利用 CompressionPlugin 先 prebuild 好 gzip 檔案
而 gzip 需要 server side 支持
下面列幾種方法

  1. express 的 middleware 插件
  2. nginx 設定 static gzip file 支持
  3. server on-the-fly 支持(不建議,因為會吃系統效能)

8. 利用 chunkhash 與 htmlWebpackPlugin 來達到 cache

修改 output 裡面的 chunkhash 來達到確認是否有更新
因為只要有添加新的模組或修改 chunkhash 就會改變
(app.38d8273182132939.js -> app.38d8d73182139823.js)

但是問題在於原本 express 的 view template 中是固定的
因此透過 htmlWebpackPlugin 把新的得到的 chunkhash inject 回去
這樣就能完美使用瀏覽器的 cache 機制

真實案例

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 優化前
plugins: [
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin()
]
// 優化後
plugins: [
// new BundleAnalyzerPlugin(),
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
}),
new CleanWebpackPlugin(path.join(__dirname, 'dist')),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.[chunkhash].js',
minChunks: module => module.context && module.context.indexOf('node_modules') >= 0
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true
},
output: {
comments: false
},
sourceMap: true
}),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.HashedModuleIdsPlugin(),
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js|html)$/,
threshold: 10240,
minRatio: 0.8
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src', 'server', 'view', 'template.html'),
inject: true,
filename: path.join(__dirname, 'src', 'server', 'view', 'index-prod.html')
})
]

Reference

  1. webpack 官方

Javascript ES6 Generator 介紹

Tu-Szu-Chi 發表於 2017-09-27

在ES6中,Promise將我們從「Callback Hell」稍微解放出來

1
2
3
4
5
6
7
asyncFun1(() => {
// do something...
return result
})
.then(asyncFun2)
.then(asyncFun3)
// ...more and more asyncFun

但Promise也有缺點,例如當asyncFun3需要調用asyncFun1的result時,該怎做呢?可能會用巢狀式Promise,或者用變數暫存每一次的result之類,我們都知道這些不是個好方法

Generator

generator可讓我們的異步流程寫得更synchronous-style,免除了之前的困擾
先用個範例將Promise小改成Generator

1
2
3
4
5
6
7
8
9
10
function* logGen(name) {
const url = 'https://api.github.com/users/' + 'name'
const result1 = yield fetch(url)
}
const gen = logGen('sov')
const result = gen.next()
result.value
.then(data => data.json())
.then(data => gen.next(data))

Ln:7的result會收到Ln:3 yield回傳的Promise
待API回傳後,Ln:10 gen.next(data) 會將API回傳的data賦值給result1

但我們不可能一直.then下去,這樣又回到地獄了,所以我們需要一個Function幫我們管理這整個流程,可以自動接著下去做事的傢伙
Co - 知名的TJ大神在幾年前就實現了Generator概念,有興趣的可以參考他的實踐方式,在此我們先寫一個簡易版的管理器用用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const co = (gen, parameter) => {
const g = gen(parameter)
next = (data, err) => {
let res
if (err) {
return g.throw(err)
} else {
res = g.next(data)
}
if (!res.done) {
res.value
.then(data => data.json())
.then(data => { next(data) })
} else {
console.log('DONE! ',res.value)
}
}
next()
}

這簡易版的可以協助我們連續的Promise請求,可在這JS Bin Run看看,在此也附上我設定的「任務」Code

1
2
3
4
5
6
7
8
9
10
11
function* logGen(name) {
const url = 'https://api.github.com/users/'
const result1 = yield fetch(url + name)
const result2 = yield fetch(url + 'abc')
const result3 = yield fetch(url + 'def')
return result1.id + '~' + result3.id
}
co(logGen, 'sov')

但這樣的流程管理器只能處理都是yield Promise的需求,固我們可以加個工具幫我們判斷是否為Promise

1
const isPromise = obj => Boolean(obj) && (typeof obj.then === 'function')

將isPromise加入到我們的流程管理器 - JS Bin

1
2
3
4
5
6
7
8
9
if(!res.done) {
isPromise(res.value)
? res.value
.then(data => data.json())
.then(data => { next(data) })
: next('It is not Promise')
} else {
console.log('DONE! ',res.value)
}

對應任務的不同再將流程管理器改造一下就可以用了

進階 (Observable-style)

接下來這章節主要是想將Generator的Iterable-style寫成Observable-style的方式

GTOR

這表格來自於Kris Kowal’s - GTOR: A General Theory of Reactivity,比較不好理解的部分是Iterable/Observable的差別

Iterable是pull value,Observable是push value

Interable

Generator通過next()來pull value

1
2
3
4
5
6
7
8
9
10
11
12
const gen = function* () {
yield 1;
yield 2;
yield 3;
}
const iterator = gen();
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: false }
iterator.next(); // { value: undefined, done: true }

Observable

而所謂的Observable通常是指Value「可使用」後,會有通知/訂閱機制(subscription/notification)將Value送出去
在此我們要將流程管理器抽象化成Observable模式

1
2
3
4
5
const co = (gen) => (...args) => ({
subscribe: (onNext, onError, onCompleted) => {
next(gen(...args), {onNext, onError, onCompleted})
}
})

co管理器多了.subscribe(),就跟Rx’Library中的Observable一樣,並且接收三個Function作為參數
原本的迭代器next()也要改動一下

因為是簡易版,所以沒有加上errorHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const next = (iter, callbacks, prevData = undefined) => {
const { onNext, onCompleted } = callbacks
const res = iter.next(prevData)
const value = res.value
if(!res.done) {
if(isPromise(value)) {
value
.then(data => data.json())
.then(data => {
onNext(data)
next(iter, callbacks, data)
})
} else {
onNext(value)
next(iter, callbacks, value)
}
} else {
return onCompleted()
}
}

這樣當每次Value「可使用」後,我們會直接push到onNext()去,這邊任務的範例是會接收一個Array of name,去call API得到這個name的Github user

1
2
3
4
5
6
7
function* logGen(...names) {
const url = 'https://api.github.com/users/'
for(let i = 0; i < names.length; i++) {
yield fetch(url + names[i])
}
}

再來是產生一個偽-Observable對象

1
2
3
4
5
6
7
8
const asyncFun = co(logGen)
asyncFun('sov','abc', 'ww', 'tt')
.subscribe(
(val) => console.log('Success: ', val.login + '-' + val.id),
(err) => console.log('Error: ', err),
() => console.log('Completed!')
)

這樣就完成了(完整的JS Bin)!而這樣做的好處就是…

你會發現RxJS原來這麼好用

結語

其實co()在這就是個「迭代器(Iterator)」角色,讓我們不用一直像以下這樣做

1
2
3
4
generatorFun.next()
generatorFun.next()
generatorFun.next()
// ... more and more next()

也可以直接看Co.js的原始碼,並不複雜

看到這裡,希望你對Generator有更深的認識,或許也會對ES7 async/await 和 RxJS存在的用意更有感觸~

Reference

The Hidden Power of ES6 Generators
Teaching RxJS
初探ES6 Generators

1234
SOVMedCare

SOVMedCare

Full Stack JS #FRP #RxJS #Redux #React

31 文章
19 標籤
GitHub E-Mail FB Page Instagram
© 2017 — 2020 SOVMedCare
由 Hexo 強力驅動
|
主題 — NexT.Gemini v5.1.2
0%