为什么要使用函数式编程

  1. 越来越多的前端框架都在使用函数式编程,学习函数式编程可以更好的学习他们的源码
  2. 使我们代码更简洁,看起来更高级一些。
  3. 函数式编程可以抛弃 this

什么是函数式编程

函数式编程是一种编程范式之一,是一种编程风格,它和面向对象编程是并列的关系。

常见的编程范式还有面向过程编程,面向对象编程

  • 面向过程编程
    按照步骤一步一步实现功能
  • 面向对象编程思维模式
    将现实的事物抽象成程序中的类和对象,通过封装、继承、多态来演示事物事件的联系
  • 函数式编程思维模式
    把现实世界中的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
    • 程序的本质:根据输入 通过某种运算获得相应的输出。

函数相关概念

函数是一等公民

  • 函数可以存储在变量中 const fn = function() {}
  • 函数可以作为参数 forEach(function() {})
  • 函数可以作为返回值 function Foo() { return function() {} }

在 JavaScript 中,函数就是一个普通的对象,所以可以存储在变量中,可以作为参数,可以作为函数的返回值

高阶函数

高阶函数就是 函数作为参数、函数作为返回值 的体现

闭包

  • 闭包:函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
    • 可以在另一个作用域中调用一个函数 fn 的内部函数 gn,并访问到 fn 的内部成员
  1. // 函数作为返回值
  2. function foo() {
  3. let msg = 'hello world'
  4. return function() {
  5. console.log(msg)
  6. }
  7. }
  8. const fn = foo();
  9. fn();
  • 闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除;但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

函数式编程基础

lodash

lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库

lodash 中的 fp 模块

lodash 中的 fp 模块提供了对函数式编程友好的方法

  • 提供了 不可变的 自动柯里化
  1. const fp = require('lodash/fp')
  2. const f = fp.curry(function(a, b, c) {
  3. return a + b + c
  4. });
  5. f(1, 2, 3)
  6. f(1, 2)(3)
  7. f(1)(2)(3)
  • 如果需要柯里化的函数有多个参数,遵循 函数优先 数据在后

纯函数

对于纯函数,相同的输入始终会得到相同的结果

它不是程序中的函数,而是数学中的函数,它是一个值与另一个值的映射关系的体现

  1. function getArea(r) {
  2. return Math.PI * r * r;
  3. }
  4. getArea(2)

柯里化

函数柯里化可以对函数 fn 进行降元,把多元函数(多个参数)降为一元函数(一个参数),并返回一个函数 f。

柯里化函数会对先传入的参数进行‘缓存’,当调用函数f 时传递的参数等于 函数fn 所需参数时,才会执行函数 fn

  1. // 函数柯里化原理
  2. // 函数柯里化:它是一个函数,这个函数接收一个函数 func 作为参数,
  3. // 并返回一个函数,返回的函数接收 func 所需的参数,
  4. // 直到将 func 的所需参数接收齐后,执行 func 函数
  5. function curry(func) {
  6. // 不确定参数个数。可用 reset
  7. return function curried(...args) {
  8. // 参数个数小于 func 所需的参数个数(func.length: 代表这个函数形参的个数)
  9. if (args.length < func.length) {
  10. // 将参数合并,拆分后继续调用
  11. return function() {
  12. return curried(...args.concat(Array.from(arguments)));
  13. };
  14. }
  15. // 参数个数等于 func 所需参数的时候,执行 func,并将结果返回
  16. return func(...args);
  17. };
  18. }
  19. const add = curry(function(a, b, c) {
  20. return a + b + c;
  21. });
  22. console.log(add(1, 2, 3));
  23. console.log(add(1, 2)(3));
  24. console.log(add(1)(2)(3));

管道

把函数看成一个管道,传入x,经过管道处理后得到结果y

函数组合

函数组合,把多个一元函数进行组合,返回一个新的函数。

函数组合中的函数,默认从右到左执行

  1. // 函数组合:将多个细粒度较小的纯函数组合成一个函数,组合是默认从右到左执行
  2. function compose(...args) {
  3. // 返回一个函数,该函数接收用于操作所需的参数
  4. return function(value) {
  5. // 返回执行结果
  6. return args.reverse()
  7. .reduce(function(acc, fn) { // acc: 上一次的执行结果 fn: 当前函数
  8. return fn(acc);
  9. }, value); // value: acc 的初始值
  10. };
  11. }
  12. // 纯函数:反转数组
  13. function reverse(array) {
  14. return array.reverse();
  15. }
  16. // 纯函数:取出数组第一个元素
  17. function getFirst(array) {
  18. return array[0];
  19. }
  20. // 组合函数
  21. let comp = compose(getFirst, reverse);
  22. const array = [1, 2, 3, 4];
  23. console.log(comp(array)); // 4

函子

函子是一个实现了 map 方法契约的对象,
它可以处理函数式编程中运算过程的问题(函数式编程就是对运算过程进行抽取);

函数式编程不直接操作值,而是由函子进行操作。

也就是说我们可以使用 函子来帮助我们进行 函数式编程中,对值进行操作的过程

函子中存储一个值,这个值就是我们要处理的一个值, 如果想要处理这个值,只能通过 map 方法传入一个处理函数来处理(这个处理函数必须是纯函数);
并返回一个包含处理完成的 函子 对象

我们可以把函子看成一个容器,这个容器中存放了一个值,和值的变化关系(函数)。这个函数是 map,它会执行一个函数来进行对这个值的操作

Functor 函子:

  1. // 一个容器
  2. class Container {
  3. constructor(value) {
  4. // 一个值,这个值是私有的。不对外公开
  5. this._value = value;
  6. }
  7. // 一个操作值的方法,该方法接收一个函数,使用这个函数对值进行操作
  8. map(fn) {
  9. // 返回一个包含了操作后的值 的一个容器,以便后续的操作
  10. return new Container(fn(this._value));
  11. }
  12. }
  13. // 使用函子
  14. let r = new Container(5)
  15. .map(v => v + 2) // 这个v就是函子中的值,我们对其进行操作; v=7
  16. .map(v => v * v) // 上一个 map 操作后会返回一个包含处理过的值的容器,所以可以继续调用 map 操作值 v=49
  17. console.log(r); // 最终的结果就是 包含了最终map处理过的值 的容器(函子)
  18. // Container { _value: 49 }

为了方便调用 Container 函子,可以在 Container 中创建一个 静态方法,用来返回 Container 实例

  1. class Container {
  2. static of(value) {
  3. return new Container(value);
  4. }
  5. // ...
  6. map(fn) {
  7. // return new Container(fn(this._value));
  8. return Container.of(fn(this._value));
  9. }
  10. }
  11. const r = Container.of(5)
  12. .map(v => v + 2)
  13. .map(v => v * v);
  14. console.log(r); // Container { _value: 49 }

上面的函子,如果传入的是 null,就会报错

  1. // 这个 null 可能是接口数据
  2. const r = Container.of(null)
  3. .map(v => v.toUpperCase()); // v是null,对 null 进行属性调用,就会报错
  4. // TypeError: Cannot read property 'toUpperCase' of null

MayBe 函子:将函子的副作用(当传入空时(null、undefined等))控制在允许范围内

  1. class MayBe {
  2. static of(value) {
  3. return new MaBe(value);
  4. }
  5. constructor(value) {
  6. this._value = value;
  7. }
  8. map(fn) {
  9. return this.isEmpty() ? MayBe.of(null) : MayBe(fn(this._value));
  10. }
  11. isEmpty() {
  12. return this._value === null || this._value === undefined;
  13. }
  14. }
  15. const r = MayBe.of(5)
  16. .map(v => null) // 直接返回一个 null
  17. .map(v => v.toString()); // 对 null 进行处理
  18. console.log(r); // 输出: MayBe { _value: null }

可以看到,经过空值判断将副作用控制在允许范围内。

但是这样会有问题,如果出错了,我们不知道是哪一个步骤出的错。

所以我们需要对这种异常的情况进行处理

Either 函子:两者之一,类似 if…else 的情况

比如我们要对一串字符串数据进行 JSON 转化。有可能这个数据不是 JSON 格式的,转化的时候就会出错

  1. function parseJson(str) {
  2. // 对转化过程进行一场捕获
  3. try {
  4. return JSON.parse(str);
  5. } catch(err) {
  6. console.log(err);
  7. }
  8. }

我们将这种转化过程 使用函子来抽取

  • Either 函子需要声明两个对象,一种用于处理正常情况,一种用于处理异常情况
  1. // Left: 用于处理异常情况的函子
  2. class Left {
  3. static of(value) {
  4. return new Left(value);
  5. }
  6. constructor(value) {
  7. this._value = value;
  8. }
  9. map(fn) {
  10. // 直接返回自身对象,方便查看对象信息
  11. return this;
  12. }
  13. }
  14. // Right: 用于处理正常情况的函子
  15. class Right {
  16. static of(value) {
  17. return new Right(value);
  18. }
  19. constructor(value) {
  20. this._value = value;
  21. }
  22. map(fn) {
  23. return Right.of(fn(this._value));
  24. }
  25. }

使用 Either 函子处理异常

  1. function parseJson(str) {
  2. try {
  3. Right.of(JSON.parse(str));
  4. } catch(err) {
  5. Left.of({ error: err.message })
  6. }
  7. }
  8. const r = parseJson('{ name: zs }');
  9. // Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
  10. const r2 = parseJson('{"name": "zs"}');
  11. // Right { _value: { name: 'zs' } }

前面提到的函子都是需要纯函数来处理的,如果遇到不是纯函数的情况要怎么处理呢?

IO 函子: 用于处理不纯的用户操作

  • IO 函子跟前面的函子不一样:它的 value 是函数,这里是把函数当作 value 来处理的
  • IO 函子可以把不纯的动作存储到 _value 中,_value 中存储的是函数,在函子内部并没有调用这个函数
    通过 IO 函子,我们可以延迟 执行这个不纯的操作(相当于惰性操作),
  • 把不纯的操作交给调用者处理
  1. // 组合函数,将多个函数组合成一个全新的函数
  2. const fp = require('lodash/fp');
  3. class IO {
  4. // IO 函子的 of 方法跟其他函子不一样,它接收一个数据 value
  5. static of(value) {
  6. // 并声明返回一个 IO 实例,
  7. return new IO(function() {
  8. // 最终 IO 函子返回一个值, 但是这个值已经被 _value 包装过, 并不是直接返回, 所以需要手动调用 _value() 来获取这个 value
  9. return value;
  10. })
  11. }
  12. // IO 函子的构造器接收一个函数, 把 函数当成 value 来处理
  13. constructor(fn) {
  14. // 将 函数 保存到 _value
  15. this._value = fn;
  16. }
  17. // map 方法接收一个函数
  18. map(fn) {
  19. // 将 _value 和 fn 组合之后的新函数传递给 IO 进行实例化, 并返回一个全新的 IO 实例.
  20. return new IO(fp.flowRight(fn, this._value));
  21. // this._value 是一个函数, 它包装了一个数据 value
  22. // 它会被作为 fn 的参数传递到 fn 函数中
  23. // 由 fn 处理这个数据 value
  24. }
  25. }
  26. const r = IO.of('hello world')
  27. .map(function(x) { // map中 传入的函数 它接收到的参数x 就是of传入的'hello world'
  28. // 将数据转为大写
  29. return x.toUpperCase();
  30. });
  31. console.log(r); // IO { _value: [Function] }
  32. // 执行 _.value()
  33. console.log(r._value()); // HELLO WORLD
  34. const r2 = IO.of(process) // process: node 环境中 进程的意思
  35. .map(p => p.execPath); // 这里的p: 就是我们前面传入的 process. 他是经过 IO 中的 _value 返回出来的.
  36. console.log(r2); // IO { _value: [Function] }
  37. console.log(r2._value()); // C:\Program Files\nodejs\node.exe

IO 函子与其他函子最大的区别就是:

  1. 它的 _value 是一个函数。它把不纯的操作(比如IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行。
    所以我们认为,IO 包含的是被包裹的操作的返回值
  2. IO其实也算是惰性求值
  3. IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维护性

IO 函子不纯的操作的例子

  1. // 调用 IO.of 并传入 hello world
  2. const r = IO.of('hello world')
  3. // 第一次进行 map 操作, 接收到的参数x: hello world
  4. .map(x => {
  5. // 返回一个 Promise
  6. return new Promise(resolve => {
  7. // 在两秒后 对 x 拼接字符串, 然后 resolve
  8. setTimeout(() => {
  9. resolve(x + ' world wilde web');
  10. }, 2000);
  11. });
  12. })
  13. // 第二次 map, 接收到的参数x: 是上一次 map 返回的 Promise 状态
  14. .map(x => {
  15. // 只有等待 上一次 map 操作 resolve 了数据, 才进行下一步操作
  16. return x.then(res => { // res 就是上一次 map 操作过后的结果
  17. // 对结果进行大写转换
  18. return res.toUpperCase();
  19. // 也可以使用 Promise.resolve() 返回数据
  20. // return Promise.resolve(res.toUpperCase());
  21. });
  22. });
  23. // r 是 IO 对象, 其 _value 中存储着对数据处理后的结果.
  24. // 而
  25. r._value().then(res => {
  26. console.log('最终结果', res);
  27. });
  28. // 最终结果 HELLO WORLD WORLD WILDE WEB

IO 函子可以有效的处理 不纯操作

task 函子: 用于处理异步执行的操作

使用 falktale 中的 task 来执行异步操作.

task 是函数, 它接收一个操作函数, 并返回一个 Task 函子

  1. var task = function task(computation) {
  2. return new Task(computation);
  3. };

使用 task 来执行文件读取任务

  1. // 引入文件模块
  2. const fs = require('fs');
  3. // 引入 task 模块
  4. const { task } = require('folktale/concurrency/task');
  5. // 声明一个方法, 用于读取文件
  6. function readFile(filename) {
  7. // task 函数接收一个 操作函数 computation, computation 接收一个参数: 执行器对象.
  8. // 如果执行成功 可以调用 执行器.resolve(); 失败调用 执行器.reject();
  9. return task(resolver => {
  10. // 读取文件
  11. fs.readFile(filename, 'utf8', (err, data) => {
  12. // 异常
  13. if (err) resolver.reject(err);
  14. // 成功. 将结果返回
  15. resolver.resolve(data)
  16. })
  17. });
  18. }
  19. // 读取 package.json文件
  20. readFile('package.json')
  21. // 并没有立即执行读取文件的操作. 因为 readFile 函数返回的是一个 Task 函子, 需要手动触发读取文件操作
  22. // run() Task 函子中启动任务的执行器
  23. .run() // 执行 读取文件操作
  24. // 对于执行结果, 需要在 run 后面使用 listen 进行监听
  25. // listen 接收一个对象参数, 该对象涵盖了操作事件
  26. .listen({
  27. onResolve: value => {
  28. console.log(value);
  29. },
  30. onReject: err => {
  31. console.log(err);
  32. }
  33. });

如果需要对读取到的文件进行操作. 可以在 map 方法中执行操作

  1. const { split, find } = require('lodash/fp');
  2. // 读取 package.json 文件, 并获取 version 的值
  3. readFile('package.json')
  4. // map 方法返回一个新的 Task 函子
  5. // 这个函子先执行了 run 方法, 执行 去读取文件的操作, 然后再对数据进行处理
  6. // 使用 lodash 中的 分割函数, 将数据 以换行符进行分割
  7. .map(split('\n'))
  8. // 经过前面的分割操作, 次数数据已经是数组了
  9. // 使用 find 查找数组中含有 version 的元素
  10. .map(find(x => x.includes('version'))) // find 函数接收一个参数, 这个参数就是数组中的每一个元素, find 寻找包含 version 的元素
  11. .run() // run, 开始执行 task 任务. 只在需要的地方执行一次即可
  12. // map 方法只能在 run 前面执行
  13. // .map(res => { // TypeError: readFile(...).map(...).run(...).map is not a function
  14. // console.log('map2', res);
  15. // return res;
  16. // });
  17. // listen 监听执行事件, 里面包含成功失败等事件. 它只能在 run 后面执行
  18. .listen({
  19. onResolve: value => {
  20. console.log(value);
  21. },
  22. onReject: err => {
  23. console.log(err);
  24. }
  25. });
  26. // "version": "1.0.0",

pointed 函子

pointed 函子其实就是 声明了 of 方法的 函子.

of 的实现是为了避免我们经常使用 new 去创建对象, 使代码看起来像面向对象

of 方法是用来把 值 放在 context 上下文中(把值放到容器中, 然后使用 map 方法来处理值)

  1. class Container {
  2. // 有 of 方法, 所以这个函子是 pointed 函子
  3. // of 方法的作用是 帮我们把值包裹在一个新的 函子 里面, 并返回. 这个返回的结果就是上下文
  4. static of(value) {
  5. return new Container(value);
  6. }
  7. ...
  8. }
  9. // 调用 of 的时候, 获得一个上下文
  10. Container.of(2)
  11. // 在上下文中处理数据
  12. .map(x => x + 5)

IO 函子的调用问题

  1. const fs = require('fs');
  2. const fp = require('lodash/fp');
  3. class IO {
  4. static of(value) {
  5. return new IO(function() {
  6. return value;
  7. });
  8. }
  9. constructor(fn) {
  10. this._value = fn;
  11. }
  12. map(fn) {
  13. return new IO(fp.flowRight(fn, this._value));
  14. }
  15. }
  16. function readFile(filename) {
  17. return new IO(function() {
  18. console.log('readFile');
  19. return fs.readFileSync(filename, 'utf-8');
  20. });
  21. }
  22. function print(x) {
  23. return new IO(function() {
  24. console.log('print');
  25. return x;
  26. });
  27. }
  28. // cat 读取文件
  29. const cat = fp.flowRight(print, readFile);
  30. // cat此时是一个嵌套的 IO, IO(IO(x)). 假设 readFile返回的IO为 IO1, print返回的IO为 IO2
  31. // 因为限制新了 readFile, 它返回了一个IO1, 并将这个 IO1 传递给了 print
  32. // 所以 print 函数中的参数 x 就是 IO1. 而 print 也返回了一个 IO
  33. // 这个 IO 的回调函数里面return IO1
  34. // 所以嵌套 IO 中, 外层的 IO 是 IO2, 里面的 IO是 IO1
  35. // const r = cat('package.json');
  36. // console.log(r); // IO { _value: [Function] }
  37. /*
  38. IO2 里面嵌套了 IO1. 可以看成一个嵌套的 IO 对象
  39. {
  40. tag: 'print 返回的 IO'
  41. _value: function() {
  42. return {
  43. tag: 'readFile 返回的 IO',
  44. _value: function() {
  45. return fs.readFile(...)
  46. }
  47. }
  48. }
  49. }
  50. 第一次调用 _value() 就是执行了 print 返回的 IO 函子
  51. 第二次调用 _value() 就是执行了 readFile 返回的 IO 函子
  52. */
  53. const r = cat('package.json')._value()._value();
  54. console.log(r);

控制台打印结果

  1. print # 调用第一个 _value() 的输出
  2. readFile # 调用第二个 _value() 的输出
  3. { # 输出读取文件的结果
  4. "name": "demo",
  5. "version": "1.0.0",
  6. "description": "",
  7. "main": "01-函数式编程.js",
  8. "scripts": {
  9. "test": "echo \"Error: no test specified\" && exit 1"
  10. },
  11. "keywords": [],
  12. "author": "",
  13. "license": "ISC",
  14. "dependencies": {
  15. "folktale": "^2.3.2",
  16. "lodash": "^4.17.19"
  17. }
  18. }

函子出现了嵌套, 调用起来特别不方便

比如想要执行开始执行 读取文件操作, 需要 一直 ._value()._value()

Monad 函子: 解决函子嵌套问题

Monad 函子是可以 变扁 的 Pointed 函子

变扁: 可以将解决嵌套函子问题

如果函数发生了嵌套, 可以使用 函数组合来解决; 如果函子发生了嵌套, 可以使用 Monad 来解决

如果一个函子同时具有 joinof 两个方法, 并遵守一些定律, 这个函子就是 Monad 函子

如果想要合并一个函数, 这个函数返回一个值, 使用 map 方法

如果想要合并一个函数, 这个函数返回一个函子, 使用 flatMap 方法

  • of

静态方法,返回 IO 函子,便于使用

  1. static of(value) {
  2. return new IO(function() {
  3. return value;
  4. });
  5. }
  • join

当使用 IO 函子的时候,需要传入一个函数, 如果传入的函数 返回了一个函子, 这个时候可以使用 Monad 函子,

通过调用 join 方法调用 this._value() 可以返回一个函子, 这样就可以解决函子嵌套的问题

  1. join() {
  2. return this._value()
  3. }
  • flatMap

在使用 Monad 函子的时候,经常将 mapjoin 联合起来一起使用

因为 map 方法接收一个函数 fn,map 的作用就是将这个函数和当前的 _value 组合起来,它返回一个新的函子。

map 在组合函数的时候,它最终也会返回一个函子。

  1. const fs = require('fs');
  2. const fp = require('lodash/fp');
  3. // 如果一个函子中 同时具有 of 和 join 两个方法, 就是 Monad 函子
  4. class IO {
  5. // of 方法: 返回一个函子
  6. static of(value) {
  7. return function() {
  8. return value;
  9. };
  10. }
  11. constructor(fn) {
  12. this._value = fn;
  13. }
  14. map(fn) {
  15. // 返回一个函子, 并将 当前的 _value 和 fn 组合成一个新的函数传递进去
  16. // 此时 constructor 中 this._value 就是(this._value 和 fn)合并后的 新函数
  17. return new IO(fp.flowRight(fn, this._value));
  18. }
  19. join() {
  20. // 返回对 this._value 的调用
  21. // 因为在 Monad 中, 我们传入的函数(constructor 中传入的函数), 最终会返回一个函子
  22. // 所以 调用 this._value 会返回一个函子
  23. // 比如: function a() { return new IO(function(x) { return x }) }
  24. // 此时 函数 a 中返回了一个函子
  25. // 所以在这里, 执行了 this._value() 之后, 会返回一个函子.
  26. return this._value();
  27. // 此时的 _value 是 map 方法中合并后的函数,fp.flowRight(fn, this._value)
  28. // 调用 this._value(), 会先执行 fp.flowRight 中的 this._value, 然后再将执行结果传给 fn, 并执行 fn
  29. }
  30. // 如果传入的函数返回的是函子,则调用 flatMap
  31. // flatMap 调用 map,所以需要一个函数
  32. flatMap(fn) {
  33. // 1. 调用 map 的时候,将 _value 和 fn 进行合并
  34. // map 在组合这些方法的时候,这个函数最终也会返回一个函子,就会形成一个嵌套的 IO 函子
  35. // 2. map 返回了一个 新的IO函子
  36. // 新的 IO 函子中包裹的函数 _value,最终也会返回一个函子,
  37. // 4. 将 join 后的结果(函子)返回
  38. return this.map(fn)
  39. // 3. 调用 join 方法,把 _value 变扁
  40. .join();
  41. }
  42. }
  43. // 读取文件的函数,它依赖外部环境,所以是不纯的操作
  44. function readFile(filename) {
  45. // 为了保证当前操作是纯的,
  46. // 不直接去读取文件
  47. // 而返回一个 IO 函子
  48. // 根据相同的输入,获得的是一个固定的内容:一个 IO函子
  49. return new IO(function() {
  50. // 将读取文件的操作封装到 IO函子里面去
  51. // 读取文件, 并将结果返回
  52. return fs.readFileSync(filename, 'utf-8');
  53. });
  54. }
  55. function print(x) {
  56. // 返回一个 IO 函子
  57. return new IO(function() {
  58. return x;
  59. });
  60. }
  61. // 调用 readFile 函数,这个函数返回一个 IO 函子
  62. // IO 函子中包裹着读取文件的操作
  63. const r = readFile('package.json') // 返回了一个 IO 函子,里面包裹了读取文件的操作
  64. // 如果要合并的函数返回的是值,则调用 map 方法
  65. .map(fp.toUpper)
  66. // 将读取文件的操作 和 打印的操作合并
  67. // 因为要合并的函数中返回的是函子,所以使用 flatMap 方法
  68. .flatMap(print) // 由 readFile 返回的IO函子调用 flatMap,并传入 print 会和前面包裹的 读取文件的操作 合并
  69. .join(); // 执行 _value
  70. console.log(r);