我在哪里可以找到用于解释函数式编程的符号的解释/概要,特别是Ramda.js? [英] Where can I find an explanation/summary of symbols used to explain functional programming, specifically Ramda.js?

查看:111
本文介绍了我在哪里可以找到用于解释函数式编程的符号的解释/概要,特别是Ramda.js?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

JavaScript函数式编程库Ramda.js的API文档包含符号缩写,但没有提供理解这些缩写的图例。是否有一个地方(网站,文章,cheatsheet等),我可以去破译这些?



来自Ramda.js API文档的一些例子: p>

 数字 - >数字 - > Number 
Apply f => f(a - > b) - > f a - > f b
数字 - > [a] - > [[a]]
(* ... - > a) - > [*] - > a(b,b,...,m)→v)}→> ((a,b,...,m) - > {k:v})
可过滤的f => (a - >布尔) - > 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(ta)

我目前能够理解Ramda.js试图做的事情,而且我经常可以做出有教育意义的猜测,就像上面所说的那样。但是,如果我更好地理解这些符号/陈述,我确信我会更容易理解。我想了解各个组件的含义(例如特定字母,关键字,不同箭头类型,标点符号等)。我也想知道如何读取这些行。



我还没有成功搜索这个或搜索StackExchange。我已经使用了拉姆达,函数式编程,符号,缩写,速记等各种组合。我也不完全确定我是否正在寻找(A)普遍使用的缩写(或者甚至只是普通的编程),或者(B)拉姆达作者正在使用的专用语法(或者可能从其他地方合作,但进一步修改),仅仅是为了他们的图书馆。

解决方案

来自Ramda Wiki



(Part 1/2 - 对于单个SO回答太长!)




类型签名



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



查看Ramda的 over 函数,我们看到
的第一件事是看起来像是两条线e:

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

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



最后,一次我们了解如何 这些工作,我们将调查
为什么人们会希望他们。
$ b 命名类型

许多 ML 受影响的语言(包括 Haskell )使用
标准方法描述其函数的签名。由于
函数式编程在Javascript中变得越来越常见,这种
签名的风格正在慢慢地变得几乎标准化。我们借用并适应了拉姆达的
Haskell版本。



我们不会尝试创建一个正式的描述,而只是捕捉到
的精髓这些签名通过例子。

  //长度::字符串 - > Number 
const length = word => word.length;
长度('abcde'); // => 5

这里有一个简单的函数, length ,它接受一个类型为
String 的单词,并返回字符串中的字符数,即
。该函数上面的注释是一个签名行。它使用函数的名称,然后是分隔符 :: 开始
,然后是函数的
实际描述。应该很清楚该描述的
语法是什么。提供函数的输入,
然后是箭头,然后是输出。在源代码中,您通常会看到上面写有
的箭头, - > ,并且会显示为输出
文档。他们的意思完全一样。



我们在箭头之前和之后放置的是
参数的类型,而不是它们的名。在这个级别的描述中,我们真正的
所说的是,这是一个函数,它接受一个字符串并返回一个
数字。

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

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



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



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

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

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



注意ES6样式箭头
函数定义与这些类型声明之间的平行可能是有益的。该函数被
定义为

$ p $ (pos,word)=> word.charAt(POS);

通过将参数名称替换为它们的类型,使用
类型 - > ,以及一个瘦身的,
- > ; ,我们得到签名:

  //(Number,String) - >字符串



值列表



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

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

该函数的输入是<$ c $的 List C>数秒。有关于精确我们所列举的意思的单独
讨论,但现在,我们可以将
看作基本上就好像它们是阵列一样。要描述一个给定类型的List
,我们用方括号 [] 包装这个类型名称。 字符串的清单
应该是 [字符串] 布尔 s将是
[布尔值] 列表列表数字 s会是 [[数字]]



这些列表可以是当然也有函数返回值:

  // findWords :: String  - > [String] 
const findWords = sentence => sentence.split(/ \s + /);
findWords('她在海边出售贝壳');
// => [She,sells,seashells,by,the,seashore]

我们不应该意识到我们可以合并这些:

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

这个函数接受一个 Number val 以及数字 s,
nbrs ,并返回一个新列表 Number s。



这很重要意识到这是所有签名告诉我们。
没有办法区分这个函数,仅仅通过签名来区分
和其他碰巧接受 Number 的函数和
Number s并返回一个 Number s的列表。[^ theorems]



[^定理]:那么我们可以收集其他信息,以
的形式出现在

函数



还有一个非常重要的类型,我们没有真正讨论过。
函数式编程全是关于函数的;我们将函数作为
参数传递,并接收函数作为其他
函数的返回值。我们也需要表示这些。



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

  // applyCalculation ::((数字 - >数字),[数字]) - > [数字] 
const applyCalculation =(calc,nbrs)=> nbrs.map(nbr => calc(nbr));
applyCalculation(n => 3 * n + 1,[1,2,3,4]); // => [4,7,10,13]

这里函数 calc (数字→数字)描述
就像我们的顶级函数签名一样,只是用
括号包裹把它作为一个单独的单元进行合理分组我们可以使用从另一个函数返回的函数完成
同样的事情:

  // makeTaxCalculator :: Number  - > ; (Number  - > Number)
const makeTaxCalculator = rate => base =>
Math.round(100 * base + base * rate)/ 100;
const afterSalesTax = makeTaxCalculator(6.35); //税率:6.35%
售后税(152.83); // => 162.53

makeTaxCalculator 接受税率,表示作为百分比(类型
Number ),并返回一个新函数,它自己接受一个 Number
并返回一个 Number 。同样,我们描述由
(Number→Number)返回的函数 ,这使得整个函数
Number→(Number→Number)的签名。



Currying



使用Ramda,我们可能不会像这样编写一个 makeTaxCalculator 完全是
。拉姆达,我们可能会在这里获得
的优势。[^ curry-desc]



相反,在拉姆达,人们很可能会写一个咖喱 calculateTax
函数可以像 makeTaxCalculator 一样使用,如果这是
您想要的,但可以也可用于单次传递:

  // calculateTax :: Number  - >麻木呃 - > Number 
const calculateTax = R.curry((rate,base)=>
Math.round(100 * base + base * rate)/ 100);
const afterSalesTax = calculateTax(6.35); //税率:6.35%
售后税(152.83); // => 162.53
// OR
calculateTax(8.875,49.95); // => 54.38

这个curried函数可以通过提供
front和back两个参数来使用一个值,或者只提供一个值并且获得
返回正在寻找第二个函数的函数。为此,我们使用
数字→数字→数字。在Haskell中,模糊性很容易被解析为
:箭头绑定到右边,所有函数都使用
单个参数,尽管手边有一些句法花招可以使
感觉为尽管你可以用多个参数调用它们。



在Ramda中,在我们调用函数之前,模糊不会被解决。当
时,我们调用 calculateTax(6.35),因为我们选择不提供
第二个参数,所以我们返回最后的数字→数字
签名的一部分。当我们调用 calculateTax(8.875,49.95)时,我们提供了
前两个 Number 参数,并且因此只返回最后的
Number



curried函数的签名总是像这样,由$ 分隔的一系列
类型。因为这些类型中的一些可能
本身就是函数,所以可能会有一些带圆括号的子结构
,它们本身就有箭头。这是完全可以接受的:

  // someFunc ::((Boolean,Number) - > String) - > (对象 - >布尔) - > 
//(Object - > Number) - >对象 - >字符串

由此组成。我没有一个真正的功能指向这里。但是我们
可以从它的类型签名中学到一些关于这种功能的公平信息。它
接受三个函数和一个 Object 并返回一个 String 。它自己接受的
first函数需要一个布尔值和一个数字
返回一个字符串。请注意,这里没有描述这是一个curried
函数(或者它被写为(布尔→数字→
字符串)
。)第二个函数参数接受一个 Object 并返回
a Boolean ,第三个函数接受一个 Object 并返回一个 Number



这只比一点点复杂拉姆达功能是现实的。
我们通常不具有四个参数的函数,并且我们当然不会有
接受三个函数参数。所以如果这一点很清楚的话,那么我们就可以很好地理解拉姆达必须抛出的
美元。



[^咖喱-desc]:对于来自其他语言的人,拉姆达的
currying可能与以前不同:如果 f ::
(A,B,C)→ (a)(b)(c)== g(c)(c)(c)和(c $ c $) a)(b,c)==
g(a,b)(c)== g(a,b,c)== f(a,b,c)
/ p>

类型变量



如果您使用过 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]); // => [
$ / pre

从这个,我们想要将所有以下类型的签名应用于
map:

  // map ::(String  - > String) - > [字符串]  - > [String] 
// map ::(String - > Number) - > [字符串] - > [Number]
// map ::(Number - > Number) - > [数量] - > [数字]
// map ::(Number - > Boolean) - > [数量] - > [布尔]

但显然还有更多的可能性。我们不能简单地列出所有的
。为了解决这个问题,输入签名不仅要处理具体的
类,如 Number String ,并且 Object ,而且还包含泛型类的
表示形式。



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



这是我们如何描述它的:

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

除了具体的类型,我们使用泛型占位符,单个
lower-字符代表任意类型。


很容易将这些与具体类型区分开来。这些是b $ b全文,按惯例是大写。通用类型变量
只是 a b c 等。偶尔,如果有强有力的理由,
,我们可能会使用字母表后面的一个字母,如果它有助于使
感知泛型可能代表什么类型的类型(认为 k
v 用于 value n 为一个数字),但大多数情况下我们只是使用
这些从字母开始。

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



但是没有什么可以说两个不同的变量有时不能指向相同的类型。 map(n => n * n,[1,2,3]); // => [1,4,9]
(数字→数字)→[数字]→[数字] ,所以如果我们'重新匹配
(a→b)→[a]→[b] ,然后 a b 指向数字
这不是问题。我们仍然有两个不同的类型变量,因为
会有不同的情况。



参数化类型



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

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

// addItem :: a - >方框a - > Box a
const addItem = curry((item,box)=> / * ... * /);

这是我们如何指定 Box 参数化由未知类型 a
Box a 。这可以用在我们需要某个类型的地方,作为一个参数或
函数的返回值。当然,我们也可以使用
来定义类型,也可以是 Box Candy Box Rock 。 (虽然这
是合法的,但我们目前在拉姆达实际上并没有这样做,也许
我们根本不想被指责为像一盒石头一样笨手笨脚。) p>

不一定只有一个类型参数。我们可能有一个
Dictionary 类型,该类型根据键
的类型和它使用的值的类型进行参数化。这可以写成字典k
v
。这也表明我们可能会使用单个
字母的地方,这些字母不是来自字母表的最初字母。



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



类型别名



有时候我们的类型会失去控制,因为它们内在的复杂性或者它们太通用了,所以很难用它们来工作
。 Haskell允许类型别名来简化对这些元素的理解
。拉姆达也借用了这个概念,尽管它很少使用



这个想法很简单。如果我们有一个参数化类型用户字符串,其中
字符串是为了表示一个名字,我们希望更具体的关于类型的
当生成
URL时,我们可以创建一个类型别名:

  // toUrl ::用户名u =>网址 - > u  - > Url 
// Name = String
// Url = String
const toUrl = curry((base,user)=> base +
user.name.toLowerCase()。替换(/ \W / g,' - '));
toUrl('http://example.com/users/',{name:'Fred Flintstone',年龄:24});
// => 'http://example.com/users/fred-flintstone'

别名名称 Url 显示在 = 左侧。他们的
等价值出现在右边。



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

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

稍后我们将尝试分解该复数值,但现在,
它应该足够清楚,无论> Lens sa 表示,它下面的
只是复杂表达式的别名, Functor
f⇒(a→fa)→s→fs



单独回答。)


The API documentation for the JavaScript functional programming library Ramda.js contains symbolic abbreviations but does not provide a legend for understanding these. Is there a place (website, article, cheatsheet, etc.) that I can go to to decipher these?

Some examples from the Ramda.js API documentation:

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)

I am currently able to understand much of what Ramda.js is trying to do, and I can often make an educated guess what statements like the above mean. However I'm certain I would understand more easily if I understood these symbols/statements better. I would like to understand what individual components mean (e.g. specific letters, keywords, different arrow types, punctuation, etc.). I would also like to know how to "read" these lines.

I haven't had success googling this or searching StackExchange. I have used various combinations of "Ramda", "functional programming", "symbols", "abbreviations", "shorthand", etc. I'm also not exactly sure whether I'm looking for (A) universally used abbreviations in the broader field of functional programming (or perhaps even just programming in general), or (B) a specialized syntax that the Ramda authors are using (or perhaps co-opting from elsewhere but modifying further) just for their library.

解决方案

From the Ramda Wiki:

(Part 1 / 2 -- too long for a single SO answer!)


Type Signatures

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

Looking at the documentation for Ramda's over function, the first thing we see are two lines that look like this:

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

For people coming to Ramda from other FP languages, these probably look familiar, but to Javascript developers, they can be pure gobbledy-gook. Here we describe how to read these in the Ramda documentation and how to use them for your own code.

And at the end, once we understand how these work, we will investigate why people would want them.

Named Types

Many ML-influenced languages, including Haskell, use a standard method of describing the signatures of their functions. As functional programming becomes more common in Javascript, this style of signatures is slowly becoming almost standard. We borrow and adapt the Haskell version for Ramda.

We will not try to create a formal description, but simply capture to the essence of these signatures through examples.

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

Here we have a simple function, length, that accepts a word, of type String, and returns the count of characters in the string, which is a Number. The comment above the function is a signature line. It starts with the name of the function, then the separator "::" and then the actual description of the functions. It should be fairly clear what the syntax of that description is. The input of the function is supplied, then an arrow, then the output. You will generally see the arrow written as above, "->", in source code, and as "" in output documentation. They mean exactly the same thing.

What we put before and after the arrow are the Types of the parameters, not their names. At this level of description, all we really have said is that this is a function that accepts a String and returns a Number.

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

In this one, the function accepts two parameters, a position -- which is a Number -- and a word -- which is a String -- and it returns a single-character String or the empty String.

In Javascript, unlike in Haskell, functions can accept more than a single parameter. To show a function which requires two parameters, we separate the two input parameters with a comma and wrap the group in parentheses: (Number, String). As with many languages, Javascript function parameters are positional, so the order matters. (String, Number) has an entirely different meaning.

Of course for a function that takes three parameters, we just extend the comma-separated list inside the parentheses:

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

And so too for any larger finite list of parameters.

It might be instructive to note the parallel between the ES6-style arrow function definition and these type declarations. The function is defined by

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

By replacing the argument names with their types, the body with the type of value it returns and the fat arrow, "=>", with a skinny one, "->", we get the signature:

// (Number, String) -> String

Lists of Values

Very often we work with lists of values, all of the same type. If we wanted a function to add all the numbers in a list, we might use:

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

The input to this function is a List of Numbers. There is a separate discussion on precisely what we mean by Lists, but for now, we can think of it essentially as though they were Arrays. To describe a List of a given type, we wrap that type name in square braces, "[ ]". A List of Strings would be [String], a list of Booleans would be [Boolean], a List of Lists of Numbers would be [[Number]].

Such lists can be the return values from a function, too, of course:

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

And we should not be surprised to realize that we can combine these:

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

This function accepts a Number, val, and a list of Numbers, nbrs, and returns a new list of Numbers.

It's important to realize that this is all the signature tells us. There is no way to distinguish this function, by the signature alone, from any other function which happens to accept a Number and a list of Numbers and return a list of Numbers.[^theorems]

[^theorems]: Well, there is other information we can glean, in the form of the free theorems the signature implies.

Functions

There is still one very important type we haven't really discussed. Functional programming is all about functions; we pass functions as parameters and receive functions as the return value from other functions. We need to represent these as well.

In fact, we've already seen how we represent functions. Every signature line documented a particular function. We reuse the technique above in the small for the higher-order functions used in our signatures.

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

Here the function calc is described by (Number → Number) It is just like our top-level function signatures, merely wrapped in parentheses to properly group it as an individual unit. We can do the same thing with a function returned from another function:

// 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 accepts a tax rate, expressed as a percentage (type Number, and returns a new function, which itself accepts a Number and returns a Number. Again, we describe the function returned by (Number → Number), which makes the signature of the whole function Number → (Number → Number).

Currying

Using Ramda, we would probably not write a makeTaxCalculator exactly like that. Currying is central to Ramda, and we would probably take advantage of it here.[^curry-desc]

Instead, in Ramda, one would most likely write a curried calculateTax function that could be used exactly like makeTaxCalculator if that's what you wanted, but could also be used in a single pass:

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

This curried function can be used either by supplying both parameters up front and getting back a value, or by supplying just one and getting back a function that is looking for the second one. For this we use Number → Number → Number. In Haskell, the ambiguity is resolved quite simply: the arrows bind to the right, and all functions take a single parameter, although there is some syntactic sleight of hand to make it feel as though you can call them with multiple parameters.

In Ramda, the ambiguity is not resolved until we call the function. When we call calculateTax(6.35), since we have chosen not to supply the second parameter, we get back the final Number → Number part of the signature. When we call calculateTax(8.875, 49.95), we have supplied the first two Number parameters, and so get back only the final Number.

The signatures of curried functions always look like this, a sequence of Types separated by ''s. Because some of those types might themselves be functions, there might be parenthesized substructures which themselves have arrows. This would be perfectly acceptable:

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

This is made up. I don't have a real function to point to here. But we can learn a fair bit about such a function from its type signature. It accepts three functions and an Object and returns a String. The first function it accepts itself takes a Boolean and a Number and returns a String. Note that this is not described here as a curried function (or it would have been written as (Boolean → Number → String).) The second function parameter accepts an Object and returns a Boolean, and the third accepts an Object and returns a Number.

This is only slightly more complex than is realistic in Ramda functions. We don't often have functions of four parameters, and we certainly don't have any that accept three function parameters. So if this one is clear, we're well on our way to understanding anything Ramda has to throw at us.

[^curry-desc]: For people coming from other languages, Ramda's currying is perhaps somewhat different than you're used to: If f :: (A, B, C) → D and g = curry(f), then g(a)(b)(c) == g(a)(b, c) == g(a, b)(c) == g(a, b, c) == f(a, b, c).

Type Variables

If you've worked with map, you'll know that it's fairly flexible:

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]

From this, we would want to apply all the following type signatures to map:

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

But clearly there are many more possibilities too. We cannot simply list them all. To deal with this, type signatures deal not only with concrete classes such as Number, String, and Object, but also with representations of generic classes.

How would we describe map? It's fairly simple. The first parameter is a function that takes an element of one type, and returns an element of a second type. (The two type don't have to have to be different.) The second parameter is a list of elements of the input type of that function. It returns a list of elements of the output type of that function.

This is how we could describe it:

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

Instead of the concrete types, we use generic placeholders, single lower-character letters to stand for arbitrary types.

It's easy enough to distinguish these from the concrete types. Those are full words, and by convention are capitalized. Generic type variables are just a, b, c, etc. Occasionally, if there is a strong reason, we might use a letter from later in the alphabet if it helps makes some sense of what sorts of types the generic might represent (think k and v for key and value or n for a number), but mostly we just use these ones from the beginning of the alphabet.

Note that once a generic type variable is used in a signature, it represents a value that is fixed for all uses of that same variable. We can't use b in one part of the signature and then reuse it elsewhere unless both have to be of the same type in the entire signature. Moreover, if two types in the signature must be the same, then we have to use the same variable for them.

But there is nothing to say that two different variables can't sometimes point to the same types. map(n => n * n, [1, 2, 3]); //=> [1, 4, 9] is (Number → Number) → [Number] → [Number], so if we're to match (a → b) → [a] → [b], then both a and b point to Number. This is not a problem. We still have two different type variables since there will be cases where they are not the same.

Parameterized Types

Some types are more complex. We can easily imagine a type representing a collection of similar items, let's call it a Box. But no instance is an arbitrary Box; each one can only hold one sort of item. When we discuss a Box we always need to specify a Box of something.

// 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) => /* ... */);

This is how we specify a Box parameterized by the unknown type a: Box a. This can be used wherever we need a type, as a parameter or as the return of a function. Of course we could parameterize the type with a more specific type as well, Box Candy or Box Rock. (Although this is legitimate, we don't actually do this in Ramda at the moment. Perhaps we simply don't want to be accused of being as dumb as a box of rocks.)

There does not have to be just a single type parameter. We might have a Dictionary type that is parameterized over both the type of the keys and the type of the values it uses. This could be written Dictionary k v. This also demonstrates the sort of place where we might use single letters that are not the initial ones from the alphabet.

There aren't many declarations like this in Ramda itself, but we might find ourselves using such things fairly often in custom code. The largest usage of these is to support typeclasses, so we should describe those.

Type Aliases

Sometimes our types get out of hand, and it becomes difficult to work with them because of their inner complexity or because they're too generic. Haskell allows for type aliases to simplify the understanding of these. Ramda borrows this notion as well, although it's used sparingly.

The idea is simple. If we had a parameterized type User String, where the String was meant to represent a name, and we wanted to be more specific about the type of String that is represented when generating a URL, we could create a type alias like this:

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

The aliases Name and Url appear to the left of an "=". Their equivalent values appear to the right.

As noted, this can also be used to create a simple aliases to a more complex type. A number of functions in Ramda work with Lenses, and the types for those are simplified by using a type alias:

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

We'll try to break down that complex value a little later, but for now, it should be clear enough that whatever Lens s a represents, underneath it is just an alias for the complicated expression, Functor f ⇒ (a → f a) → s → f s.

(Part 2 in a separate answer.)

这篇关于我在哪里可以找到用于解释函数式编程的符号的解释/概要,特别是Ramda.js?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆