SOVMedCare Tech

@sovmedcare


  • 首頁

  • 關於

  • 作者群

  • 標籤

  • 分類

  • 歸檔

用 CSS 完成常見網頁版型

dannnyliang 發表於 2020-08-25

想要在網頁上達成特定的視覺效果,做法很多
而且建議同樣的結果,可以多學幾種做法,當情境限縮選擇時,至少還有其他路可以選擇
但,也不是所有做法都值得學習

做法仍有好有壞


什麼是較好的做法?

  • 不用寫太多
    屬性精簡:margin-top, margin-right, margin-bottom, margin-left 可被精簡成 margin
  • 易讀
    相關的樣式集中、不分散在其他元素
  • 具 RWD 性質
    在裝置寬度改變時,仍保持原比例
  • 具擴展性
    元素被重複使用,或是有其他元素加入時,原本的樣式不需要改動
  • 樣式不相依
    元素本身要達成的效果,不需要其他元素配合,也不影響其他元素

本篇主要會針對樣式的「擴展性」、「相依性」進行討論,找出即使介面設計修改,仍能穩定的樣式寫法


遇到的問題

layout-wireframe

需求:

1
2
- Header1, Header2 固定出現在上方
- Header1 向下捲動時收合,向上捲動時展開

實作:

  1. 建立一個 state 紀錄 Header 高度
  2. 頁面渲染、滾動事件觸發時,更新此 state
  3. 將 state 傳到 Header 元件、Content 元件

這樣的實作會出現些問題:

  • 需 state, props,且若無 styled-component 配合會更難達成
  • 需多個元件配合才能達到效果
  • 更新邏輯複雜,容易有 bug

練習 I - Fixed Header

先不用想得那麼複雜,從一個簡單的範例開始 (CodeSandbox)

需求:

  • (Required) Header 固定在畫面上方不動
  • (Required) 其他的內容可上下捲動
  • (Optional) 擴展性:多複製一個 <Header /> 時,仍符合需求
  • (Optional) 低相依:Content 元件的樣式,不需要使用 menuHeight


做法1 - Pass Height

(CodeSandbox)

  • 複製 <Header /> 時,會跟原本的 Header 重疊,沒有擴展性
  • Content 的樣式需要知道 Header 的高度,相依度高
  • Header 的背景有透明度的話會發現,Content 捲動時會跑到 Header 的下面
    如果這是其中一項視覺需求,雖然沒達成 Optional 項目,但仍可考慮採用此作法


做法2 - Seperate Area

(CodeSandbox)

  • 在 Content 外多包一層 Wrapper,用來定義 Content 存在的空間,如此一來 Content 就不需要知道 Header 的高度,可以達到低相依
  • 但複製 <Header /> 時,仍需調整 Wrapper 的實作,擴展性仍不佳


做法3 - Grid

  • grid 的強項除了二維排列以外,也能輕鬆規劃版型
  • 作法2 實際上就是先規劃區塊,再將元素放入個區塊中,因此用 grid 可以輕鬆重現
  • 但是缺點和作法2 相同


做法4 - Flex

(CodeSandbox)

  • 一樣是從「先規劃區塊」的想法出發,flex 本身的特性會用子元素填滿父元素,因此只需要定義 Header 高度後,Content 就會自動撐滿空間
  • 複製 <Header /> 時,Content 仍會自動撐滿空間,滿足擴充性
  • 不只是 Content,連父元素都不需要知道 Header 的高度,滿足低相依性
  • 四種做法中最好的一個( flex 會成為主流不是沒有道理的👍


練習 II - 多區塊滾動

除了 Fixed Header 以外,另一個要會的版型是「多區塊滾動」
上下: 在前面的練習的 Header 中加入 overflow: scroll 就可以直接達成,上下區塊各自滾動的效果
左右: 也就是常見的 SideMenu,從這個 範例 開始試著實作吧!

需求:

  • (Required) Menu 和 Content 元素左右排列
  • (Required) Menu 和 Content 元素可以各自滾動


作法 1 - fixed width

  • 直接訂死寬度 ${MenuWidth}px, calc(100% - ${MenuWidth}px)
  • 最直觀的做法,但擴展性不足,且相依度高


作法 2 - padding width

  • Menu 元素 ${MenuWidth}px + position: fixed、Content 元素 paddding-left: ${MenuWidth}px
  • 和做法1 大同小異


作法 3 - inline block

  • 要把元素排入同一列中,一定會想到 inline-block,但筆者嘗試了許久,頂多做到「左右同步滾動」,沒辦法「分區塊滾動」
  • 猜測無法達成「分區塊滾動」的效果,因為 inline-block 是定義在父元素,讓子元素可以排在同一列。實際上是對父元素滾動,不是對子元素觸發滾動,因此無法分區


作法 4 - grid

(CodeSandbox)

  • 一樣從規劃版型的觀念出發,先畫好 Menu 區域與 Content 區域
  • 低相依,但擴展性低


作法 5 - flex

(CodeSandbox)

  • 兼具低相依、高擴展


結語

上面兩個練習都列出了多種做法,不是說一定要滿足「高擴展」、「低相依」才是唯一正解,每種做法的結果都有些微差異,要找出最符合自己開發情境及需求的做法。flex 看似強大,但如果遇到無法使用 flex 的狀況,這時其他做法就派上用場了。
如果還有其他更好、更有創意的作法,歡迎告訴我們喔 🎉

state_monad

tancc 發表於 2018-04-02

許多的程式應用情境,是要依賴隨著時間或是運算而不停變換的狀態而進行。Haskell 中沒有變數,數值都是不可變的。

Haskell 要達到這種需求,同樣也是靠函數。藉由傳入 state 作為輸入參數,經過運算後回傳最後的結果以及更新後的 mewState。

原本的 state 並沒有被修改到,而我們也同樣達到了根據 state 做計算並且變換 state 的目的了

而在 Haskell 中,這種需要處理狀態性問題時,常用到的工具就是 State monad。


範例

一般會用躑骰子當範例

1
2
3
4
5
6
7
8
9
10
11
12
import System.Random
rollDie5Times :: StdGen -> (Int, Int, Int, Int, Int)
rollDie5Times g =
let
(one, g1) = randomR (1, 6) g
(two, g2) = randomR (1, 6) g1
(three, g3) = randomR (1, 6) g2
(four, g4) = randomR (1, 6) g3
(five, _) = randomR (1, 6) g4
in
(one, two, three, four, five)

目前這種模擬躑五次骰子的方法看起來很不舒服,原因在於 generator 他必須手動取出和傳入,多了很多重複的步驟。

如果使用了 State monad,我們就可以將 generator 作為 state 來改善目前的範例。


定義 State monad

1
newtype State s a = State {runState :: s -> (a, s)}

State 包裹著一個函數型別為s -> (a, s)。其意義就是如一開始所說的,傳入 state,經過運算回傳結果以及新的 state。

State monad 的Functor Applicative Monad instance 我們可以如此定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
instance Functor (State s) where
fmap f (State g) = State $ \s -> let (a, s1) = g s
in (f a, s1)
instance Applicative (State s) where
pure a = State $ \s -> (a, s)
(State fab) <*> (State fa) = State $ \s -> let (a, s1) = fa s
(ab, s2) = fab s1
in (ab a, s2)
instance Monad (State s) where
return = pure
State f >>= k = State $ \s -> let (a, s1) = f s
in runState (k a) s1

從實作中可以看到 state 會不斷的更新然後傳遞下去。


Methods

一般的 State monad 還會提供幾個方便的 method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 將函數包進 State 中,變為 State monad
state :: (s -> (a, s)) -> State s a
state = State
-- 取的當前的 state
get :: State s s
get = State $ \s -> (s, s)
-- 更新 state
put :: s -> State s ()
put x = State $ const ((), x)
-- 傳入 state 執行 State monad 並回傳最後的 state
execState :: State s a -> s -> s
execState (State sa) s = snd $ sa s
-- 傳入 state 執行 State monad 並回傳最後的運算結果 a
evalState :: State s a -> s -> a
evalState (State sa) s = fst $ sa s
-- 對 state 做轉換
modify :: (s -> s) -> State s ()
modify f = State $ \s -> ((), f s)

範例改寫

1
2
3
4
5
6
7
8
rollDie :: State StdGen Int
rollDie = state $ randomR (1, 6)
rollDieNTimes :: Int -> State StdGen [Int]
rollDieNTimes n = replicateM n rollDie
> evalState (rollDieNTimes 5) (mkStdGen 0)
[5, 1, 4, 6, 6]

如此一來可以定義“躑 n 次骰子”的函數,而且看起來還比原先範例更為精簡。


結語

如同 Reader monad,一般在使用時,同樣會直接使用現成的 library 像 mtl或transformers。

可以去看看這些 library 是如何實作的,藉此來學習。

此外通常看到或是使用時,因為會需搭配其他的 Monad 一起使用,並不會用State而是使用StateT。而去看原始碼可以發現 State s a 其實也就是StateT s Identity a的type alias。


參考資料

  1. Haskell/Understanding monads/State - Wikibooks, open books for an open world
  2. Learn You a Haskell for Great Good!

Reader monad

tancc 發表於 2018-03-30

程式中時常有許多函數是需要去使用共用的常數(shared environment),常見的像是設定檔(config)之類的東西。

此時函數的參數就是這些使用到的如 config,我們必須在 type signature 寫出需要的參數型別,此外在使用時必須顯式的寫出要傳入的東西

1
2
3
4
5
6
7
8
9
-- 假設此時的 configs 中有 name age weight height 等等
myFunc1 :: Name -> Age -> ...
myFunc2 :: Name ->...
myFunc3 :: Weight -> Height -> ...
-- ...
myFunc1 name age
myFunc2 name
myFunc3 weight height
-- ...

這樣的壞處很明顯就是我們必須重複寫很多東西。
要解決這問題的一個直覺想法就是,讓這些函數所在的 scope,可以自由地取得共用的資料,如此一來就不需由我們手動傳入了。
而在 functional programming 中,就是使用函數去達成。只要將這些函數做良好的定義,並塞到一個更大的函數裡,將共享資源傳入這個大函數中,裡頭的小函數們就都可以去取得了。

Reader monad 就是可以幫我們做這件事情的工具,因此他又有另外的名稱,Environment monad。
而最簡單的 Reader monad 其實就是一個函數的新名稱罷了。


範例

這裡用一段沒什麼用處的程式碼來舉例。
這裡的函數用返回執行別用Maybe,只是為了要用do notation而已

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
type Name = String
type Age = Int
type Height = Double
type Weight = Double
type Result = (String, String, String, Double)
revName :: Name -> Maybe String
revName n = Just $ "Reverse Name: " ++ reverse n
nameAndAge :: Name -> Age -> Maybe String
nameAndAge n a = Just $ "Name & Age: " ++ n ++ " " ++ show a
weightAndHeight :: Weight -> Height -> Maybe String
weightAndHeight w h = Just $ "Weight & Height: " ++ show w ++ " " ++ show h
calBmi :: Height -> Weight -> Maybe Double
calBmi h w = Just $ w / (h / 100)^2
go =
let
name = "Tom"
age = 20
height = 180
weight = 100
in
do
na <- nameAndAge name age
n <- revName name
wh <- weightAndHeight weight height
b <- calBmi height weight
return (na, n, wh, b)

可以發現 nameAndAge, revName, weightAndHeight, calBmi等函數,我們需要定義、傳地重複的東西進去,使程式碼看起來較為繁冗。
而他們用到的這些 input 就可以將其想像成 shared environment 這種在程式執行時並不會去改變且很多地方都會用到的東西。


定義 Reader monad

1
newtype Reader r a = Reader { runReader :: r -> a }

也就是說,Reader 就是包裹了一個函數 r -> a ,其中 r 就是我們要傳入的共用資料,有些地方也會用e來表示(environment);a 表示吃進 r 後,會回傳的東西是什麼。

再來我們可以自己定義他一系列的Functor Applicative Monad instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
instance Functor (Reader r) where
fmap :: (a -> b) -> Reader r a -> Reader r b
fmap f (Reader ra) = Reader $ f . ra
instance Applicative (Reader r) where
pure :: a -> Reader r a
pure a = Reader $ const a
(<*>) :: Reader r (a -> b) -> Reader r a -> Reader r b
(Reader rab) <*> (Reader ra) =
Reader $ \r -> rab r (ra r)
instance Monad (Reader r) where
return = pure
(>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
(Reader ra) >>= aRb =
Reader $ \r -> runReader (aRb (ra r)) r

可以看到這裡的 structure 是 Reader r 也就是 (->) r (function type) 這部分。而且在實作當中可以看到,r是保持著原來的值被傳遞,並沒有被做其他transform,因此可以確保 Reader monad 可以拿到相同的r

對照 (->) r 在原始碼中定義的Functor Applicative Monad instance,可以發現基本上是一樣的,只是多了 Reader 這一層包裝。

若將型別定義中的 Reader r 用 (->) r 替換,可以看到更明顯的結果

1
2
3
4
5
6
7
8
9
10
11
fmap :: (a -> b) -> Reader r a -> Reader r b
= (a -> b) -> (->) r a -> (->) r b
= (a -> b) -> (r -> a) -> (r -> b)
(<*>) :: Reader r (a -> b) -> Reader r a -> Reader r b
= (->) r (a -> b) -> (->) r a -> (->) r b
= (r -> a -> b) -> (r -> a) -> (r -> b)
(>>=) :: Reader r a -> (a -> Reader r b) -> Reader r b
= (->) r a -> (a -> ((->) r b)) -> (->) r b
= (r -> a) -> (a -> (r -> b) -> (r -> b)

所以其實 Reader monad 的概念,就是跟 function type (->) r 是一樣的。


Methods

使用了newtype Reader將 function 包裝後,還沒有什麼用。
還需要一些小工具使我們可以方便地拿到想要的資料,ask與asks以及local

1
2
3
4
5
6
7
-- 取得 shared environment
ask :: Reader a a
ask = Reader id
-- = Reader $ \r -> r
asks :: (r -> a) -> Reader r a
asks = Reader

ask就是一個id,也就是傳什麼就吐一樣的東西回來。所以就是可以拿到Reader r a中的r。

1
2
3
4
ex :: Reader Config String
ex = do
config <- ask
-- ...

asks的參數是一個函數,這個函數的作用就類似 selector,用來塞選取得在 shared environment 中想要的資料。

1
2
3
4
ex :: Reader Config String
ex = do
n <- asks $ lookup "name"
-- ...

local是用來修改 Reader content,但他不是修改全域的內容,而是只有在local scope 裡面的 Reader Monad 才會被影響。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local :: (r -> r) -> Reader r a -> Reader r a
local f (Reader ra) = Reader $ \r -> ra (f r)
-- example
ex :: Reader Int (Int, Int)
ex = do
i <- local (+1) $ do
i <- ask
return i
j <- ask
return (i, j)
> runReader ex 10
(11, 10)

從ex的執行範例可以看到,i也就是經過local後 Reader content 的確是被修改了,但是用ask拿到的 Reader content j,還是為 10 沒有改變。
所以local的作用是區域修改而不會影響到其 scope 外的地方,所以總的來說其實 Reader content並沒有發生改變。


範例改寫

原本的範例程式可以用 Reader monad 如此改寫

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
type Result = (String, String, String, Double)
data Config = Config { name :: String
, age :: Int
, height :: Double
, weight :: Double
}
revName :: Reader Config String
revName = do
n <- asks name
return $ "Reverse Name: " ++ reverse n
nameAndAge :: Reader Config String
nameAndAge = do
n <- asks name
a <- asks age
return $ "Name & Age: " ++ n ++ " " ++ show a
weightAndHeight :: Reader Config String
weightAndHeight = do
w <- asks weight
h <- asks height
return $ "Weight & Height: " ++ show w ++ " " ++ show h
calBmi :: Reader Config Double
calBmi = do
w <- asks weight
h <- asks height
return $ w / (h / 100)^2
go :: Reader Config Result
go = do
na <- nameAndAge
n <- revName
wh <- weightAndHeight
b <- calBmi
return (na, n, wh, b)
showResult = runReader go $ Config {name = "WOW", age = 20, height = 190, weight = 100}

主要的差別在於,每個函數的 type signature 是一致的 Reader Config [回傳型別],大家是共享一份 config。
各函數在其內部自行定義要取得什麼資料,如此一來在使用的時候就不需要顯示的傳遞進去了。


結語

一般情況在使用 Reader Monad 時並不會自行去定義,可能會用一些 library 像 mtl或transformers,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Control.Monad.Reader
data MyContext = MyContext
{ foo :: String
, bar :: Int
} deriving (Show)
computation :: Reader MyContext (Maybe String)
computation = do
n <- asks bar
x <- asks foo
if n > 0
then return (Just x)
else return Nothing
ex1 :: Maybe String
ex1 = runReader computation $ MyContext "hello" 1
ex2 :: Maybe String
ex2 = runReader computation $ MyContext "haskell" 0

他們同樣也都提供了基本的像是ask, asks, local,可以去看看這些 library 是如何實作的,藉此來學習。

此外通常看到或是使用時,因為會需搭配其他的 Monad 一起使用,並不會用Reader而是使用ReaderT,T表示 transformer。


參考資料及更多資源

  1. mtl
  2. transformers
  3. Three Useful Monads - adit.io
  4. What I Wish I Knew When Learning Haskell 2.3 ( Stephen Diehl )

Haskell 系列 - Functor (函子)

jackypan1989 發表於 2018-03-29

map 函數

如果之前是寫 javascript 的,那你一定會用過 map 這個 function
舉個例子

1
2
3
4
5
6
7
8
9
10
const add1 = x => x + 1
add1(5) // 答案是6
add1([5]) // 有error 因為add1 無法應用在array上
[5].map(add1) // 答案是[6]
// 更甚者, 採用ramda.js之類的lib
const mappedAdd1 = R.map(add1)
mappedAdd1([5]) // 答案一樣是[6], 但add1已經轉成了mappedAdd1

可以發現這map函數可以讓我們把原本一個只能處理int的函數
變成一個可以處理[int],也就是說其實他可以穿越[]這個所代表的意義
將這個context裡面的值取出,套用add1,再把他放回context

Functor (函子)

如同剛剛所說 functor 其實就是把許多type中可以map這件事給抽象出來
看看 haskell 中的實作

1
2
3
4
5
6
> :i Functor
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
{-# MINIMAL fmap #-}
-- Defined in ‘GHC.Base’

這 fmap 就是我們常看到的 map 函數

1
(a -> b) -> f a -> f b

看看他的意思
傳進一個 function 跟一個帶有context的值再回傳另一個帶有context值
對照一開始的範例,傳進一個 x=x+1, [5], 再回傳另一個 [6]
所以有些人會把 functor 比喻成盒子, 不能說錯
但他其實就是很簡單的把 function 提升(lift)成 可以處理context的 function而已
是 function 的處理能力提升而不是真正有一個容器或是box在 (可能是因為list太適合用box解釋)

在數學上就是,兩個Hask範疇之間的轉換
而且也讓原本範疇(Hask範疇)中的物件(type)與態射(function)都提升成帶有上下文或處理上下文

List Functor

這我們剛剛舉例過了,直接看看實作

1
2
3
instance Functor [] where
{-# INLINE fmap #-}
fmap = map

fmap 果然就是 map 函數

Maybe Functor

再來看看 Maybe
maybe a 包含了 just a 跟 nothing
其中 nothing 代表可能為空值這個 context

用想像力想一下
如果是 just 6 那我們就把 6 從 just 這個 context 取出來,做完運算再用just包回去
如果是 nothing 那就根本不用管直接 nothing 因為 nothing 怎麼操作都是 nothing

回去看看原始碼

1
2
3
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)

果然我們想的一樣
以maybe來說,我們也可以直接針對他進行fmap操作
fmap我們又把他用 <$> 來代表

1
2
3
4
5
6
7
8
9
10
>:{
add1 :: Int -> Int
add1 x = x + 1
:}
> add1 <$> [1, 2]
[2,3]
> add <$> Just 3
Just 4

這就對了,原本只能處理Int的function
變成可以處理[Int], 連Maybe Int也可以了
所以 [], Maybe 都是 functor

參考資料

  1. haskell wiki
  2. learn you a haskell
  3. hoogle

Haskell 系列 - Monoid (ㄠ半群)

jackypan1989 發表於 2018-03-28

Monoid (ㄠ半群)

Haskell 是跟純數學息息相關的程式語言,有許多抽象化的方式都來自數學理論(例如範疇論跟群論)
這裡的 typeclass Monoid 正是代表某些 data 的特性,首先看看在 haskell 中的 Monoid

1
2
3
4
5
6
> :i Monoid
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
{-# MINIMAL mempty, mappend #-}

我們可以發現 Monoid 必須至少實作兩個方法,一個為 mempty,另一個是 mappend
在數學理論中,定義如下,滿足以下兩個特徵的稱為 monoid

  1. 有單位元
  2. 有一個二元operator滿足結合律

1的意思是裡面有個元素,其他元素跟他結合都還是自己
2的意思是 a op (b op c) = (a op b) op c,表示括號不影響結果

用你的想像力,String是不是就是一個最明顯的例子,單位元是空字串(“”),其他字串怎麼跟他前後結合都不會影響結果,那二元operator自然而然就是字串串接了(++),下面用String舉例

1
2
3
4
5
> "" ++ "ABC" ++ ""
"ABC"
> ("HELLO" ++ "ABC") ++ "!" == "HELLO" ++ ("ABC" ++ "!")
True

所以String還有haskell裡面一些代表或是處理字串的type基本上一定都是Monoid,當然更廣義的來看[]都是monoid,有空的array跟array的串接的話就是monoid

list monoid ([])

看一下 [] 的實作

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
class Semigroup a => Monoid a where
-- | Identity of 'mappend'
mempty :: a
-- | An associative operation
--
-- __NOTE__: This method is redundant and has the default
-- implementation @'mappend' = '(<>)'@ since /base-4.11.0.0/.
mappend :: a -> a -> a
mappend = (<>)
{-# INLINE mappend #-}
-- | Fold a list using the monoid.
--
-- For most types, the default definition for 'mconcat' will be
-- used, but the function is included in the class definition so
-- that an optimized version can be provided for specific types.
mconcat :: [a] -> a
mconcat = foldr mappend mempty
instance Semigroup [a] where
(<>) = (++)
{-# INLINE (<>) #-}
stimes = stimesList
instance Monoid [a] where
{-# INLINE mempty #-}
mempty = []
{-# INLINE mconcat #-}
mconcat xss = [x | xs <- xss, x <- xs]

可以看到 Monoid 因為有 superclass Semigroup 定義了 mappend = (<>)
所以他裡面就不用重複實作,直接用 Semigroup 的 mappend
因此可以發現 mempty = [] (單位元), mappend = (++) = (<>)

用monoid方式的抽象來操作看看

1
2
3
> import Data.Monoid
> "" <> "ABC"
"ABC"

Sum monoid 跟 Product monoid

我第一次看到 monoid 定義的時候
我就想到了,數字也是這樣
例如

  1. 加法的時候
    單位元是 0
    二元的operator 是 (+)

    任何數加上0都沒差,而且括號不影響 (1+2)+3 == 1+(2+3)

  2. 乘法的時候
    單位元是 1
    二元的operator 是 (*)

    任何數乘上1都沒差,而且括號不影響 (1*2)*3 == 1*(2*3)

不過因為單位元不同,而且 instance 只能有一種實作
所以 Haskell 創造了 Sum 跟 Product 這兩個 newtype 來代表我剛剛說的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- Sum Monoid
newtype Sum a = Sum { getSum :: a }
instance Num a => Semigroup (Sum a) where
(<>) = coerce ((+) :: a -> a -> a)
stimes n (Sum a) = Sum (fromIntegral n * a)
instance Num a => Monoid (Sum a) where
mempty = Sum 0
-- Product Monoid
newtype Product a = Product { getProduct :: a }
instance Num a => Semigroup (Product a) where
(<>) = coerce ((*) :: a -> a -> a)
stimes n (Product a) = Product (a ^ n)
instance Num a => Monoid (Product a) where
mempty = Product 1

我一開始會覺得說為何會需要這些東西
但其實 Haskell 就是不斷利用數學一些抽象的技巧來層層疊疊
來幫助大家去優化最佳化,而不是每個data都有自己獨特實際實作很長的api

例如 String, [], Sum, Product 的這種monoid特性
不就隱含了一件常見的事情,這些事情是可以分開去做的,而且不會影響結果
所以我們也可以作一個分散式的function
例如 getData(xxx) <> getData(yyy) <> getData(zzz)
可以讓這三個function由不同cpu去跑,反正執行順序根本沒差
而這個 typeclass 正是把這種特性給抽象出來

參考資料

  1. haskell wiki
  2. learn you a haskell
  3. 知乎:為何需要monoid

Monad

tancc 發表於 2018-03-28

Monad對工程師而言,如果沒牽扯到數學相關的範疇其實不是那麼可怕難懂的東西。

1
fmap :: (a -> b) -> f a -> f b

Functor實作了fmap,使得可將普通函數(a -> b)lift到f a -> f b;

1
(<*>) :: f (a -> b) -> f a -> f b

Applicative實作了<*>,讓我們可以將保裹在某 structure 中的函數f (a -> b),apply 到同樣含有相同結構的數值中f a -> f b

他們做的事情其實都是 function application,只是應用的情境不同而已。而從工程面來看,Monad 其實也是一樣的,也為我們做了某情境下的函數調用的抽象。


Monad TypeClass

Monad typeClass 的部分定義如下

1
2
3
4
5
6
class Applicative m => Monad m where
{-# MINIMAL (>>=) #-}
return :: a -> m a
return = pure
(>>=) :: m a -> (a -> m b) -> m b
join :: m m a -> m a

其中可以看到 Monad 必須是 Applicative,所以return的預設定義就是pure,將一個數值包進 monadic structure m。

>>=(bind),是使用monad進行操作時十分常看到的東西。
從他的 type signature 不難發現,他要做的事也是 function application,只是這次要應用的函數是(a -> m b),而這函數會所 return 的值是將 input 包上一個額外的結構m,而最終>>=所返回值m b會跟輸入m a是具有相同結構的。


Monad laws

如同Applicative Functor,就算一個型別實作了>>=成為了 Monad 的instance了,也還不能夠說是一個Monad。

必須還要在滿足三個 Monad laws

1
2
3
4
5
6
-- 1. right identity
m >>= return = m
-- 2. left identity
return x >>= f = f x
-- 3. associativity
(m >>= f) >>= g = m >>= (\x -> f x >>= g)

這部分可以自行拿一些已知的Monad去驗證(List, Maybe, Either Monad),而這在自行定義 Monad 時也是要自己去確保滿足的,Haskell compiler 並不會跟你說符不符合。


Maybe Monad

當在做除法運算的時候,會不希望除數為 0,否則後續運算可能會出現非預期的結果,因此定義了一個safeDivide

1
2
3
safeDivide :: Double -> Double -> Maybe Double
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)

safeDivide會回傳Maybe Double,這樣子的話做其他的運算或者說在做函數組合時,其他函數 input 型別都也要改成Maybe型別,才可以拿其結果作為輸入來使用。
所以在其他函數內部,都需根據 Maybe 的兩種可能性Just a與Nothing做不同的運算。

1
2
3
4
5
maybeFunc1 ::Num a => Maybe a -> Maybe a
maybeFunc1 n =
case of n of
Nothing -> Nothing
Just a -> ...

可以想像,每個 maybeFunc 都要去做這些判斷是會非常麻煩,要寫很多重複的程式碼。

Maybe Monad 就可以幫我們將判斷這些case ... of的工作抽象出來

1
2
3
4
5
instance Monad Maybe where
return = pure
-- >>= :: Maybe a -> (a -> Maybe b) -> Maybe b
(Just x) >>= k = k x
Nothing >>= _ = Nothing

其>>=的定義,幫我們做掉判斷Nothing跟Just然後做不同任務的工作。
當是 Nothing 時,就不理會後續的函數k,直接返回 Nothing。
若是 Just 時,則將其中的值a,傳進後續要執行的函數k中。

也可以看到,以Maybe而言,使用>>=我們就可以不斷串接有這種a -> Maybe b型別的函式,因為(Just x) >>= k回傳的也還是Maybe型別的數值,可以繼續用>>=串接下一步的運算。

事實上就是,如果我們有一系列的函數a -> m b,我們可以藉由>>=將這些相依的運算做序列組合。

所以原本的函數組合就很容易可以這樣寫

1
2
3
4
5
safeDivide >>= maybeFunc1 >>= maybeFunc2...
-- (safeDivide 10 0) >>=...
-- = Nothing >>= ...
-- = Nothing

同樣類似的行為,如Either Monad 也能幫助我們省下判斷Left和Right的程式碼


List Monad

首先看一下如何定義 List Monad 內容

1
2
3
4
instance Monad [] where
return = pure
-- >>= :: [a] -> (a -> [b]) -> [b]
m >>= f = concat (map f m)

return和其在Applicative中定義的pure一樣,就是把值丟進一個 List 中。
>>= 的行為是將f map 到 List 中,然後concat最終結果。

1
2
3
4
5
6
7
8
9
10
a = [1,3,5,7]
f :: a -> [a]
f = \x -> [x, x]
a >>= f
= [1,3,5,7] >>= \x -> [x, x]
= concat (map (\x -> [x, x]) [1,3,5,7])
= concat [[1,1],[3,3],[5,5],[7,7]]
= [1,1,3,3,5,5,7,7]

其實上面 List Monad 的bind>>=所做的事情,可以用List comprehension達到。
而實際上,原始碼中,List Monad 的>>=就是用 List comprehensions 實作的。

1
2
3
4
5
6
instance Monad [] where
xs >>= f = [y | x <- xs, y <- f x]
a >>= f
= [y | x <- [1,3,5,7], y <- (\x -> [x, x]) x]
= [1,1,3,3,5,5,7,7]

所以從List Monad來看,因為兩者行為的等價,也比較能明白為何有人會說 Monad 做的事情,其實就是 flatmap 或 concatmap。


Do notation

如果只用>>=的話有可能會發生程式碼巢狀結構太深的問題

1
2
3
4
f >>= \a ->
(g a) >>= \b ->
(h b) >>= \c ->
return (a, b, c)

這時候可以使用do notation這個語法糖,來幫助用imperative programming的形式由上至下撰寫代碼,增加可讀性

1
2
3
4
5
do
a <- f
b <- g a
c <- h b
return (a, b, c)


Type signature

這段希望能從type signature來得到一些操作上的直覺

Function Application

從 TypeClass 知道,若一個東西是 Monad,那他必然是 Applicative 亦即也必然是 Functor。

所以Functor、Applicative、Monad在應用的行為上肯定有一定程度的一致性。

1
2
3
4
5
<$> :: (a -> b) -> f a -> f b
<*> :: f (a -> b) -> f a -> f b
-- 將 >>= 做flip
-- (>>=) :: m a -> (a -> m b) -> m b
flip . >>= :: (a -> m b) -> m a -> m b

三者從 type signature 來比較,更有一開始所說的,都是將 function application 做不同應用情境的抽象。

Dependent computation

同樣從>>=的型別去看

1
(>>=) :: m a -> (a -> m b) -> m b

>>=接受了m a,而第二個參數是個函數(a -> m b),其中的a從哪來,就是從m a的運算結果而來。

因此>>=所做的事情,就是將一個 monadic structure 所包裹的 computation m a,傳遞到串接的函數中 (a -> m b),讓此函數根據傳遞進來的參數去做某些運算,最後再返回m b。而此m b又可搭配其他 monadic function (a -> m b),繼續根據函數返回值操作下去。

而這種相依關係也是 Monad 較靈活的原因之一,因為它可以根據函數返回的值再去後續的運算。而這是 Applicative 無法做到的,<*>所串接的函數彼此是不相依的。

General concatenation

如果都是去 lift 普通函數並且 apply 到有額外結構的值中,如f a, m a,那麼用 fmap 就行了。
但如果這個函數a -> m b,是返回一個有 monadic structure 的值時,使用fmap會發生什麼事情呢?

1
2
3
4
5
6
7
8
-- 將 f 用 m 來表示
<$> :: (a -> b) -> m a -> m b
-- 將 b 代換成 m b
<$> :: (a -> m b) -> m a -> m (m b)
-- ex
fmap (\x -> [x]) [1,2,3]
= [[1], [2], [3]]

可以看到最終返回的結果是m (m b),但我們並不想要有改變原有的架構,變成巢狀的結構,如巢狀的 List。所以要想辦法將 m (m b) 轉換成 m b,也就是去 flattern 或是 concate 這個雙層的結構。

而這就是 Monad 的一個特色所在,join。

1
join :: Monadm m => m (m a) -> m a

可以看到,join所做的事情就是去concate這兩層m m為一個m。因此也可以看出 Monad 其實提供了更 general 的 concat 方法。

所以我們用join和fmap其實就可以構造出>>=

1
2
(>>=) : m a -> (a -> m b) -> m b
m >>= f = join $ fmap f m

因此可以看出,其實 Monad 就是提供一個方法,讓我們去 map 函數,最後再將其結果join起來。


結語

Monoid, Funtor, Applicative, Monad … 這些 typeclass,都代表著某些行為的抽象。

所以這些東西真的有存在的必要嗎?個人認為就工程的角度而言都沒有。
今天就算把名稱換成Apple, Orange, Banana, Pineapple也是可以(並且比較不會讓人畏懼?),叫什麼名字都不是重點。並不是因為他是Monad我們才可以做到什麼什麼,一樣可以用其他方式來達成一樣的目的。
我們需要是程式行為上的抽象或是本質上的改變,使得程式碼的重用性和表達能力能夠提升,而這也正是 functional programming language 的強項。

而因為這些被抽象的行為產生出來的輪廓 (type signature),可以跟範疇論 (category theory) 去結合,所以拿範疇論的專有名詞作為 typeclass 的名稱,較能將數學與程式結合的意圖反應出來。

除此之外我們可以看到,不同型別(Maybe, List, …),在不同 typeclass instance 的定義中,儘管都有做fmap,<*>, >>=等等的函數存在,但他們所做的事情、邏輯是獨立的,但最終他們所具有的結構是一致的。

我們其實也可以將其命名為listFmap, maybeFmap … 等等,但因為統一了 interface 大家的名稱保持一致,並且具有相同的結構,這帶來的好處就是能再更近一步的去重用這些代碼,建構更高層次的抽象方法。

參考資料

What I Wish I Knew When Learning Haskell 2.3 ( Stephen Diehl )

Monad - HaskellWiki

Learn You a Haskell for Great Good!

Haskell 系列 - typeclass (類型類)

jackypan1989 發表於 2018-03-27

Type class (類型類)

首先看一下 (+) 這個 operator 的 type

1
2
> :t (+)
(+) :: Num a => a -> a -> a

可以發現除了原本的 a -> a -> a 之外
還多了一個大箭頭, 這裡的 Num a 代表後面類型 a 一定是 Num 這個 typeclass
我們又稱為類型約束, 如果用 oo 講法來看, 他就像是 interface
你必須實作一些 Num typeclass 的 function 才能屬於 Num

在看一下 Num 的詳細資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> :i Num
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
{-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
-- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’

這裡表示 Num 這個 typeclass 底下必須
實作 (+), (*), abs, signum, fromInteger, (negate | (-)) ( | 代表至少一個要作)
目前有 Word, Integer, Int, Float, Double 這幾個 type 是 Num 的 instance

其他常用的內建 typeclass

Show

將 值或是data 轉為String
例如

1
2
3
4
5
> :t show
show :: Show a => a -> String
> show 1
"1"

Read

跟 show 相反, 將String轉為對應的值或data
例如

1
2
3
4
5
> :t read
read :: Read a => String -> a
> read "3" :: Int -- 這裡給compile提示
3

Ord

實做了 compare 或是比較運算元
例如

1
2
3
4
5
6
7
8
> :t (>)
(>) :: Ord a => a -> a -> Bool
> (>) 3 4
False
> compare 3 4
LT

其中 compare 會回傳 Ordering 這個 type

1
2
> :i Ordering
data Ordering = LT | EQ | GT

分別代表小於, 等於, 跟大於

實作 instance & 自動派生 (derive)

實作 instance 部分,舉個例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> data Grade = A | B | C | Fail
> show A
<interactive>:32:1: error:
> :{
instance Show Grade where
show A = "A"
show B = "B"
show C = "C"
show Fail = "no grade!"
:}
> show A
"A"
> show Fail
"no grade!"

對於一些簡單的類型,haskell compiler 可以將他自動派生給一些 typeclass
像是 Read, Show, Bounded, Enum, Eq, Ord 都可以
他就會自己去產生一些行為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> :{
data Grade = A | B | C | Fail
deriving (Read, Show, Eq, Ord)
:}
> show A
"A"
> show Fail
"Fail"
> A == C
False
> compare A B
LT

newtype

之前提到 data 關鍵字 可用來定義一個 type
但現在如果只有一個 data constructor (沒有 xx | oo) 又只有一個參數時
就可以用 newtype 取代

1
2
3
data MyType = MyType Int
等於
newtype MyType = MyType Int

差別在於編譯期 MyType 都是一個新的 type 編譯器會幫你檢查
但實際上執行時由於只有一個 data constructor 且 唯一參數
他就直接把 該參數型態 Int 當作 MyType 型態了, 也當然省去了 data constructor 的解構

參考資料

  1. haskell wiki
  2. learn you a haskell

在Haskell使用Module

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

Module

大部分的專案我們都會需要引入其他第三方或是自己寫的Module、Package,目的就是爲了讓許多函數可以重複操作
而最基本的一些操作都是在Prelude這模組裡,預設會自動載入,下stack ghci指令就會發現會是Prelude> ...的prompt

Data.List

Data.List讓我們可以更方便的對List做處理,nub這Function可以幫我們從List取出不重複的數字

1
2
3
4
5
6
7
8
9
10
-- Main.hs
module Main (main) where
import Data.List
main :: IO ()
main = print "Main"
numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub

在ghci中:l Main後,呼叫numUniques [1,2,2,3]就會是3了
但在此個人建議指針對要用到的Function做引入,例如這邊只用到nub那我們就改寫一下import部分

1
import Data.List(nub)

System.Random

都說Hakell是一個純函數語言,餵給一個函數相同的參數,不管怎樣都會回傳相同的結果,那要怎實作「隨機」的狀況呢?
就是利用這個System.Random Module,而要實做隨機的函數就叫做random

1
random :: (RandomGen g, Random a) => g -> (a, g)

來分別講解兩個參數,RandomGen typeclass是指作爲亂源的type(Gen是Generator縮寫)
Random typeclass是指可以裝亂數的type(像是Int,Bool…)
首先我們要一個RandomGen的實例,在System.Random裡的StdGen就是了,剛好有個mkStdGen函數可以幫我們產生這實例,只要傳給他Int即可

1
mkStdGen :: Int -> StdGen

跑跑看

1
2
random (mkStdGen 100)
> (-3633736515773289454,693699796 2103410263)

剛有提到random會回傳的是一個Tuple(Random, RandomGen),逗號右邊的確實是RandomGen(空格沒問題),我們的值不一樣也很正常
在沒有指定回傳的type時,預設是Int,所以我們也可以指定成Bool、Char

1
2
random (mkStdGen 100) :: (Bool, StdGen)
> (True,4041414 40692)

在這也可以發現兩次都有回傳不一樣的StdGen,如果要連續做好幾次random就可以接着用下去,所以剛好有個randoms函數,接收一個StdGen然後回傳無窮的List

1
2
3
-- randoms :: (RandomGen g, Random a) => g -> [a]
take 5 $ randoms (mkStdGen 11) :: [Bool]
> [True,True,True,True,False]

自己的Module

該來自己寫個Moduel試試,在Main.hs當前目錄新增一個Lib.hs,並如下這樣寫,記得在Main裡import Lib

1
2
3
4
5
6
7
8
9
module Lib
( someFunc
) where
someFunc :: IO ()
someFunc = putStrLn "someFunc"
someFunc' :: IO ()
somwFunc' = putStrLn "no no no..."

:r重新載入後,分別呼叫someFunc、someFunc'試試,會發現someFunc'會報錯,因爲我們只有export出someFunc而已
通常專案還會有許多子資料夾,我們新建一個Module的資料夾,將我們的Lib.hs移進去,這時候重新載入可以,但呼叫someFunc就報錯,因爲找不到了,我們要改寫一下module的名字就可以了

1
2
3
4
5
6
-- /Module/Lib.hs
module Module.Lib
( someFunc
) where
-- /Main.hs
import Module.Lib

那會碰上不同Module可是Function name重複的問題,Haskell當然也有alias的方法,呼叫時只要用Lib.someFunc的形式就可以了

1
2
import qualified Module.Lib as Lib
import qualified Module.XXX as XXX

參考資料

Learn your haskell

Haskell 系列 - type (類型)

jackypan1989 發表於 2018-03-23

Type (類型)

在 haskell 中我們是使用 data 關鍵字來定義所謂的類型
先看一個例子, Bool 是一個常見的類型, 包含兩個值(data), True 跟 False

1
data Bool = True | False

在這裡
= 號的左邊我們稱為 Type Constructor
= 號的右邊我們稱之為 Data Contructor (或者是 Value Contructor)

所以 Bool 是 Type Constructor, 用 kind 可查看這 type 的特徵
:: 的後面代表他的類型

1
2
> :k Bool
Bool :: *

出現 * 表示他本身就是一個 Concrete type
所以 Bool 既是 type 同時也是 type constructor (但無參數)

另一方面
True 跟 False 是 Data Contructor, 用 :t 可查看他是屬於什麼 type

1
2
> :t True
True :: Bool

True 是 Bool type 的data/value, 也不需要參數

小結一下, 如果要用命令列來看
:k 是用來查看等號左邊的 type contructor (例如 Bool)
:t 是用來查看等號右邊的 data contructor (例如 True)

看看比較複雜的 Maybe a 類型

1
data Maybe a = Just a | Nothing

這裡的 Maybe a 就如同上面的 Bool 一樣, 由兩個 data contructor 組成
但這裡的 a 我們又稱為 type variable
用來跟 Maybe 一起組合並回傳一個真正的 type 即為 Maybe a
a 用表示他有可能是 Int, String, 甚至可能是另一個 Maybe b (酷吧, 自己的定義自己用)
所以, 以類型 Maybe Int 來說
他的 data 中可能有 Just 1, Just 2, Just 4 … & Nothing 他們的 type 都是 Maybe Int

1
2
3
4
5
6
7
8
> :t Just "a"
Just "a" :: Maybe String
> :t Nothing
Nothing :: Maybe a
> :t Nothing :: Maybe Int
Nothing :: Maybe Int :: Maybe Int

在看 Nothing 的 type 時必須給他一點提示
協助他去推斷 Nothing, 否他會只給你一個 Maybe a
因為 Nothing 都是所有 Maybe a 會有的值

再回頭看看這個 type 的 kind

1
2
3
4
5
> :k Maybe Int
Maybe Int :: *
> :k Maybe
Maybe :: * -> *

剛剛有說過一個 * 才表示已經是 concrete type 了
這裡會出現一個 * -> * 表示他只是 type contructor
你必須先傳進一個 concrete type 才會回傳一個 concrete type
這就是為什麼只打一個 Maybe 還不夠, 一定要接一個 Int (concrete type)

注意事項

1. data constructor 產生出來的是值/data, 可以傳入 function, 但 type 不能傳入 function (Data constructors as first class values)

例如 f 是某個 function
則 f (True) 或是 f (Just 2) 都合法
但 f (Maybe) 就不合法了

2. data constructor 不是 type (Data constructors are not types)

例如我們定義一個

1
2
3
4
5
6
7
8
9
data MyMaybe a = Just a | Nothing
合法
data MyMaybe a = Just (Just a) | Nothing
不合法
data MyMaybe a = Just (Maybe a) | Nothing
合法
(Data contrutor 裡面如果要塞參數一定是要塞 type 只有 Maybe a 是 type, Just a 是值)

更多例子

1
data Color = Red | Blue | Green | RGB Int Int Int

Color 是一個 type 而且也是一個不用傳值的 type contructor

1
2
> :k Color
Color :: *

Red 跟 RGB 都是 data contructor
其中 Red 不用傳參數
RGB 要傳三個 Int 才會回傳一個 value, 其 type 是 Color

1
2
3
4
> :t Red
Red :: Color
> :t RGB
RGB :: Int -> Int -> Int -> Color

參考資料

  1. haskell wiki
  2. learn you a haskell

Applicative Functor

tancc 發表於 2018-03-21

1 Applicative TypeClass

Applicative Functor比起Functor更為強大,以下是其部分typeclass定義

1
2
3
4
5
6
class Functor f => Applicative f where
{-# MINIMAL pure, ((<*>) | liftA2) #-}
-- | Lift a value.
pure :: a -> f a
-- | Sequential application.
(<*>) :: f (a -> b) -> f a -> f b

一個類型如果為Applicative的實例(instance)的話,則他同樣會具有funtoral structure,也就是f a 中的 f。

而和Functor最主要多的不同在於,多了兩個方法pure和<*>

pure:將型別為a的input,包進一個Applicative的結構f中,轉換成型別為f a的output

<*>: 與fmap相似,都是把函數提升(lift),使其能應用在型別具有額外結構的值中。不同的是<*>所提升的function,本身就被包裹在f裡f (a -> b)

同時比較fmap跟<*>會比較有感:

1
2
fmap :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

差別只在要lift的函數有沒有f被包裹起來。


2 Applicative functor laws

但一個型別就算其實做了pure以及<*>成為了Applicative的instance了,也還不能夠說是一個Applicative

除了實作pure以及<*>外,還需要滿足4個Applicative functor laws

1
2
3
4
5
6
7
8
9
10
11
-- 1. Identity
pure id <*> v = v
2. Composition
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
3. Homomorphisom
pure f <*> pure x = pure (f x)
4. Interchange
u <*> pure y = pure ($ y) <*> u

因為在Haskell中,你定義完instance Applicative XXX where...後,並不會幫你檢查是否滿足這些Applicative functor laws,因此這部分需要自己去確保。


3 一些使用情境

  • 將普通函數(a -> b -> c ->...),應用在多個有functoral structure/context的值f a, fb, ...時
1
2
fmap :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

從type signature中可以看到,fmap無法將f (a -> b) 應用於 f a,f b

1
2
3
4
5
6
7
x = fmap (+) (Just 1)
--x = Just (+ 1) 型別為 f (a -> b)
fmap x (Just 2)
-- error
-- 我們無法用fmap對x進行操作了
-- fmap的第一個參數型別要為 (a -> b),不可為 f (a -> b)

這時就可以使用<*>搭配<$>來達成需求了

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
(+) <$> (Just 1) <*> (Just 2)
>> Just 3
(+) <$> (Just 1) <*> (Just 2)
= Just (+ 1) <*> (Just 2)
= Just 3
-- 另個例子
(\x y z -> [x*y, y*z, x*z]) <$> (Just 1) <*> (Just 2) <*> (Just 3)
>> Just [2,6,3]
(\x y z -> [x*y, y*z, x*z]) <$> (Just 1) <*> (Just 2) <*> (Just 3)
= Just (\y z -> [1*y, y*z, 1*z]) <*> (Just 2) <*> (Just 3)
= Just (\z -> [1*2, 2*z, 1*z]) <*> (Just 3)
= Just ([1*2, 2*3, 1*3])
= Just [2,6,3]
-- 另個例子
data User = User { firstName :: String
, lastName :: String
, email :: String
} deriving (Show)
validate :: String -> Maybe String
validate [] = Nothing
validate s = Just s
makeUser :: String -> String -> String -> Maybe User
makeUser f l e = User
<$> validate f
<*> validate l
<*> validate e
-- 可以想像makeUser如果不用<*>,勢必就必須寫很多case...of來達成

  • 多個運算間沒有相依關係時
    1
    2
    (<*>) :: Applicative f => f (a -> b) -> f a -> f b
    (>>=) :: Monad m => m a -> (a -> m b) -> m b

從type signature中可以看到,>>=所串聯的運算式有相依關係的,下一個運算的依賴上一個運算的結果。

也就是說Monad >>=的運算其實是相依(Dependency),但這也是他彈性的部分,因為我們可以針對運算的返回值再進一步的操作。

而Applicative <*>的運算是不相依(Independency)。因為少了相依性,對比起來使用Applicative可以寫出更乾淨的代碼。


4

前面有個例子(+) <$> (Just 1) <*> (Just 2),不過Control.Applicative有提供我們一些方便的工具來做同樣的事情

1
2
3
liftA :: (a -> b) -> f a -> f b
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
liftA3 :: (a -> b -> c -> d) -> f a -> f b -> f c -> f d

因此可改寫成

1
liftA2 (+) (Just 1) (Just 2)

視覺上看起來有比較簡潔了
而liftA liftA2 lift3的差別只在其所要lift的函數的參數個數而已

此外,在閱讀或撰寫上,使用<$>..<*>..<*>或許對部分人來說比較不直覺的。

這部分可以使用ApplicativeDo,使得可以跟以往使用do notation那般由上到下的書寫方式

1
2
3
4
5
6
7
{-# LANGUAGE ApplicativeDo #-}
run = do
x <- expr1
y <- expr1
z <- expr1
return (f x y z)

因為expression並不會相互依賴,因此會被轉換成f <$> expr1 <*> expr2 <*> expr3,其實就還是一樣的東西。


5 參考資料及更詳盡的內容

GHC.Base#Applicative
Haskell/Applicative functors - Wikibooks, open books for an open world
ApplicativeDo – GHC

12…4
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%