我在哪里可以找到用于解释函数式编程的符号的解释/摘要,特别是Ramda.js?

And*_*ems 6 javascript functional-programming symbols ramda.js

JavaScript函数编程库Ramda.js的API文档包含符号缩写,但没有提供理解这些的图例.有没有地方(网站,文章,备忘单等)我可以去解读这些?

Ramda.js API文档中的一些示例:

Number -> Number -> Number
Apply f => f (a -> b) -> f a -> f b
Number -> [a] -> [[a]]
(*... -> a) -> [*] -> a
{k: ((a, b, ..., m) -> v)} -> ((a, b, ..., m) -> {k: v})
Filterable f => (a -> Boolean) -> f a -> f a
Lens s a = Functor f => (a -> f a) -> s -> f s
(acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)
Run Code Online (Sandbox Code Playgroud)

我现在能够理解Ramda.js正在尝试做的很多事情,而且我经常可以做出有根据的猜测.但是我确信如果我更好地理解这些符号/陈述,我会更容易理解.我想了解单个组件的含义(例如特定字母,关键字,不同箭头类型,标点符号等).我也想知道如何"阅读"这些行.

我没有成功谷歌搜索或搜索StackExchange.我使用了"Ramda","函数式编程","符号","缩写","速记"等各种组合.我也不确定我是否正在寻找(A)普遍使用的缩写更广泛的函数式编程领域(或者甚至可能只是一般的编程),或者(B)Ramda作者正在使用的专门语法(或者可能从其他地方合作但只是进一步修改)仅用于他们的库.

Sco*_*yet 6

来自 Ramda维基

(第 2 / 2 部分 - 对于单个 SO 答案来说太长了!)


类型约束

有时我们想以某种方式限制我们可以在签名中使用的泛型类型。我们可能想要一个maximum可以对Numbers、 on Strings、 on进行操作Dates但不能对任意进行操作的函数Objects。我们想要描述有序类型,它们 a < b总是会返回有意义的结果。我们Ord 在类型部分讨论了类型的细节;就我们的目的而言,足以说明它旨在捕获那些具有与<.

// maximum :: Ord a => [a] -> a
const maximum = vals => reduce((curr, next) => next > curr ? next : curr,
    head(vals), tail(vals))
maximum([3, 1, 4, 1]); //=> 4
maximum(['foo', 'bar', 'baz', 'qux', 'quux']); //=> 'qux'
maximum([new Date('1867-07-01'), new Date('1810-09-16'),
         new Date('1776-07-04')]); //=> new Date("1867-07-01")
Run Code Online (Sandbox Code Playgroud)

这个描述 [^maximum-note]在开头添加了一个约束部分,用右双箭头与其余部分分开(=>代码中的“ ?” ,有时其他文档中的“ ”。)Ord a ? [a] ? a 说maximum需要某种类型的元素的集合,但那种类型必须坚持Ord

在动态类型的 Javascript 中,没有简单的方法来强制执行 此类型约束而不对每个参数甚至每个列表的每个值添加类型检查。[^strong-types] 但通常我们的类型签名确实如此。当我们[a]在签名中要求时,无法保证用户不会通过我们[1, 2, 'a', false, undefined, [42, 43], {foo: bar}, new Date, null]。所以我们的整个类型注释都是描述性的和有抱负的,而不是编译器强制的,就像在 Haskell 中那样。

Ramda函数最常见的类型约束是 Javascript FantasyLand 规范指定的类型约束。

当我们之前讨论一个map函数时,我们只讨论了在值列表上映射一个函数。但是映射的想法比这更普遍。它可用于描述将函数应用于任何包含一定数量的某种类型值的数据结构,如果它返回另一个具有新值的相同形状的结构。我们可能会映射 a Tree, a Dictionary,一个Wrapper只包含一个值的平原 ,或许多其他类型。

其他语言和 FantasyLand 从抽象数学中借用的代数类型(称为Functor. AFunctor只是一种类型,它包含map受一些简单规则约束的方法。Ramda 的map 函数将map在我们的类型上调用该方法,假设如果我们没有传递列表(或 Ramda 已知的其他类型)但确实传递了一些东西map,我们希望它像Functor.

为了在签名中描述这一点,我们在签名块中添加了一个约束部分:

// map :: Functor f => (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

请注意,约束块不必只有一个约束。我们可以有多个约束,用逗号分隔并用括号括起来。所以这可能是一些奇怪函数的签名:

// weirdFunc :: (Functor f, Monoid b, Ord b) => (a -> b) -> f a -> f b
Run Code Online (Sandbox Code Playgroud)

无需纠结于它做什么或如何使用Monoidor Ord,我们至少可以看到需要提供哪些类型才能使该函数正确运行。

[^maximum-note]:这个maximum函数有问题;它将在空列表上失败。试图解决这个问题会让我们走得太远。

[^strong-types]:有一些非常好的工具可以解决 Javascript 的这个缺点,包括语言内技术,例如 Ramda 的姊妹项目Sanctuary,Javascript 的扩展以具有更强的类型,例如flowTypeScript等等编译为 Javascript 的强类型语言,例如 ClojureScriptElmPureScript

多重签名

有时,与其试图找到签名的最通用版本,不如单独列出几个相关的签名。它们作为两个单独的 JSDoc 标签包含在 Ramda 源代码中,并在文档中作为两个不同的行结束。这就是我们在自己的代码中编写一个的方式:

// getIndex :: a -> [a] -> Number
//          :: String -> String -> Number
const getIndex = curry((needle, haystack) => haystack.indexOf(needle));
getIndex('ba', 'foobar'); //=> 3
getIndex(42,  [7, 14, 21, 28, 35, 42, 49]); //=> 5
Run Code Online (Sandbox Code Playgroud)

显然,如果我们选择,我们可以做两个以上的签名。但请注意,这不应该太常见。目标是编写足够通用的签名来捕获我们的用法,而不是抽象到它们实际上掩盖了函数的用法。如果我们可以用一个签名来做到这一点,我们可能应该这样做。如果需要两个,那就这样吧。但是如果我们有很长的签名列表,那么我们可能缺少一个共同的抽象。

拉姆达杂记

可变函数

将此样式签名从 Haskell 移植到 Javascript 涉及几个问题。Ramda 团队已经临时解决了这些问题 ,这些解决方案仍然会发生变化

在 Haskell 中,所有函数都有一个固定的元数。但是 Javsacript 必须处理可变参数函数。Ramda 的flip函数就是一个很好的例子。这是一个简单的概念:接受任何函数并返回一个交换前两个参数顺序的新函数。

// flip :: (a -> b -> ... -> z) -> (b -> a -> ... -> z)
const flip = fn => function(b, a) {
  return fn.apply(this, [a, b].concat([].slice.call(arguments, 2))); 
}; 
flip((x, y, z) => x + y + z)('a', 'b', 'c'); //=> 'bac'
Run Code Online (Sandbox Code Playgroud)

这个[^flip-example] 展示了我们如何处理可变参数函数或固定但未知元函数的可能性:我们简单地使用省略号(...源代码中的“”,输出文档中的“``”)来表明存在该签名中缺少一些不计其数的参数。Ramda 已经从它自己的代码库中删除了几乎所有的可变参数函数,但这就是它处理与我们不知道的签名交互的外部函数的方式。

[^flip-example]:这不是 Ramda 的实际代码,它牺牲了一些简单性以换取显着的性能提升。

Any / * 类型

我们希望尽快改变这一点,但 Ramda 的类型签名通常包含星号 ( *) 或Any合成类型。这只是一种报告方式,尽管这里有一个参数或返回值,但我们无法推断出它的实际类型。我们已经意识到只有一个地方仍然有意义,那就是当我们有一个类型可以变化的元素列表时。那时,我们可能应该报告[Any]. 任意类型的所有其他用途可能都可以替换为泛型类型名称,例如ab。这种变化可能随时发生。

简单对象

我们可以选择多种方式来表示纯 Javascript 对象。显然,我们可以只说Object,但有时似乎需要其他东西。当一个对象被用作类似类型值的字典(而不是它作为 a 的其他角色 Record)时,键和值的类型就会变得相关。在一些签名中 Ramda 使用“ {k: v}”来表示这种对象。

// keys :: {k: v} -> [k]
// values :: {k: v} -> [v]
// ...
keys({a: 86, b: 75, c: 309}); //=> ['a', 'b', 'c']
values({a: 86, b: 75, c: 309}); //=> [86, 75, 309]
Run Code Online (Sandbox Code Playgroud)

而且,与往常一样,这些可以用作函数调用的结果

// makeObj :: [k,v]] -> {k: v}
const makeObj = reduce((obj, pair) => assoc(pair[0], pair[1], obj), {});
makeObj([['x', 10], ['y', 20]]); //=> {"x": 10, "y": 20}
makeObj([['a', true], ['b', true], ['c', false]]);
//=> {a: true, b: true, c: false}
Run Code Online (Sandbox Code Playgroud)

记录

尽管这可能与 Ramda 本身并不完全相关,但有时能够区分用作记录的 Javascript 对象而不是用作字典的对象是有用的。字典更简单,{k: v}上面的描述可以根据需要变得更具体,用{k: Number}{k: Rectangle},或者即使我们需要它,用{String: Number}等等。如果我们选择,我们可以类似地处理记录:

// display :: {name: String, age: Number} -> (String -> Number -> String) -> String
const display = curry((person, formatter) => 
                      formatter(person.name, person.age));
const formatter = (name, age) => name + ', who is ' + age + ' years old.';
display({name: 'Fred', age: 25, occupation: 'crane operator'}, formatter);
//=>  "Fred, who is 25 years old."
Run Code Online (Sandbox Code Playgroud)

记录符号看起来很像对象文字,字段的值由它们的类型代替。我们只考虑与我们有某种关系的字段名称。(在上面的例子中,即使我们的数据有一个“occupation”字段,它也不在我们的签名中,因为它不能直接使用。

复杂示例: over

所以此时,我们应该有足够的信息来理解over函数的签名:

Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s
Run Code Online (Sandbox Code Playgroud)

我们从类型别名Lens s a = Functor f ? (a ? f a) ? s ? f s. 这告诉我们该类型Lens由两个泛型变量s、 和 参数化a。我们知道 a 中f使用的变量的类型有一个限制Lens:它必须是 a Functor。考虑到这一点,我们看到 aLens是两个参数的柯里化函数,第一个是从泛型类型的值 a到参数化类型之一的函数f a,第二个是泛型类型的值s。结果是参数化类型的值f s但是它什么作用呢?我们不知道。我们无法知道。我们的类型签名告诉我们很多关于函数的信息,但它们没有回答关于函数实际做什么的问题。我们可以假设在某个地方必须调用的map方法f a,因为这是 type 定义的唯一函数Functor,但我们不知道如何或为什么map调用它。尽管如此,我们知道 aLens是所描述的函数,我们可以使用它来指导我们对 的理解over

该函数over被描述为三个参数的柯里化函数,一个Lens a s刚刚分析的,一个从泛型类型a到相同类型的函数,以及一个泛型类型的值s。整个过程返回一个 type 值s

我们可以更深入地挖掘,也许可以进一步推论over必须如何处理它收到的类型。对所谓的自由定理进行了重要的研究,这些定理证明了仅从类型签名可推导出的不变量。但是这个文件已经太长了。如果您有兴趣,请参阅进一步阅读。

但为什么?

所以现在我们知道如何读写这些签名了。为什么我们想要,为什么函数式程序员如此迷恋它们?

有几个很好的理由。首先,一旦我们习惯了它们,我们就可以从一行元数据中获得很多关于一个函数的洞察力,而不会被名称分心。名字听起来是个好主意,直​​到你意识到别人选择的名字不是你会选择的名字。上面我们讨论了名为“ maximum”和“ makeObj”的函数。知道在 Ramda 中,等效函数被称为“ max”和“ fromPairs”是有帮助的还是令人困惑的?参数名称要糟糕得多。当然,通常还需要考虑语言障碍。即使英语已经成为通用语言在 Web 中,有些人不会理解我们关于这些功能的优美、优雅的散文。但这与签名无关;它们简洁地表达了关于函数的所有重要内容,除了它实际执行的操作

但比这更重要的是,这些签名让我们非常容易思考我们的功能以及它们如何组合。如果我们得到这个函数:

foo :: Object -> Number
Run Code Online (Sandbox Code Playgroud)

map,我们已经看到的看起来像

map :: (a -> b) -> [a] -> [b]
Run Code Online (Sandbox Code Playgroud)

然后马上就可以推导出函数的类型map(foo) 通过注意到,如果我们替换ObjectaNumberb,我们满足的第一个参数的签名map通过讨好我们将留下,其余部分,并且因此:

map(foo) :: [Object] -> [Number]
Run Code Online (Sandbox Code Playgroud)

这使得使用函数有点像众所周知的“将选项卡 A 插入插槽 A”指令。我们可以仅通过函数的形状来准确识别它们是如何插入在一起以构建更大的函数的。能够做到这一点是函数式编程的关键特性之一。类型签名使得这样做更容易。

进一步阅读


Sco*_*yet 5

来自Ramda Wiki:

(第1/2部分 - 对于单个SO答案来说太长了!)


输入签名

(或"那些有趣的箭头是什么?")

看看Ramda over函数的文档,我们首先看到的是两行,如下所示:

Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s
Run Code Online (Sandbox Code Playgroud)

对于从其他FP语言来到Ramda的人来说,这些看起来很熟悉,但对于Javascript开发人员来说,他们可能是纯粹的gobbledy-gook.在这里,我们将介绍如何在Ramda文档中阅读这些内容以及如何将它们用于您自己的代码.

最后,一旦我们了解这些是如何工作的,我们就会调查 人们为什么会这样做.

命名类型

许多受ML影响的语言(包括Haskell)使用标准方法来描述其功能的签名.随着函数式编程在Javascript中变得越来越普遍,这种签名风格正逐渐变得几乎成为标准.我们为Ramda借用和改编Haskell版本.

我们不会尝试创建正式的描述,而只是通过示例捕获这些签名的本质.

// length :: String -> Number
const length = word => word.length;
length('abcde'); //=> 5
Run Code Online (Sandbox Code Playgroud)

这里我们有一个简单的函数,length它接受一个类型的单词 String,并返回字符串中的字符数,即a Number.函数上方的注释是签名行.它从函数的名称开始,然后是分隔符" ::",然后是函数的实际描述.应该相当清楚该描述的语法是什么.提供功能的输入,然后是箭头,然后是输出.您通常会看到上面写的箭头," ->",源代码和?输出文档中的" ".他们的意思完全相同.

我们在箭头之前和之后放置的是参数的类型,而不是它们的名称.在这个描述级别,我们真正说过的是这是一个接受String并返回Number的函数.

// charAt :: (Number, String) -> String
const charAt = (pos, word) => word.charAt(pos); charAt(9, 'mississippi'); //=> 'p'
Run Code Online (Sandbox Code Playgroud)

在这一个中,函数接受两个参数,一个位置 - 一个Number- 和一个单词 - 这是一个String- 它返回一个单字符String或空String.

在Javascript中,与Haskell不同,函数可以接受多个参数.要显示需要两个参数的函数,我们用逗号分隔两个输入参数并将该组包装在括号中: (Number, String).与许多语言一样,Javascript函数参数是位置的,因此顺序很重要.(String, Number)有着完全不同的含义.

当然对于带有三个参数的函数,我们只是在括号内扩展逗号分隔的列表:

// foundAtPos :: (Number, String, String) -> Boolean
const foundAtPos = (pos, char, word) => word.charAt(pos) === char;
foundAtPos(6, 's', 'mississippi'); //=> true
Run Code Online (Sandbox Code Playgroud)

对于任何更大的有限参数列表也是如此.

注意ES6风格的箭头函数定义和这些类型声明之间的并行可能是有益的.该功能由.定义

(pos, word) => word.charAt(pos);
Run Code Online (Sandbox Code Playgroud)

通过与它们的类型与替换参数名称,身体 类型,它返回的值和脂肪箭头" =>"有一个瘦小的一个," ->",我们得到的签名:

// (Number, String) -> String
Run Code Online (Sandbox Code Playgroud)

价值清单

我们经常使用所有相同类型的值列表.如果我们想要一个函数来添加列表中的所有数字,我们可能会使用:

// addAll :: [Number] -> Number
const addAll = nbrs => nbrs.reduce((acc, val) => acc + val, 0);
addAll([8, 6, 7, 5, 3, 0, 9]); //=> 38
Run Code Online (Sandbox Code Playgroud)

输入这个功能是一个列表Number秒.关于列表的确切含义有一个单独的讨论,但是现在,我们可以将它看作基本上就像它们是数组一样.要描述给定类型的List,我们将该类型名称包装在方括号" [ ]"中.的名单Strings就[String]的列表,Booleans就 [Boolean]的列表,列表Numbers就[[Number]].

当然,这样的列表也可以是函数的返回值:

// findWords :: String -> [String]
const findWords = sentence => sentence.split(/\s+/);
findWords('She sells seashells by the seashore');
//=> ["She", "sells", "seashells", "by", "the", "seashore"]
Run Code Online (Sandbox Code Playgroud)

我们不应该意识到我们可以将这些结合起来:

// addToAll :: (Number, [Number]) -> [Number]
const addToAll = (val, nbrs) => nbrs.map(nbr => nbr + val);
addToAll(10, [2, 3, 5, 7]); //=> [12, 13, 15, 17]
Run Code Online (Sandbox Code Playgroud)

此函数接受一个Number,val和一个Numbers 的列表nbrs,并返回一个新的Numbers 列表.

重要的是要意识到这是所有签名告诉我们的.没有办法通过单独的签名将这个函数与接受a NumberNumbers 列表的 任何其他函数区分开来并返回Numbers 列表.[^定理]

[^定理]:我们可以用签名所暗示的自由定理的形式收集其他信息.

功能

还有一个非常重要的类型,我们还没有真正讨论过.功能编程完全与功能有关; 我们将函数作为参数传递,并将函数作为其他函数的返回值.我们也需要代表这些.

事实上,我们已经看到了我们如何表示功能.每个签名行记录了一个特定的功能.对于签名中使用的高阶函数,我们重复使用上面的小技术.

// applyCalculation :: ((Number -> Number), [Number]) -> [Number]
const applyCalculation = (calc, nbrs) => nbrs.map(nbr => calc(nbr));
applyCalculation(n => 3 * n + 1, [1, 2, 3, 4]); //=> [4, 7, 10, 13]
Run Code Online (Sandbox Code Playgroud)

这里的函数calc描述(Number ? Number)它就像我们的顶级函数签名一样,只是用括号括起来将它正确地分组为一个单独的单元.我们可以使用从另一个函数返回的函数执行相同的操作:

// makeTaxCalculator :: Number -> (Number -> Number)
const makeTaxCalculator = rate => base =>
    Math.round(100 * base + base * rate) / 100;
const afterSalesTax = makeTaxCalculator(6.35); // tax rate: 6.35%
afterSalesTax(152.83); //=> 162.53
Run Code Online (Sandbox Code Playgroud)

makeTaxCalculator接受税率,表示为百分比(类型 Number,并返回一个新函数,它本身接受Number 并返回a Number.再次,我们描述返回的函数 (Number ? Number),它使整个函数的签名 Number ? (Number ? Number).

哗众取宠

使用Ramda,我们可能不会写出makeTaxCalculator完全相同的东西.Currying是Ramda的核心,我们可能会在这里利用它.[^ curry-desc]

相反,在Ramda中,人们很可能会编写一个curried calculateTax 函数,可以像makeTaxCalculator你想要的那样使用,但也可以在一次传递中使用:

// calculateTax :: Number -> Number -> Number
const calculateTax = R.curry((rate,  base) =>
    Math.round(100 * base + base * rate) / 100);
const afterSalesTax = calculateTax(6.35); // tax rate: 6.35%
afterSalesTax(152.83); //=> 162.53
  // OR 
calculateTax(8.875, 49.95); //=> 54.38
Run Code Online (Sandbox Code Playgroud)

这个curried函数可以通过提前提供参数和返回值,或者只提供一个并获取正在寻找第二个的函数来使用.为此,我们使用 Number ? Number ? Number.在Haskell中,歧义很简单地解决了:箭头绑定到右边,并且所有函数都采用单个参数,尽管有一些语法手法使它感觉好像可以用多个参数调用它们.

在Ramda中,直到我们调用函数才解决歧义.当我们打电话时calculateTax(6.35),由于我们选择不提供第二个参数,我们回到Number ? Number签名的最后部分.当我们打电话时calculateTax(8.875, 49.95),我们提供了前两个Number参数,因此只返回最终参数 Number.

curried函数的签名总是这样,一系列由?'s 分隔的类型.由于其中一些类型本身可能是函数,因此可能存在带括号的子结构,这些子结构本身具有箭头.这是完全可以接受的:

// someFunc :: ((Boolean, Number) -> String) -> (Object -> Boolean) ->
//             (Object -> Number) -> Object -> String
Run Code Online (Sandbox Code Playgroud)

这是弥补的.我没有真正的功能指向这里.但是我们可以从它的类型签名中学到很多关于这样一个函数的知识.它接受三个函数和一个Object并返回一个String.它接受的第一个函数接受a Boolean和a Number并返回a String.注意,这里没有将其描述为curried函数(或者它将被写为(Boolean ? Number ? String).)第二个函数参数接受a Object并返回a Boolean,第三个函数接受a Object并返回a Number.

这比Ramda函数中的实际情况稍微复杂一些.我们通常没有四个参数的功能,我们当然没有任何接受三个功能参数的功能.所以,如果这一点很明确,我们就能很好地理解Ramda必须向我们抛出的任何东西.

[^咖喱降序]:对于人从其他语言,Ramda的钻营也许是有些不同的比你已经习惯了:如果f :: (A, B, C) ? Dg = curry(f),然后g(a)(b)(c) == g(a)(b, c) == g(a, b)(c) == g(a, b, c) == f(a, b, c).

类型变量

如果你曾经使用过map,你会知道它非常灵活:

map(word => word.toUpperCase(), ['foo', 'bar', 'baz']); //=> ["FOO", "BAR", "BAZ"]
map(word => word.length, ['Four', 'score', 'and', 'seven']); //=> [4, 5, 3, 5]
map(n => n * n, [1, 2, 3, 4, 5]); //=> [1, 4, 9, 16, 25]
map(n => n % 2 === 0, [8, 6, 7, 5, 3, 0, 9]); //=> [true, true, false, false, false, true, false]
Run Code Online (Sandbox Code Playgroud)

从这里,我们希望将以下所有类型的签名应用于地图:

// map :: (String -> String) -> [String] -> [String]
// map :: (String -> Number) -> [String] -> [Number]
// map :: (Number -> Number) -> [Number] -> [Number]
// map :: (Number -> Boolean) -> [Number] -> [Boolean]
Run Code Online (Sandbox Code Playgroud)

但显然还有更多的可能性.我们不能简单地列出所有这些.为了解决这个问题,类型签名不仅涉及具体的类,如Number,StringObject,也与泛型类的表示.

我们将如何描述map?这很简单.第一个参数是一个函数,它接受一种类型的元素,并返回第二种类型的元素.(这两种类型不必相同.)第二个参数是该函数的输入类型的元素列表.它返回该函数的输出类型的元素列表.

这是我们如何描述它:

// map :: (a -> b) -> [a] -> [b]
Run Code Online (Sandbox Code Playgroud)

我们使用通用占位符代替具体类型,单个低字符字母代表任意类型.

将这些与具体类型区分开来很容易.这些都是完整的词,按照惯例是大写的.泛型类型变量只是a,b,c,等偶尔,如果有一个强有力的理由,我们可能会使用从后来的一封信中,如果它有助于字母表使得通用可能代表什么各种类型某种意义上(想kv用于keyvaluen对于一个数字),但主要是我们只是从字母表的开头使用这些.

请注意,一旦在签名中使用泛型类型变量,它就表示为该同一变量的所有使用而固定的值.我们不能b在签名的一部分中使用,然后在其他地方重复使用它,除非两者必须在整个签名中具有相同的类型.而且,如果签名中的两个类型必须相同,那么我们必须为它们使用相同的变量.

但没有什么可说的,两个不同的变量有时不能指向相同的类型.map(n => n * n, [1, 2, 3]); //=> [1, 4, 9](Number ? Number) ? [Number] ? [Number]的,所以如果我们要匹配 (a ? b) ? [a] ? [b],那么这两个abNumber.这不是问题.我们仍然有两个不同的类型变量,因为它们会有不同的情况.

参数化类型

有些类型更复杂.我们可以很容易地想象出一个代表类似物品集合的类型,我们称之为Box.但没有任何实例是任意的Box; 每个人只能持有一种物品.当我们讨论一个时,Box我们总是需要指定Box一些东西.

// makeBox :: Number -> Number -> Number -> [a] -> Box a
const makeBox = curry((height, width, depth, items) => /* ... */);

// addItem :: a -> Box a -> Box a
const addItem = curry((item, box) => /* ... */);
Run Code Online (Sandbox Code Playgroud)

这就是我们如何指定Box由未知类型参数化的方法a: Box a.这可以在我们需要类型,参数或函数返回的任何地方使用.当然,我们也可以使用更具体的类型来参数化类型,Box Candy或者Box Rock.(虽然这是合法的,但我们现在实际上并没有在Ramda这样做.也许我们根本不想被指责像一盒石头一样愚蠢.)

不必只有一个类型参数.我们可能有一个 Dictionary类型,它通过键的类型和它使用的值的类型进行参数化.这可以写Dictionary k v.这也证明了我们可能使用的单个字母不是字母表中的初始字母.

在Ramda本身中没有很多这样的声明,但我们可能会发现自己经常在自定义代码中使用这些东西.这些的最大用途是支持类型类,因此我们应该描述它们.

键入别名

有时我们的类型失控,由于它们的内在复杂性或者因为它们过于通用而变得很难与它们一起工作.Haskell允许使用类型别名来简化对这些的理解.拉姆达也借用了这个概念,尽管它很少使用.

这个想法很简单.如果我们有一个参数化类型User String,其中String表示一个名称,并且我们想要更具体地说明生成URL时表示的String类型,我们可以创建一个类型别名,如下所示:

// toUrl :: User Name u => Url -> u -> Url
//     Name = String
//     Url = String
const toUrl = curry((base, user) => base +
user.name.toLowerCase().replace(/\W/g, '-'));
toUrl('http://example.com/users/', {name: 'Fred Flintstone', age: 24});
//=> 'http://example.com/users/fred-flintstone'
Run Code Online (Sandbox Code Playgroud)

别名NameUrl显示在" =" 的左侧.它们的等效值显示在右侧.

如上所述,这也可用于为更复杂的类型创建简单别名.Ramda中的许多函数都使用Lenses,并且使用类型别名简化了这些函数的类型:

//     Lens s a = Functor f => (a -> f a) -> s -> f s
Run Code Online (Sandbox Code Playgroud)

我们稍后会尝试分解这个复杂的值,但是现在,它应该足够清楚,无论Lens s a代表什么,它下面只是复杂表达式的别名,Functor f ? (a ? f a) ? s ? f s.

(第2部分在单独的答案中.)