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

Where can I find an explanation/summary of symbols used to explain functional programming, specifically 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)

我目前能够理解 Ramda.js 试图做的大部分事情,而且我经常可以有根据地猜测上述陈述的意思。但是我敢肯定,如果我能更好地理解这些 symbols/statements,我会更容易理解。我想了解各个组件的含义(例如特定字母、关键字、不同的箭头类型、标点符号等)。我也想知道如何 "read" 这些行。

我用谷歌搜索或搜索 StackExchange 都没有成功。我使用了 "Ramda"、"functional programming"、"symbols"、"abbreviations"、"shorthand" 等的各种组合。我也不确定我是否在寻找因为(A)在更广泛的函数式编程领域(或者甚至可能只是一般编程)中普遍使用的缩写,或者(B)Ramda 作者正在使用的专门语法(或者可能从其他地方增选但进一步修改)只是对于他们的图书馆。

这是一些函数式语言(最著名的是 Haskell)用于其类型签名的语法。

最后一个符号代表return类型,其余代表参数类型。看似奇怪的语法的原因与 Haskell 被柯里化的事实有关;所有函数都采用 1 个参数和 return 一个值。多参数函数由 return 个新函数组成。任何时候你看到一个 ->,那就是函数应用程序。您可以将箭头视为接受 1 个输入并给出 1 个输出的 "black box"。这就是我刚开始时的形象 Haskell.

例如:

Number -> [a] -> [[a]]

是一个函数的签名,它接受一个数字和一个通用 a 列表,而 return 是一个 a 的二维列表。请注意,在 Haskell 中,这将表示一个接受 Number 的函数,而 return 是一个接受 a 列表的函数,而 return 是一个a 的二维列表。不过,您通常不需要担心柯里化行为。您可以调用该函数,就像它实际上有 2 个参数一样。

as 在这种情况下表示通用输入。我们不关心类型,因为可能从未使用过各个元素。如果一个字母出现在签名中而没有与类型类限制相关联(下面更多关于类型类),假设它意味着一个我们根本不关心类型的通用参数(比如在签名中添加 <T>在 Java 中,然后使用 T).

Apply f => f (a -> b) -> f a -> f b

是一个函数的签名,它接受一个函数和一个 a,并返回一个 b。它似乎是一种通用的 map 方法。如果列表是 Apply 类型类的成员,您可以认为 a 在这种情况下可能是一个列表,而 b 是该列表的修改版本。

在第二个例子中,"thick arrow"之前的部分表示类型限制。 Apply f 意味着在签名的其余部分中,f 表示属于 Apply 类型类(类似于接口)的成员的类型。据推测,Apply 类型类表示能够应用的类型,因此 f aa(任何类型),但仅限于可以应用的类型。从上下文来看,我不得不假设函数是 Apply 类型类的隐式成员,因为它们可以被应用,并且上面的签名在函数参数 ((a -> b)) 之前,带有 f.

这部分:

(a -> b)

表示一个函数,它接受一个a,并将它变成一个b;但在任何一种情况下,我们都不关心 ab 实际上是什么类型。因为它周围有括号,所以它表示正在传递的单个函数。任何时候你看到一个类似 (a -> b) 的签名,这意味着它是一个高阶函数的签名。

推荐阅读:

Understanding Haskell Type Signatures

来自 Ramda Wiki:

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


类型签名

(或"What are all those funny arrows about?")

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

Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s

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

最后,一旦我们了解如何这些工作,我们将调查 为什么 人们会想要它们。

命名类型

许多ML-influenced languages, including Haskell,使用一个 描述其函数签名的标准方法。作为 函数式编程在 Javascript 中变得更加普遍,这种风格 签名正慢慢成为标准。我们借用并改编 Haskell Ramda 版本。

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

// length :: String -> Number
const length = word => word.length;
length('abcde'); //=> 5

这里我们有一个简单的函数,length,它接受一个词,类型 String 和 returns 字符串中的字符数,这是一个 Number。函数上方的注释是签名行。开始 加上函数名,然后是分隔符“::”,然后是 功能的实际描述。应该相当清楚 该描述的语法是。提供函数的输入, 然后是箭头,然后是输出。你通常会看到写着的箭头 如上,在源代码中为“->”,在输出中为“” 文档。他们的意思完全一样。

我们在箭头前后放置的是 Types 参数,而不是它们的名称。在这个级别的描述中,我们真正 已经说过,这是一个接受字符串和 returns 的函数 数.

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

在这个函数中,函数接受两个参数,一个位置 -- 这是 一个 Number -- 和一个词 -- 这是一个 String -- 它 return 是一个 single-character String 或空 String.

在Javascript中,不像在Haskell中,函数可以接受多个 范围。为了显示需要两个参数的函数,我们分开 两个带逗号的输入参数并将组括在括号中: (Number, String)。与许多语言一样,Javascript 函数 参数是位置性的,因此顺序很重要。 (String, Number) 有 完全不同的意思。

当然对于一个带三个参数的函数,我们只要扩展 comma-separated括号内列表:

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

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

注意 ES6 风格的箭头之间的平行关系可能会有启发意义 函数定义和这些类型声明。函数定义 通过

(pos, word) => word.charAt(pos);

通过将参数名称替换为它们的类型,将正文替换为 type 的值 returns 和粗箭头,“=>”,还有一个细箭头, "->",我们得到签名:

// (Number, String) -> String

值列表

我们经常使用相同类型的值列表。要是我们 想要一个函数来添加列表中的所有数字,我们可以使用:

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

此函数的输入是 List,共 Number 个。有一个单独的 讨论恰好 what we mean by Lists,但现在,我们可以 从本质上将其视为数组。描述一个列表 对于给定的类型,我们将该类型名称括在方括号中,“[ ]”。一个列表 String 的列表将是 [String]Boolean 的列表将是 [Boolean]Number 列表 的列表将是 [[Number]]

这样的列表当然也可以是函数的 return 值:

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

当我们意识到我们可以结合这些时,我们应该不会感到惊讶:

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

此函数接受 NumbervalNumber 列表, nbrs,return 是 Number 的新列表。

重要的是要认识到这是 all 签名告诉我们的。 没办法区分这个函数,单靠签名, 来自恰好接受 Number 和列表的任何其他函数 Numbers 和 return Numbers 的列表。[^theorems]

[^theorems]: 好吧,我们还可以收集其他信息,在 free theorems 签名暗示的形式。

函数

还有一种非常重要的类型我们还没有真正讨论过。 函数式编程就是关于函数的;我们将函数传递为 参数和接收函数作为来自其他的 return 值 职能。我们也需要代表这些。

事实上,我们已经了解了如何表示函数。每一个签名 行记录了一个特定的功能。我们重用上面的技术 我们签名中使用的 higher-order 函数的小.

// 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]

这里函数calc描述为(Number → Number)是 就像我们的 top-level 函数签名一样,只是包裹在 括号将其正确地分组为一个单独的单元。我们可以做到 从另一个函数 returned 得到的函数也一样:

// 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

makeTaxCalculator 接受以百分比表示的税率(类型 Number 和 return 是一个新函数,它本身接受一个 Number return 是 Number。同样,我们描述函数 returned by (Number → Number),使得整个函数的签名 Number → (Number → Number).

柯里化

使用 Ramda,我们可能不会准确地写出 makeTaxCalculator 像那样。柯里化是 Ramda 的核心,我们可能会 在这里发挥它的优势。[^curry-desc]

相反,在 Ramda 中,人们很可能会写一个咖喱 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

可以通过提供两个参数来使用这个柯里化函数 前面并返回一个值,或者只提供一个值并获取 返回正在寻找第二个函数的函数。为此我们使用 Number → Number → Number。在Haskell中,歧义得到解决 很简单:箭头向右绑定,所有函数都采用 单个参数,尽管有一些语法技巧 让它感觉好像你可以用多个参数调用它们。

在 Ramda 中,直到我们调用函数才解决歧义。什么时候 我们调用 calculateTax(6.35),因为我们选择不提供 第二个参数,我们得到最后的 Number → Number 部分 签名。当我们调用 calculateTax(8.875, 49.95) 时,我们提供了 前两个 Number 参数,所以只取回最后一个 Number.

柯里化函数的签名总是这样的,一系列 由“”分隔的类型。因为其中一些类型可能 本身就是函数,可能有带括号的子结构 它们本身有箭头。这是完全可以接受的:

// someFunc :: ((Boolean, Number) -> String) -> (Object -> Boolean) ->
//             (Object -> Number) -> Object -> String

这是编造的。我没有真正的功能可以指向这里。但是我们 可以从其类型签名中了解有关此类功能的一些信息。它 接受三个函数和一个 Object 和 return 一个 String。这 它接受的第一个函数本身接受一个 Boolean 和一个 Number 和 return 一个 String。请注意,这里没有将其描述为咖喱 函数(或者它会写成 (Boolean → Number → String)。)第二个函数参数接受 Object 和 returns a Boolean,第三个接受 Object 和 returns a Number.

这只比 Ramda 函数中的实际情况稍微复杂一点。 我们不经常有四个参数的函数,我们当然也没有 有任何接受三个函数参数的。所以如果这个很清楚, 我们正在努力理解 Ramda 必须投入的任何东西 我们.

[^curry-desc]:对于来自其他语言的人,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]

据此,我们希望将以下所有类型签名应用于 地图:

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

但显然还有更多的可能性。我们不能简单地列出 商场。为了解决这个问题,类型签名不仅处理具体的 类 例如 NumberStringObject,但也有 泛型 类.

的表示

我们如何描述 map?这很简单。第一个参数是 一个函数,它接受一种类型的元素,并且 returns 是一种类型的元素 第二种。 (这两种类型不必不同。) 第二个参数是输入类型的元素列表 功能。它 returns 输出类型的元素列表 功能。

我们可以这样描述它:

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

我们使用通用占位符代替具体类型,单个 lower-character 代表任意类型的字母。

很容易将它们与具体类型区分开来。那些是 完整的词,并且按照惯例是大写的。通用类型变量 只是 abc 等。偶尔,如果有充分的理由, 如果有帮助,我们可能会使用字母表中后面的字母 了解泛型可能代表的类型(想想 kv 用于 keyvaluen 用于数字),但大多数情况下我们只是使用 这些是字母表开头的。

请注意,一旦在签名中使用泛型类型变量,它 表示对于同一变量的所有使用都是固定的值。我们 不能在签名的一部分使用 b 然后在其他地方重复使用 除非两者在整个签名中必须属于同一类型。 此外,如果签名中的两个类型必须相同,那么我们有 使用相同的他们的变量。

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

参数化类型

有些类型更复杂。我们可以很容易地想象一个类型代表一个 类似项目的集合,我们称之为 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) => /* ... */);

这就是我们指定由未知类型 a 参数化的 Box 的方式: Box a。这可以在我们需要类型的任何地方使用,作为参数或作为 一个函数的return。当然我们可以参数化类型 还有一个更具体的类型,Box CandyBox Rock。 (虽然这 是合法的,目前我们实际上并没有在 Ramda 中这样做。也许 我们只是不想被指责像一盒石头一样愚蠢。)

不必只有一个类型参数。我们可能有一个 Dictionary 在两种键类型上参数化的类型 以及它使用的值的类型。这可以写成Dictionary k v。这也演示了我们可能会使用 single 的地方 不是字母表中首字母的字母。

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

键入别名

有时我们的类型会失控,工作变得很困难 因为他们内在的复杂性或者因为他们太 通用的。 Haskell 允许 键入别名 以简化理解 这些。 Ramda 也借用了这个概念,尽管它被使用 节俭地。

这个想法很简单。如果我们有一个参数化类型 User String,其中 String 本来是用来代表一个名字的,我们想要更多 特定于生成时表示的字符串类型 URL,我们可以像这样创建一个类型别名:

// 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'

别名 NameUrl 出现在“=”的左侧。他们的 等效值显示在右侧。

如前所述,这也可以用来创建一个简单的别名 复杂类型。 Ramda 中的许多函数使用 Lenses,并且 这些类型通过使用类型别名得到简化:

//     Lens s a = Functor f => (a -> f a) -> s -> f s

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

的别名

中的第 2 部分。)

来自 Ramda Wiki:

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


类型约束

有时我们想限制我们可以在 以某种方式签名。我们可能想要一个 maximum 函数 可以在 NumbersStringsDates 上运行,但不能在 任意Objects。我们想要描述 ordered 类型,其中 a < b 总是 return 一个有意义的结果。我们讨论细节 类型 Ord in the Types section;为了我们的目的,它 足以说明它是为了捕获那些具有 一些适用于 <.

的排序操作
// 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")

这个描述[^maximum-note]在约束部分 开始,用右双箭头(“=>”与其余部分分开 代码,有时在其他文档中为“”。)Ord a ⇒ [a] → a 说 maximum 接受某种类型的元素的集合,但是 类型必须遵守 Ord.

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

Ramda 函数最常见的类型约束是那些指定的 通过 Javascript FantasyLand specification.

之前讨论map函数时,我们只讨论了映射 值列表上的函数。但是映射的想法更多 比一般。它可以用来描述一个应用程序 对任何包含 a 的一些值的数据结构的函数 某种类型,如果它 return 是另一种具有新形状的相同形状的结构 其中的价值观。我们可能会映射一个 Tree、一个 Dictionary、一个普通的 Wrapper 仅包含单个值或许多其他类型。

可以映射的事物的概念由 其他语言和 FantasyLand 从抽象中借用的代数类型 数学,被称为FunctorFunctor 只是一种类型 包含一个 map 受一些简单法则约束的方法。 Ramda 的 map 函数将在我们的类型上调用 map 方法,假设如果我们 没有通过列表(或 Ramda 已知的其他类型)但确实通过了一些东西 上面有 map,我们希望它表现得像 Functor

为了在签名中对此进行描述,我们在 签名块:

// map :: Functor f => (a -> b) -> f a -> f b

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

// weirdFunc :: (Functor f, Monoid b, Ord b) => (a -> b) -> f a -> f b

不详述它的作用或使用方式 MonoidOrd,我们至少可以看到需要提供什么样的类型 为了使该功能正常运行。

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

[^strong-types]: 有一些非常好的工具可以解决这个问题 Javascript 的缺点,包括语言技巧,例如 Ramda 的姊妹项目 Sanctuary,Javascript 的扩展 更强类型,例如 flow and TypeScript,以及 编译为 Javascript 的强类型语言,例如 ClojureScript, Elm, and PureScript.

多重签名

有时候,而不是试图找到一个最通用的版本 签名,列出几个相关的签名更直接 分别地。它们作为两个独立的部分包含在 Ramda 源代码中 JSDoc 标签,并在文档中以截然不同的两行结束。这个 是我们如何在自己的代码中编写一个:

// 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

显然,如果我们愿意,我们可以做两个以上的签名。但是做 请注意,这不应该太常见。目标是写签名 足够通用以捕获我们的用法,而不会抽象到 他们实际上掩盖了函数的用法。如果我们可以这样做 单一签名,我们可能应该。如果需要两个,那就这样吧。 但是如果我们有一长串签名,那么我们可能会遗漏一个 通用抽象。

Ramda 杂记

可变函数

将此样式签名从 Haskell 到 Javascript。 Ramda 团队已经 临时 解决了这些问题 基础,这些解决方案仍然是 subject to change.

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

// 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'

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

[^flip-example]: 这不是 Ramda 的实际代码,它交易了一个 显着提高性能的简单性。

Any / *类型

我们 hoping to change this 很快,但是 Ramda 的类型签名 通常包含星号 (*) 或 Any 合成类型。这是 只是一种报告方式,尽管有一个参数或 return 在这里,我们无法推断出它的实际类型。我们来到了 意识到只有一个地方这仍然有意义, 这是当我们有一个类型可能不同的元素列表时。那时 点,我们可能应该报告 [Any]。任意的所有其他用途 type 可能可以替换为通用类型名称,例如 ab。这种变化随时可能发生。

简单对象

我们可以选择几种方式来表示普通 Javascript 对象。显然我们可以直接说 Object,但有时 似乎还需要别的东西。当一个对象被用作 类似类型值的字典(相对于它作为 Record), 那么key和value的类型就可以变成 相关的。在某些签名中,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]

而且,一如既往,这些可以用作函数调用的结果 相反:

// 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}

记录

虽然这可能与 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."

记录符号看起来很像对象字面量,其值为 字段替换为它们的类型。我们只考虑字段名称 这在某种程度上与我们相关。 (在上面的例子中,即使 我们的数据有一个 'occupation' 字段,它不在我们的签名中,因为 不能直接使用

复杂示例:over

所以在这一点上,我们应该有足够的信息来理解 over 函数的签名:

Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s

我们从类型别名开始,Lens s a = Functor f ⇒ (a → f a) → s → f s。这告诉我们类型 Lens 由两个参数化 通用变量 sa。我们知道有一个约束 Lens 中使用的 f 变量的类型:它必须是 Functor。 考虑到这一点,我们看到 Lens 是两个的柯里化函数 参数,第一个是泛型​​类型值的函数 a 到参数化类型之一 f a,第二个是值 泛型 s。结果是参数化类型 f s 的值。 但是它做什么我们不知道。我们无法知道。我们的类型 签名告诉我们很多关于函数的信息,但他们没有回答 关于函数实际做什么的问题。我们可以假设 在某处必须调用 f amap 方法,因为那是 只有 Functor 类型定义的函数,但我们不知道如何或 为什么调用 map。不过,我们知道 Lens 是一个函数 描述,我们可以用它来指导我们对 over.

的理解

函数 over 被描述为三个函数的柯里化函数 parameters, a Lens a s 刚刚分析过的,一个来自泛型的函数 类型 a 到相同的类型,以及泛型类型的值 s。这 整个 return 是一个 s.

类型的值

我们可以更深入地挖掘一下,也许可以进一步推论 over 必须如何处理它接收到的类型。有显着 研究所谓的自由定理证明不变量 仅可从类型签名派生。但是这份文件已经很远了 太长。有兴趣的请看延伸阅读

但是为什么呢?

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

有几个很好的理由。首先,一旦我们习惯了 他们,我们可以从一行中获得很多关于函数的洞察力 的元数据,没有 names 的干扰。名字听起来不错 想法,直到你意识到别人选择的名字不是 你会选择的名字。上面我们讨论了称为 “maximum”和“makeObj”。知道在 Ramda,等价的函数叫做“max”和“fromPairs”? 参数名称明显更糟。当然还有 通常还要考虑语言障碍。即使英语变成了 Web 的通用语,有些人不会理解 我们关于这些功能的优美、优雅的散文。但是 none 与签名有关的事项;他们简明扼要地表达了一切 除了它实际 做的事情 .

之外,一个函数很重要

但比这更重要的是这些签名使它成为现实 非常容易思考我们的功能以及它们如何组合。如果 我们得到了这个功能:

foo :: Object -> Number

map,我们已经看到了

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

那么我们可以直接推导出函数的类型map(foo) 请注意,如果我们用 Object 代替 a 并用 Number 代替 b,我们满足第一个参数的签名给map,并且 因此通过柯里化我们将剩下剩余的:

map(foo) :: [Object] -> [Number]

这使得使用函数有点像众所周知的“插入 Tab A into Slot A”指令。我们可以通过形状识别 我们的功能究竟是如何将它们组合在一起构建的 更大的功能。能够做到这一点是关键功能之一 函数式编程。类型签名使它更容易 这样做。

进一步阅读