iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 10
1

本章重點

  • 在某些特定的情形,將導致程式錯誤,如:搜尋不到結果、 undefined 、 null 。
  • 為了這種錯誤處理, FP 中特別使用了新的 Type ︰ MaybeEither ,並且實作與 Array 相同的 Higher order Function 。
  • Maybe 當遇到錯誤將回傳 Nothing , 而 Nothing 將不會對 map 有任何反應。
  • Either 為 Nothing 也帶值的 Maybe , 為了能操作 Left 而有了 bimap

在開始之前

在這個 gist 有我們之前實作的 FP Function,在後續文章中也不時會用到,如果在這篇文章中有發現不認識的 Function ,你應該可以在這找到它。

另外,這兩篇有大量的 JS 語法講解,如果你覺得看不太懂程式碼,可以先看看這兩篇。

不安全的 head

還記得在遞迴陣列的時候,我們最常用的 pattern 嗎?

const map = curry((func, array) => {
    if (array.length == 0) {
        return []
    } else {
        const [x, ...xs] = array
        return [func(x), ...map(func, xs)]
    }
})

這段程式碼不會出錯,是因為我們很清楚自己在做什麼

試想,如果 head([]) ( head 一個空陣列)會發生什麼事呢?

headcompose 在過去的文章,你可以點擊它們觀看,也可以在 gist 找到它們。

// trim :: String -> String
const trim = string => string.trim()

// getFirstChar :: String -> Char
const getFirstChar = compose(head, trim)

// toLowerCase :: Char -> Char
const toLowerCase = string => string.toLowerCase()

console.log(toLowerCase(getFirstChar('Merry Xmas'))) // m
console.log(toLowerCase(getFirstChar('          ')))
// Uncaught TypeError: Cannot read property 'toLowerCase' of undefined
//   at toLowerCase (<anonymous>:11:38)

head([]) 會回傳 undefined ,而我們後序的操作會也會因此而中斷。同樣的, Array 搜尋也有相同的情況。

// toUpperCase :: Char -> Char
const toUpperCase = string => string.toUpperCase()

const mambo5 = ['Monica', 'Erica', 'Rita', 'Tina', 'Sandra']
const aLittleBitOf = target => 
    mambo5.find(x => x == target)

console.log(toUpperCase(aLittleBitOf('Erica'))) // ERICA
console.log(toUpperCase(aLittleBitOf('Yuniko')))
// Uncaught TypeError: Cannot read property 'toLowerCase' of undefined
//   at toLowerCase (<anonymous>:7:38)

Array.find 是 ES6 提供的新 Funciton ,須傳入一個 Function ︰

  • 回傳第一個的為 true 的元素。
  • 若沒有元素為 true ,回傳 undefined

如此就不能保證程式碼真正安全,程式會在某天意外的終止,而我們在夜裡無法安然入睡。

Maybe

或許我們需要一種新的 Type ,讓我們來嘗試看看。

我們把這種 Type 命名為 Maybe

  • 自動判斷是否為 null 或是 undefined ,如果是的話,不對它做任何動作。
  • 如果可以的話,與我們之前的 Heigher order Function 結合。
// data Maybe a = Nothing | Just a

我們用 data 語法,定義一個新的 Type 。

  • data Maybe a
    1. 定義一種新的資料結構,叫做 Maybe 。
    2. Maybe 必須傳入一個 a , a 表示任何型別, Maybe 稱之為 Type Constructor ,用在 Type Check。
  • Nothing | Just a
    1. Nothing , 不會使用到傳入的 a ,它來表示 Nullable 的資料。
    2. Just a ,接下傳入的 a ,以供後續利用。
    3. Nothing 與 Just 稱之為 Data Constructor ,是被程式利用的資料。

另外 Data 、 Type 必須為大寫,函數則為小寫

如果你還是不太明白這段語法,讓我再舉一個例子。

// data Boolean = False | True

這邊的 Boolean 不需要傳入任何參數,而且有兩種值 FalseTrue 。在程式實作,是使用 FalseTrueBoolean 是 Type 的名稱。

再來讓我們來實作 Just。

class Just {
	constructor(a) {
		this._value = a
	}
	// map :: (a -> b) -> Maybe b
	map(f) {
		return new Just(f(this._value))
	}
	// maybe :: (a- > b) -> b
    maybe(n, f) {
		return f(this._value)
	}
    // toString :: Maybe a -> String
	toString() {
		return `Just ${this._value}`
	}
}
  • Just 需要傳入一個 a ,然後實作 map 、 maybe 。
  • map
    • 傳入一個 (a -> b) ,回傳 Maybe b
  • maybe
    • 相對應 Array 的 reduce , reduece 是從 Array 取出元素, 兩者是一樣的概念。
    • 傳入 default 與 Function ,但只會回傳 Function(this._value)
class Nothing {
	constructor() {}
	// map :: (a -> b) -> Maybe b
	map(f) {
		return new Nothing()
	}
	// maybe :: (a- > b) -> b
	maybe(n, f) {
		return n
	}
    // toString :: a -> String
	toSting() {
		return 'Nothing'
	}
}
  • Nothing 不須傳入參數,亦有實作 map 、 maybe 。
  • map
    • 傳入一個 (a -> b) ,但只會 回傳 Nothing
  • maybe
    • 傳入 default 與 Function ,但只會回傳 default

最後建立一些 static 方法。

const Maybe = {
	Nothing: _ => new Nothing(),
	Just: a => new Just(a),
	of: a => new Just(a),
	fromNullable: a => {
		if (a === null || a === undefined) {
			return new Nothing()
		} else {
			return new Just(a)
		}
	}
}

在這有兩個建構式︰

  • of
    傳入任何值,都放進 Just , 相對應 Array.of
  • fromNullable
    傳入 null 或是 undefined 會回傳 Nothing,其餘形同 Maybe.of 。

那我們來實作安全的 Array.find 。

// safeFind :: Array a -> (a -> Bool) -> Maybe a
Array.prototype.safeFind = function(f) {
	return Maybe.fromNullable(this.find(f))
}

// toUpperCase :: Char -> Char
const toUpperCase = string => string.toUpperCase()

const mambo5 = ['Monica', 'Erica', 'Rita', 'Tina', 'Sandra']
const aLittleBitOf = target =>
	mambo5
		.safeFind(x => x == target)
		.map(toUpperCase)
		.maybe(
			'Not found, so sad.',
			x => `a little bit ${x} in my life!`
		)

console.log(
    aLittleBitOf('Erica'), // a little bit ERICA in my life!
    aLittleBitOf('Yuniko') // Not found, so sad.
)

太棒了,不只讓我們遠離了討人厭的 undefined ,還能繼續使用 map , api 都保持一致!

那讓我們試試安全的 head 。


const safeHead = compose(Maybe.fromNullable, head)

const mambo5 = ['Monica', 'Erica', 'Rita', 'Tina', 'Sandra']

console.log(
    safeHead(mambo5).toString() // Just Monica
)

或許把所有 api 包裝,是個好點子。當我們很清楚自己在幹麻時,可以用 unsafe 的版本,如果要避免出錯,則使用 safe 的版本。

另外 Maybe 也能用在 Try...Catch , 但一旦發生錯誤,只能回傳 Nothing ,沒辦法保存錯誤訊息,讓我們試試另一個 Type 。

Either

// data Either a b = Left a | Right b

Either 比較不同的是 需要傳入兩個參數 ,這是個很大的差異,這會讓我們定義函數有些許的差異,我們後面慢慢解釋。

class Left {
	constructor(a) {
		this._value = a
	}
    // map :: (b -> c) -> Either a c
	map(f) {
		return new Left(this._value)
	}
    // bimap :: (a -> c) -> (b -> d) -> Either c d
	bimap(f, _) {
		return new Left(f(this._value))
	}
    // either :: (a -> c) -> (b -> d) -> c
	either(f, _) {
		return f(this._value)
	}
    // toString :: a -> String
    toString(){
        return `Left ${this._value}`
    }
}

class Right {
	constructor(b) {
		this._value = b
	}
    // map :: (b -> c) -> Either a c
	map(f) {
		return new Right(f(this._value))
	}
    // bimap :: (a -> c) -> (b -> d) -> Either c d
	bimap(_, g) {
		return new Right(g(this._value))
	}
    // either :: (a -> c) -> (b -> d) -> d
	either(_, g) {
		return g(this._value)
	}
    // toString :: a -> String
    toString(){
        return `Right ${this._value}`
    }
}

const Either = {
	Left: a => new Left(a),
	Right: b => new Right(b),
	of: b => new Right(b)
}

Maybe a 其實與 Array a 相同,只要傳入一個參數就可以建構,而 Either 卻需要兩個。

如果 map 是操作 MaybeArray,那 map 應該是操作 Either a (都是少一個參數)。
所以我們另外創作 bimap ,一個雙頭龍的 Map ,可以接下兩個 Funtcion,使它可以操作 Either (少兩個參數)。
同理 either 也是接下兩個 Function , 與 reduce 不同

或許你會覺得「沒道理呀,為什麼 map 一定只能接受一個 Function ?」,這個我保證你會在之後的文章得到答案的。

讓我們試用看看 Either ,我們把 正確的結果放在 Right ,畢竟 Right 就是正確嘛。

// tryCatch :: (a -> b) -> Either c b
const tryCatch = f =>
	(...args) => {
		try {
			return Either.Right(f.apply(null, args))
		} catch (e) {
			return Either.Left(e)
		}
	}

// toDecimal :: String -> Int
const toDecimal = string => {
	const result = parseInt(string, 10)
	if (!isNaN(result)) {
		return result
	} else {
		throw new Error('Parse Error')
	}
}

const safeToDecimal = tryCatch(toDecimal)

console.log(
	safeToDecimal('911')
		.map(x => x + 1)
		.toString(), // Right 912
	safeToDecimal('玖壹壹')
		.map(x => x + 1)
		.toString() // Left Parse Error
)

太好了,現在我們擁有兩個 Type 可以做錯誤處理,而且 MaybeEither 擁有 mapreduce 的特性,讓 api 可以玩串串樂。

這就是...是抽象的力量的力量嗎?

現在相信你有一種感覺,我們能夠自行創造 容器 ,而這個容器會跟 Array 一樣,並且有一樣的 api 。

後記

我去過聖誕了,不過別擔心,我會把文章補完的,大家聖誕快樂喔。

參考資料


上一篇
Higher order Function = { Compose } 與 如何處理 Promise 、 Object
下一篇
Algebraic Data Types ,在 Functional Programming 定義資料結構
系列文
30天快樂學習 Functional Programming14

1 則留言

0
jameschen38
iT邦新手 5 級 ‧ 2017-12-29 17:05:01
class Just {
	constructor(a) {
		this._value = a
	}
	// map :: (a -> b) -> Maybe b
	map(f) {
		return new Just(f(this._value))
	}
	// maybe :: (a- > b) -> b
    maybe(default, f) {
		return f(this._value)
	}
    // toString :: Maybe a -> String
	toString() {
		return `Just ${this._value}`
	}
}

map的實作是不是有誤?

是否該呼叫map function?

return new Just(map(f, this._value))

Anyway, 感謝大大這一系列的文章,帶領我跨進FP的領域

看到Type Signature了

// map :: (a -> b) -> Maybe b

看來是我把這裡的map跟 map function 搞混

阿志 iT邦新手 5 級‧ 2018-01-01 14:18:28 檢舉

小修正了一下
default 為保留字@@
失誤失誤

我要留言

立即登入留言