- 文档
- 教程
【免费下载链接】mostly-adequate-guide
Mostly adequate guide to FP (in javascript)
导读
本文基于《Mostly Adequate Guide to Functional Programming》第 7 章(ch07.md),系统讲解函数式编程中通用的 Hindley-Milner(HM)类型签名体系:如何阅读String -> String、(a -> b) -> [a] -> [b]这类签名,如何借助柯里化理解多参数函数,以及如何利用"参数化"推导函数的可能行为与自由定理。读完本文,你将能够在动态语言 JavaScript 中以注释形式书写类型签名,直接读懂仓库 support/index.js 与各章练习中所有带签名注释的代码,并以此为工具指导自己的函数式编码。
一、为什么函数式程序员如此看重类型签名
刚接触函数式世界的人,很快就会发现自己被"淹没"在类型签名(type signatures)之中。类型是一种元语言(meta language),它让来自不同背景的开发者能够简洁、高效地交流。在函数式世界,这套签名体系绝大多数时候以Hindley-Milner系统的形式书写,这也是本章(以及后续所有章节)反复使用的语言。
对于纯函数而言,类型签名具有英语无法比拟的表现力:一行紧凑的签名就能"泄露"函数的行为与意图,你可以从中推导出"自由定理"(free theorems);类型可以被自动推断,因此无需显式标注;它既可以精确到极致,也可以保持一般与抽象;它不仅是编译期检查的依据,更是"最好的文档"。
JavaScript 是一门动态语言,但这不意味着我们要完全回避类型——字符串、数字、布尔值依然存在,只是语言层面没有集成,我们得把类型信息"记在脑子里"。既然签名用于文档目的,我们完全可以用注释来承担这一角色:
// capitalize :: String -> String const capitalize = s => toUpperCase(head(s)) + toLowerCase(tail(s)); capitalize('smurf'); // 'Smurf'当然,JavaScript 生态中也有 Flow、TypeScript 这类类型检查工具。但本书的目标是"学会写出函数式代码",因此采用 FP 语言中标准的类型系统(HM),而非绑定某一种 JS 方言。
二、HM 签名基础:从 "a -> b" 开始
在 HM 系统中,函数写作a -> b,其中a、b是任意类型的变量。因此capitalize的签名可以读作"一个从String到String的函数"——输入一个String,输出一个String。
再来看几个签名:
// strLength :: String -> Number const strLength = s => s.length; // join :: String -> [String] -> String const join = curry((what, xs) => xs.join(what)); // match :: Regex -> String -> [String] const match = curry((reg, s) => s.match(reg)); // replace :: Regex -> String -> String -> String const replace = curry((reg, sub, s) => s.replace(reg, sub));strLength与capitalize同构:输入String,输出Number。而join、match、replace第一眼可能让人困惑——一个快速技巧是:把最后一个类型当作返回值。于是match可理解为"接受一个Regex和一个String,返回[String]"。
但这里还藏着更重要的东西,它与第 4 章的柯里化(currying)直接相关(见 ch04.md):每个参数都会从签名最前面"弹出"一个类型。
用括号揭示柯里化的本质
将最后一部分括起来,能揭示更多信息:
// match :: Regex -> (String -> [String]) const match = curry((reg, s) => s.match(reg));现在match被看作"接受一个Regex,返回一个从String到[String]的函数"。因为柯里化,事实确实如此:给它一个Regex,我们就拿回一个等待String参数的函数:
// match :: Regex -> (String -> [String]) // onHoliday :: String -> [String] const onHoliday = match(/holiday/ig);onHoliday就是已经持有Regex的match。给replace加满括号看得更清楚:
// replace :: Regex -> (String -> (String -> String)) const replace = curry((reg, sub, s) => s.replace(reg, sub));可见多余的括号既吵闹又冗余,所以通常省略。如果愿意,你也可以一次性给出全部参数,因此更省事的读法是:replace接受一个Regex、一个String、再一个String,返回一个String。
类型变量:id 与 map
// id :: a -> a const id = x => x; // map :: (a -> b) -> [a] -> [b] const map = curry((f, xs) => xs.map(f));id接受任意类型a,返回同类型a。类型中可以使用变量,正如代码中的变量一样;a、b只是约定俗成的名字,可随意替换,但同一个变量必须对应同一个类型:a -> b可以是任意类型到任意类型,而a -> a则要求输入输出类型一致。例如id可以是String -> String或Number -> Number,但绝不能是String -> Bool。
map引入了b,它可以与a相同也可以不同:map接受一个从任意类型a到相同或不同类型b的函数,然后接受一个a的数组,最终得到一个b的数组。这个签名几乎逐字描述了函数行为——它拿到一个a -> b的函数和一个a数组,唯一合理的做法就是把函数逐个应用到每个a上,得到b数组。除此之外的任何实现都是"睁眼说瞎话"。
三、实战解读:head、filter 与 reduce
以下是三个可以自己试着解读的签名,仓库 exercises/ch04/exercise_a.js 中类似// words :: String -> [String]的注释就是这种实践的直接体现:
// head :: [a] -> a const head = xs => xs[0]; // filter :: (a -> Bool) -> [a] -> [a] const filter = curry((f, xs) => xs.filter(f)); // reduce :: ((b, a) -> b) -> b -> [a] -> b const reduce = curry((f, x, xs) => xs.reduce(f, x));head :: [a] -> a:取a数组的第一个元素,返回一个a。filter :: (a -> Bool) -> [a] -> [a]:接受一个"a到布尔"的谓词和一个a数组,返回a数组——元素类型不变,只做筛选,这由签名本身保证。reduce最富表现力,也最难啃。逐段拆解:第一个参数是一个"吃进b与a、产出b"的函数;后续参数是一个b与一个a数组,因此b和每个a都会被喂给那个函数;函数结果是b,所以最后一次调用该函数得到的b就是整个reduce的输出。这与Array.prototype.reduce的行为完全吻合。
四、参数化:类型如何收窄实现的可能性
一旦引入类型变量,就会出现一个有趣的属性——参数化(parametricity):一个函数会以统一的方式作用于所有类型。
// head :: [a] -> ahead只拿到一个[a],除了"数组"这个具体类型外没有任何其他信息,因此它的行为被限定在数组本身的操作上。对于变量a,它一无所知、也做不了任何与a具体类型相关的事——a不是某个特定类型,而是任意类型,这迫使函数必须对每一种可能类型都统一地工作。合理的猜测只剩"取第一个、取最后一个、或随机取一个元素",head这个名字泄露了正确答案。
再看:
// reverse :: [a] -> [a]仅凭签名,reverse能做什么?它不能对a做任何特定操作,不能把a变成别的类型(否则会引入b),不能排序(信息不足,无法对所有类型排序),但可以"重排"——而且必须以完全相同的可预测方式重排;也可以选择删除或复制某个元素。无论如何,多态类型把可能的实现空间大幅收窄了。
这种收窄还带来一个实际用途:你可以借助基于类型签名的搜索工具(如 Haskell 生态的 Hoogle)按签名查找想要的函数——一条签名里浓缩的信息量相当可观。
五、自由定理:从类型免费推导等式
除了推测实现,这类推理还能为我们带来"免费"的定理。以下两条直接取自 Wadler 关于该主题的论文:
// head :: [a] -> a compose(f, head) === compose(head, map(f)); // filter :: (a -> Bool) -> [a] -> [a] compose(map(f), filter(compose(p, f))) === compose(filter(p), map(f));不需要任何代码,这些定理直接从类型推导而来。
第一条说明:先取数组的head再对结果应用f,等价于(并且通常快得多)先map(f)遍历所有元素再取head。这看起来像常识,但计算机没有常识——它们需要形式化的方法来自动化这类代码优化,而数学恰好擅长把直觉形式化。
第二条filter定理类似:把f与p组合起来决定是否过滤,再通过map真正应用f(注意filter的签名强制它不会改动元素a),始终等价于先map(f)再用谓词p过滤。任何多态类型签名都可以套用这种推理,且恒成立。在 JavaScript 中,有些工具可以声明重写规则,也可以借助compose函数本身来组织这种变换——仓库 support/index.js 与 appendix_a.md 中提供的compose实现(reduceRight从右向左组合函数)正是这类组合式推理的载体。
六、类型约束:为类型变量限定接口
最后值得注意:我们可以把类型约束到某个接口上。
// sort :: Ord a => [a] -> [a]胖箭头(=>)左侧是一个事实声明:a必须是Ord,即a必须实现Ord接口。Ord是类型化语言中预定义的接口,表示"可以对值排序"。这不仅让我们更了解a与sort的行为,还收窄了定义域。这类接口声明被称为类型约束(type constraints)。
// assertEqual :: (Eq a, Show a) => a -> a -> Assertion这里有两个约束:Eq与Show,它们保证我们可以比较两个a是否相等,并在不等时打印出差异。更多约束的例子会在后续章节(如附录中Either、Maybe、Identity等代数结构的签名,见 appendix_b.md 与 appendix_a.md)中反复出现,届时这个概念会愈发清晰——例如liftA2 :: (Applicative f) => (a1 -> a2 -> b) -> f a1 -> f a2 -> f b就是对类型构造子f的约束。
七、在仓库中实践:从注释到运行验证
Hindley-Milner 类型签名在函数式世界中无处不在。它们读写简单,但要熟练"仅凭签名理解程序"仍需时间——因此本书从本章起,每一行代码都附带类型签名。
你可以这样在本仓库中练习:
- 阅读源码注释:support/index.js 中每个导出函数都带签名注释,例如
always :: a -> b -> a、either :: (a -> c) -> (b -> c) -> Either a b -> c、identity :: x -> x,顶层函数全部柯里化,可直接对照签名理解参数顺序。 - 完成章节练习:克隆仓库后,在
exercises/各章目录中填写exercise_*.js(其注释同样使用 HM 签名,如 exercises/ch04/exercise_a.js 中的// words :: String -> [String]),然后用npm run ch04、npm run ch05等命令运行对应测试(见 package.json 与 README.md)。 - 动手运行支持库:安装
@mostly-adequate/support后即可在 REPL 中调用所有带签名的柯里化函数,验证你对签名的理解。
八、小结与下一步
本章建立了阅读函数式代码的核心能力:以 HM 签名作为沟通语言,用柯里化解释多参数函数,用参数化收窄实现可能性,用自由定理推导恒等式,用类型约束限定定义域。下一章将把这一能力用于"容器"(Tupperware)——也就是函子与单子等代数结构,届时你会看到签名如何指导我们设计出map、ap、chain这类通用接口(相关实现已收录在 appendix_b.md 中)。继续阅读 第八章:Tupperware,让类型签名成为你理解函数式世界的罗盘。
- 文档
- 教程
【免费下载链接】mostly-adequate-guide
Mostly adequate guide to FP (in javascript)
相关推荐
跨平台应用评分解决方案:react-native-store-review的iOS与Android实现对比终极指南
跨平台应用评分解决方案:react native store review的iOS与Android实现对比终极指南 在React Native应用开发中,实现应
JavaScript函数式编程完全指南:mostly-adequate-guide深度解析
JavaScript函数式编程完全指南:mostly adequate guide深度解析 JavaScript函数式编程是一个强大而优雅的编程范式,它通过数学
人工智能AI AgentAI 安全治理策略引擎Agent 沙箱认证鉴权PDFMathTranslate 完整教程:3 条命令内保留公式排版翻译学术论文
PDFMathTranslate 完整教程:3 条命令内保留公式排版翻译学术论文 PDFMathTranslate 是一个基于 AI 的开源 PDF 科学论文翻
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考