Day 09 - Type Signature

yo, what's up?

到目前为止,目前我们把最基本的概念 pure function, curry, compose 到比较进阶概念 lens, transduce 都介绍了一遍,有没有感觉对於思考程序的方式稍微改变了呢? 接下来我们将窥探 Functor, Monad 的世界,但江湖再走,配备要有,所以笔者将花三到四的篇幅介绍一些预备知识。

而今天我们将介绍 Type Signature!

What's type signature?

只要有写过静态语言的读者们,对 Type Signature 一定不陌生

像是 Java 当我们要宣告一个函式时,我们必须将该函式参数跟其回传的型别事先声明

Java

public static int add(int a, int b);

Java

而在 ML-influenced 语言,像是 haskell,其 Type Signature 只是用不同的方式去表达,这就是我们今天要介绍的主角 Hindley-Milner type signature

Hindley-Milner type signature

如果有使用 ramda 的读者们,在阅览它们文件的时候,应该都看过这个

add :: Number -> Number -> Number

ramda

这就是 Hindley-Milner type signature.

Basic

Single argument

先从最简单的函式 length 作为范例,

// length :: String -> Number
const length = str => str.length;

length :: String -> Number ,将其拆解分析

Imgur

这样我们就可以清楚知道 "有一个函式 length,需要传入一个型别为 String 的参数,而回传的型别为 Number."

Multiple argument

然而像是 add 这种函式其接收多个参数,那要如何表达呢?

大家应该已经猜想的到,如果是多个参数就用 , 拆分,并用 () 包住

// add :: (Number, Number) -> Number
const add = (x, y) => x + y;

到这里读着们应该可以发现,这有点像 ES6 的 arrow 函式的写法,只要将参数名称, 运算式与 => 改成参数型别,回传型别跟 -> 就是 HM type signature.

ES6 Arrow function

(x, y) => x + y

HM type signature

(Number, Number) -> Number

List of values

那当参数是 Array 的话那要如何表达呢?

不意外的,就是用 [] 包起来,如果 Array 内都是 String,那就是 [Stirng],如果都是 Number 那就是 [Number],以此类推

举例,take 传入目标位置,以及一组阵列,就会回传该阵列中目标位置的值

const take = (position) => (arr) => arr.at(position)

而用 HM type signature 就可以这样表达,

take :: Number -> [Number] -> Number

Function (higher-order function)

最後一个就是 HOF 了,这在 JS 的世界是非常常见的写法,传入的参数为函式,那这要如何用 HM type signature 表示呢?

例如,现在我们用 map 函式将一组元素皆是字串的阵列透过 length 转换成字串的长度

const map = (fn) => (arr) => arr.map(x => fn(x));

map(length, ['hello', 'functional', 'programming'])

所以 map 的 HM type signature 就会是

map :: (String -> Number) -> [String] -> [Number]

Type Variable

what's the problem?

刚刚看了map 的范例後,读者们应该都有些疑惑,如果今天 transformer function 是转换出来的型态不是 (String -> Number) 呢?

有可能是

map(double, [1, 2, 3, 4]) // [1, 4, 9, 16]
map(isEven, [1, 2, 3, 4]) // [false, true, false, true]

而其对应的 HM type signature 为

map :: (Number -> Number) -> [Number] -> [Number]
map :: (Number -> Boolean) -> [Number] -> [Boolean]

在面对这麽多种可能下,我们总不能把所有可能的结果列出一张大表。

how to solve?

这就是 Type variable 存在的原因,其概念有点类似泛型,像是 map 这种通用函式,就需要用更通用的方式去表达

map :: (a -> b) -> [a] -> [b]

这边值得注意的地方有

1. 型别的写法区分

  • 通用的型别用以 小写 作为开头,例如 a, b, c,...
  • 特定的型别则是用 大写 作为开头,例如 String, Number, Boolean...

2. 通用型别在同一个 type signature 不能够进行复用,除非该通用型别皆指向相同的特定型别

Bad

例如 map, 不能将 a 同时代表不同型别

map :: (a -> a) -> [a] -> [a]

3. 不同的通用型别可以代表相同的特定型别

例如 map

map(double)([1, 2, 3, 4]) // [1, 4, 9, 16]

而上述这段函式是长这样 (Number -> Number) -> [Number] -> [Number],其对应到 map 的 HM type signature (a -> b) -> [a] -> [b],则 a, b 皆可以代表 Number.

Type Constraints

若今天某个特定的 method 只能接受某些特定的型别呢? 这也是 Type Constraint 存在的原因

在之後的章节我们会频繁的看到,像是

equals :: Setoid => a -> a -> Boolean

可以看到这里出现了先前没看过的新的符号 =>!那这个代表什麽呢?

其代表以 => 作为分界点,只要满足分界点左边的所有条件,那麽右边的表示式就是有效的。

白话一点可以这样解释, 若 aSetoid, 则 a -> a -> Bool 成立。

在这里先别管 Setoid 是什麽,这之後我们有机会讲解到。而在这里就先理解为比较两值是否相等的 Type class

令人哀伤的是,JavaScript 为动态语言,其并没有自建 Type checking 功能,我们无法知道开发者是不是真的放入正确的资料型别,举例来说,现在有一个 type signature 其要求某值必须是要 [a],但实际上我们没有能力限制开发者不能放入 [1, '1', true, () => 1] 之类的值。

但它的存在还是非常重要的,至少可以在注解上清楚的知道这个函式应该要放入的正确型别!

Why

有时函式名称不一定是最精准,若对方乱取,则很容易让人误导,Type Signature 除了可以让协作者用读你的程序时更快进状况,也能更正确的使用你写的函式。另外一个优点我想就是 Free Theorems,而该作者已经有很好的解释,笔者就不多做赘述。

小结

感谢大家阅读!

NEXT: Type class & ADT

Reference


<<:  [DAY 09]Discord Bot回覆带入图片方法

>>:  用React刻自己的投资Dashboard Day9 - useEffect hook

二十九日目:JavaScript XMLHttpRequest 弐ノ章

こんばんわー(U 'ᴗ' U)⑅ SONYKO 打油。 连续一周睡眠 < 5小时了,我是谁我在...

DAY13 Kotlin基础 Class

大学期间上系统分析时,教授在台上说: 「今天的内容呢,是 Class 的 Class 。」 ????...

Day 4:谈谈 docker 的 restart policy

经过昨天的一番努力,我们已经可以在服务无法存取的时候收到通知,那麽今天就来看看另一个议题:服务的重启...

Day25- 如何盘中计算技术指标且发送讯号到line: 成果示范

今天要整合先前所学,使用colab盘中即时计算技术指标,当技术指标达到我们要的条件时,发送讯息到li...

Day27 Let's ODOO: Backup

Odoo举凡各种设定、操作、权限都储存在自己的PostgreSQL 资料库里,所以我们要迁移服务是非...