
文档教程【免费下载链接】mostly-adequate-guideMostly adequate guide to FP (in javascript)项目地址https://gitcode.com/gh_mirrors/mo/mostly-adequate-guide点击查看免费下载导读本文基于《Mostly Adequate Guide to Functional Programming》第 7 章ch07.md系统讲解函数式编程中通用的 Hindley-MilnerHM类型签名体系如何阅读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-guideMostly adequate guide to FP (in javascript)项目地址https://gitcode.com/gh_mirrors/mo/mostly-adequate-guide点击查看免费下载相关推荐跨平台应用评分解决方案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文档生成质量DeepWiki-Open单元测试与集成测试完整指南下一篇React Native Map Link与Expo项目集成教程完整配置步骤创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考