我在哪里可以找到用于解释函数式编程的 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 个参数一样。
a
s 在这种情况下表示通用输入。我们不关心类型,因为可能从未使用过各个元素。如果一个字母出现在签名中而没有与类型类限制相关联(下面更多关于类型类),假设它意味着一个我们根本不关心类型的通用参数(比如在签名中添加 <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 a
是 a
(任何类型),但仅限于可以应用的类型。从上下文来看,我不得不假设函数是 Apply
类型类的隐式成员,因为它们可以被应用,并且上面的签名在函数参数 ((a -> b)
) 之前,带有 f
.
这部分:
(a -> b)
表示一个函数,它接受一个a
,并将它变成一个b
;但在任何一种情况下,我们都不关心 a
或 b
实际上是什么类型。因为它周围有括号,所以它表示正在传递的单个函数。任何时候你看到一个类似 (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]
此函数接受 Number
、val
和 Number
列表,
nbrs
,return 是 Number
的新列表。
重要的是要认识到这是 all 签名告诉我们的。
没办法区分这个函数,单靠签名,
来自恰好接受 Number
和列表的任何其他函数
Number
s 和 return Number
s 的列表。[^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) → D
和 g = 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]
但显然还有更多的可能性。我们不能简单地列出
商场。为了解决这个问题,类型签名不仅处理具体的
类 例如 Number
、String
和 Object
,但也有
泛型 类.
的表示
我们如何描述 map
?这很简单。第一个参数是
一个函数,它接受一种类型的元素,并且 returns 是一种类型的元素
第二种。 (这两种类型不必不同。)
第二个参数是输入类型的元素列表
功能。它 returns 输出类型的元素列表
功能。
我们可以这样描述它:
// map :: (a -> b) -> [a] -> [b]
我们使用通用占位符代替具体类型,单个
lower-character 代表任意类型的字母。
很容易将它们与具体类型区分开来。那些是
完整的词,并且按照惯例是大写的。通用类型变量
只是 a
、b
、c
等。偶尔,如果有充分的理由,
如果有帮助,我们可能会使用字母表中后面的字母
了解泛型可能代表的类型(想想 k
和
v
用于 key
和 value
或 n
用于数字),但大多数情况下我们只是使用
这些是字母表开头的。
请注意,一旦在签名中使用泛型类型变量,它
表示对于同一变量的所有使用都是固定的值。我们
不能在签名的一部分使用 b
然后在其他地方重复使用
除非两者在整个签名中必须属于同一类型。
此外,如果签名中的两个类型必须相同,那么我们有
使用相同的他们的变量。
不过没什么好说的,两个不同的变量有时不能
指向相同的类型。 map(n => n * n, [1, 2, 3]); //=> [1, 4, 9]
是
(Number → Number) → [Number] → [Number]
,所以如果我们要匹配
(a → b) → [a] → [b]
,那么a
和b
都指向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 Candy
或 Box 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'
别名 Name
和 Url
出现在“=
”的左侧。他们的
等效值显示在右侧。
如前所述,这也可以用来创建一个简单的别名
复杂类型。 Ramda 中的许多函数使用 Lens
es,并且
这些类型通过使用类型别名得到简化:
// 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
函数
可以在 Numbers
、Strings
、Dates
上运行,但不能在
任意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 从抽象中借用的代数类型
数学,被称为Functor
。 Functor
只是一种类型
包含一个 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
不详述它的作用或使用方式 Monoid
或
Ord
,我们至少可以看到需要提供什么样的类型
为了使该功能正常运行。
[^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 可能可以替换为通用类型名称,例如 a
或
b
。这种变化随时可能发生。
简单对象
我们可以选择几种方式来表示普通 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
由两个参数化
通用变量 s
和 a
。我们知道有一个约束
Lens
中使用的 f
变量的类型:它必须是 Functor
。
考虑到这一点,我们看到 Lens
是两个的柯里化函数
参数,第一个是泛型类型值的函数
a
到参数化类型之一 f a
,第二个是值
泛型 s
。结果是参数化类型 f
s
的值。 但是它做什么?我们不知道。我们无法知道。我们的类型
签名告诉我们很多关于函数的信息,但他们没有回答
关于函数实际做什么的问题。我们可以假设
在某处必须调用 f a
的 map
方法,因为那是
只有 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”指令。我们可以通过形状识别
我们的功能究竟是如何将它们组合在一起构建的
更大的功能。能够做到这一点是关键功能之一
函数式编程。类型签名使它更容易
这样做。
进一步阅读
- Chapter 7 of Frisby 教授的最适当指南
函数式编程 也深入探讨了这些
签名,重点有所不同。此外,
整本书都值得一读。
- Daniel Spiewak 的文章 什么是 Hindley-Milner? (为什么会这样
酷吗?) 很好地为外行解释了一点
关于构成这些签名的类型系统。
- Norman Ramsey 的 Whosebug answer 涵盖了相同的内容
像 Daniel Spiewak 一样扎根,但表现得非常短暂。
- Philip Wadler 的开创性论文,Theorems for Free 描述了
关于一个函数,我们如何能学到比看起来明显更多的东西
仅来自其类型签名。
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 个参数一样。
a
s 在这种情况下表示通用输入。我们不关心类型,因为可能从未使用过各个元素。如果一个字母出现在签名中而没有与类型类限制相关联(下面更多关于类型类),假设它意味着一个我们根本不关心类型的通用参数(比如在签名中添加 <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 a
是 a
(任何类型),但仅限于可以应用的类型。从上下文来看,我不得不假设函数是 Apply
类型类的隐式成员,因为它们可以被应用,并且上面的签名在函数参数 ((a -> b)
) 之前,带有 f
.
这部分:
(a -> b)
表示一个函数,它接受一个a
,并将它变成一个b
;但在任何一种情况下,我们都不关心 a
或 b
实际上是什么类型。因为它周围有括号,所以它表示正在传递的单个函数。任何时候你看到一个类似 (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]
此函数接受 Number
、val
和 Number
列表,
nbrs
,return 是 Number
的新列表。
重要的是要认识到这是 all 签名告诉我们的。
没办法区分这个函数,单靠签名,
来自恰好接受 Number
和列表的任何其他函数
Number
s 和 return Number
s 的列表。[^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) → D
和 g = 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]
但显然还有更多的可能性。我们不能简单地列出
商场。为了解决这个问题,类型签名不仅处理具体的
类 例如 Number
、String
和 Object
,但也有
泛型 类.
我们如何描述 map
?这很简单。第一个参数是
一个函数,它接受一种类型的元素,并且 returns 是一种类型的元素
第二种。 (这两种类型不必不同。)
第二个参数是输入类型的元素列表
功能。它 returns 输出类型的元素列表
功能。
我们可以这样描述它:
// map :: (a -> b) -> [a] -> [b]
我们使用通用占位符代替具体类型,单个 lower-character 代表任意类型的字母。
很容易将它们与具体类型区分开来。那些是
完整的词,并且按照惯例是大写的。通用类型变量
只是 a
、b
、c
等。偶尔,如果有充分的理由,
如果有帮助,我们可能会使用字母表中后面的字母
了解泛型可能代表的类型(想想 k
和
v
用于 key
和 value
或 n
用于数字),但大多数情况下我们只是使用
这些是字母表开头的。
请注意,一旦在签名中使用泛型类型变量,它
表示对于同一变量的所有使用都是固定的值。我们
不能在签名的一部分使用 b
然后在其他地方重复使用
除非两者在整个签名中必须属于同一类型。
此外,如果签名中的两个类型必须相同,那么我们有
使用相同的他们的变量。
不过没什么好说的,两个不同的变量有时不能
指向相同的类型。 map(n => n * n, [1, 2, 3]); //=> [1, 4, 9]
是
(Number → Number) → [Number] → [Number]
,所以如果我们要匹配
(a → b) → [a] → [b]
,那么a
和b
都指向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 Candy
或 Box 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'
别名 Name
和 Url
出现在“=
”的左侧。他们的
等效值显示在右侧。
如前所述,这也可以用来创建一个简单的别名
复杂类型。 Ramda 中的许多函数使用 Lens
es,并且
这些类型通过使用类型别名得到简化:
// Lens s a = Functor f => (a -> f a) -> s -> f s
稍后我们会尝试分解这个复杂的值,但现在,
应该很清楚,无论 Lens s a
代表什么,
在它下面只是复杂表达式 Functor
f ⇒ (a → f a) → s → f s
.
(
来自 Ramda Wiki:
(第 2 部分 / 2 -- 对于单个 SO 答案来说太长了!)
类型约束
有时我们想限制我们可以在
以某种方式签名。我们可能想要一个 maximum
函数
可以在 Numbers
、Strings
、Dates
上运行,但不能在
任意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 从抽象中借用的代数类型
数学,被称为Functor
。 Functor
只是一种类型
包含一个 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
不详述它的作用或使用方式 Monoid
或
Ord
,我们至少可以看到需要提供什么样的类型
为了使该功能正常运行。
[^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 可能可以替换为通用类型名称,例如 a
或
b
。这种变化随时可能发生。
简单对象
我们可以选择几种方式来表示普通 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
由两个参数化
通用变量 s
和 a
。我们知道有一个约束
Lens
中使用的 f
变量的类型:它必须是 Functor
。
考虑到这一点,我们看到 Lens
是两个的柯里化函数
参数,第一个是泛型类型值的函数
a
到参数化类型之一 f a
,第二个是值
泛型 s
。结果是参数化类型 f
s
的值。 但是它做什么?我们不知道。我们无法知道。我们的类型
签名告诉我们很多关于函数的信息,但他们没有回答
关于函数实际做什么的问题。我们可以假设
在某处必须调用 f a
的 map
方法,因为那是
只有 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”指令。我们可以通过形状识别 我们的功能究竟是如何将它们组合在一起构建的 更大的功能。能够做到这一点是关键功能之一 函数式编程。类型签名使它更容易 这样做。
进一步阅读
- Chapter 7 of Frisby 教授的最适当指南 函数式编程 也深入探讨了这些 签名,重点有所不同。此外, 整本书都值得一读。
- Daniel Spiewak 的文章 什么是 Hindley-Milner? (为什么会这样 酷吗?) 很好地为外行解释了一点 关于构成这些签名的类型系统。
- Norman Ramsey 的 Whosebug answer 涵盖了相同的内容 像 Daniel Spiewak 一样扎根,但表现得非常短暂。
- Philip Wadler 的开创性论文,Theorems for Free 描述了 关于一个函数,我们如何能学到比看起来明显更多的东西 仅来自其类型签名。