注入具有副作用的功能

时间:2017-02-11 18:19:31

标签: javascript dependency-injection functional-programming inversion-of-control higher-order-functions

使用高阶函数时遇到问题。 假设我有以下不使用它们的代码(而是调用全局函数):

import {db_insert} from 'some-db-lib' // function with side-effect

const save_item = (item) => {
    // some logic like validating item data...
    db_insert(item) // call db_insert globally
}

const handle_request = (request) => {
    // some logic like sanitizing request...
    save_item(request.data) // call save_item globally
}

handle_request(some_request)

现在,同样的例子,通过使用高阶函数(注入副作用作为函数参数):

import {db_insert} from 'some-db-lib' // function with side-effect

const save_item = (item, insert) => { // inject insert
    // some logic like validating item data...
    insert(item)
}

const handle_request = (request, save, insert) => { // inject save and insert
    // some logic like sanitizing request...
    save(request.data, insert)
}

handle_request(some_request, save_item, db_insert)

想象一下,这是一个更大的函数树互相调用。最后一个例子将成为将函数传递给彼此的大量函数。

这是隔离副作用的正确方法吗?我错过了什么吗?

2 个答案:

答案 0 :(得分:4)

  

使用高阶函数时遇到问题。假设我有以下不使用它们的代码(而是调用全局函数):

c = (x) => console.log(x)
b = (x) => c(x)
a = (x) => b(x)
a('Hello world')

但这是一个可怕的起点,说实话

  • c只是console.log
  • eta conversion
  • b只是c
  • 的转化
  • a只是b
  • 的转化

换句话说,a === b === c === console.log - 如果您要了解高阶函数,需要一个更好的起点

常见示例:map

人们喜欢使用Array.prototype.map

来展示高阶函数

const f = x => x + 1
const g = x => x * 2
const xs = [1,2,3]

console.log (xs.map (f)) // [2,3,4]
console.log (xs.map (g)) // [2,4,6]

这里发生了什么?它实际上非常整洁。我们可以使用输入数组xs并创建一个 new 数组,其中每个元素都是使用高阶函数对xs中元素的转换。高阶函数可以是一次性使用 lambda ,也可以是已经在别处定义的命名函数。

// xs.map(f)
[ f(1), f(2), f(3) ]
[   2 ,   3 ,   4  ]

// xs.map(g)
[ g(1), g(2), g(3) ]
[   2 ,   4 ,   6  ]

// xs.map(x => x * x)
[ (x => x * x)(1), (x => x * x)(2), (x => x * x)(3) ]
[              1 ,              4 ,              9  ]

更大的图片

好的,这是在JavaScript中使用高阶函数的一个非常实用的例子,但是......

  

我错过了什么吗?

是。高阶函数具有非常深刻和有意义的能力。让我提出另一组问题:

  • 如果没有数组这样的东西怎么办?
  • 我们如何以有意义的方式将值组合在一起?
  • 如果没有一组价值观,我们肯定无法map,对吧?

如果我告诉你,只有函数需要完成所有操作,该怎么办?

// empty
const empty = null
const isEmpty = x => x === empty

// pair
const cons = (x,y) => f => f (x,y)
const car = p => p ((x,y) => x)
const cdr = p => p ((x,y) => y)

// list
const list = (x,...xs) =>
  x === undefined ? empty : cons (x, list (...xs))
const map = (f,xs) =>
  isEmpty (xs) ? empty : cons (f (car (xs)), map (f, cdr (xs)))
const list2str = (xs, acc = '( ') =>
  isEmpty (xs) ? acc + ')' : list2str (cdr (xs), acc + car (xs) + ' ')

// generic functions
const f = x => x + 1
const g = x => x * 2

// your data
const data = list (1, 2, 3)

console.log (list2str (map (f, data)))          // '( 2 3 4 )'
console.log (list2str (map (g, data)))          // '( 2 4 6 )'
console.log (list2str (map (x => x * x, data))) // '( 1 4 9 )'

我仔细观察,您会看到此代码不使用JavaScript提供的任何本机数据结构(Number除外,例如数据和String,以便输出内容看到)。没有Object,没有Array。没有技巧。只需Function s。

它是如何做到的?数据在哪里?

简而言之,数据存在于部分应用的功能中。让我们专注于一段特定的代码,以便向您展示我的意思

const cons = (x,y) => f => f (x,y)
const car = p => p ((x,y) => x)
const cdr = p => p ((x,y) => y)

const pair = cons (1,2)
console.log (car (pair)) // 1
console.log (cdr (pair)) // 2

当我们使用pair创建cons(1,2)时,请仔细查看数据在pair中如何存储。它是什么形式的? cons会返回xy绑定到值12的函数。我们称之为p的此函数正在等待另一个函数f调用,该函数将f应用于xycarcdr提供该功能(f)并返回所需的值 - 在car的情况下,x被选中。如果是cdr,则会选择y

所以让我们再说一遍......

“我错过了什么吗?”

是。你刚才所见到的多功能数据结构的起源,除了(更高阶)功能外。

您的语言是否缺少您可能需要的特定数据结构?您的语言是否提供一流的功能?如果对这两个问题都回答“是”,那么没有问题,因为除了函数之外,你可以实现所需的特定数据结构。

是高阶函数的强大功能。

不情愿地说服

好的,所以也许你认为我在那里取了一些技巧替换上面的Array。我向你保证,没有任何诡计。

以下是我们将为新字典类型dict

制作的API
dict (key1, value1, key2, value2, ...) --> d
read (d, key1) --> value1
read (d, key2) --> value2

write (d, key3, value3) --> d'
read (d', key3) --> value3

下面,我将保持不使用除函数(以及用于演示输出目的的字符串)之外的任何内容的承诺,但这次我将实现可以保存键/值对的不同数据结构。您可以读取值,写入新值,并根据键覆盖现有值。

这将强化高阶数据的概念,即数据是较低抽象的抽象 - 即dict是使用node实现的。使用list实现,使用cons等实现

// empty
const empty = null
const isEmpty = x => x === empty

// pair
const cons = (x,y) => f => f (x,y)
const car = p => p ((x,y) => x)
const cdr = p => p ((x,y) => y)

// list
const list = (x,...xs) =>
  x === undefined ? empty : cons (x, list (...xs))
const cadr = p => car (cdr (p))
const cddr = p => cdr (cdr (p))
const caddr = p => car (cddr (p))
const cadddr = p => cadr (cddr (p))

// node
const node = (key, value, left = empty, right = empty) =>
  list (key, value, left, right)
const key = car
const value = cadr
const left = caddr
const right = cadddr

// dict
const dict = (k,v,...rest) =>
  v === undefined ? empty : write (dict (...rest), k, v)

const read = (t = empty, k) =>
  isEmpty (t)
    ? undefined
    : k < key (t)
      ? read (left (t), k)
      : k > key (t)
        ? read (right (t), k)
        : value (t)

const write = (t = empty, k, v) =>
  isEmpty (t)
    ? node (k, v)
    : k < key (t)
      ? node (key (t), value (t), write (left (t), k, v), right (t))
      : k > key (t)
        ? node (key (t), value (t), left (t), write (right (t), k, v))
        : node (k, v, left (t), right (t))

let d = dict ('a', 1, 'b', 2)
console.log (read (d, 'a')) // 1
console.log (read (d, 'b')) // 2
console.log (read (d, 'c')) // undefined

d = write (d, 'c', 3)
console.log (read (d, 'c')) // 3

当然现在你看到了高阶函数的力量,对吧? ^ _ ^

答案 1 :(得分:1)

高阶函数

高阶函数用于从函数应用程序中抽象。常见的HOF至少需要一个函数f和至少一个附加参数x,并将f应用于x。这是最简单的例子:

const apply = (f, x) => f(x);

不是特别有趣。关键的一点是,每个有意义的HOF都会做一些额外的事情。它迭代地应用给定函数(mapreduce)。它由两部分组成:

const comp = (f, g) => x => f(g(x));

const inc = x => x + 1;

console.log(
  comp(inc, inc) (0) // 2
);

它将函数应用于Object,即使此函数专门接受Number s:

const destruct = (x, y, f) => ({[x]:a, [y]:b}) => f(a, b);

const add = (x, y) => x + y;

const o = {propA:2, propB:3};

console.log(
  destruct("propA", "propB", add) (o) // 5
);

或者它部分应用了一个功能:

const partial = (f, ...args) => (...args2) => f(...args, ...args2);

sum5 = (v, w, x, y, z) => v + w + x + y + z;
subtotal = partial(sum5, 1, 2, 3);

console.log(
  subtotal(4, 5) // 15
);

延续传球风格

您的代码模式_a = (x, b, c) => b(x, c)实际上来自延续传递样式。它是高阶函数的一种特殊形式,其中函数的最后一个参数必须始终是另一个函数,它表示延续,即当前计算的其余部分。 continuation函数不会附带其周围函数的参数,而是其计算结果。继承函数或更确切地说它的应用程序可以替代return语句。

虽然你会在功能性Javascript中遇到HOF,但是延续传递风格很少见。当您查看以下示例时,您将知道原因:

const eqk = (x,y,k) => k(y === x);
const mulk = (x,y,k) => k(y * x);
const subk = (x,y,k) => k(y - x);

const powerk = (x, y, k) =>
  eqk(0, y, isDone =>
    isDone
      ? k(1)
      : subk(1, y, _y=>
        powerk(x, _y, res =>
          mulk(x, res, k))));

powerk(2, 8, x => {console.log("powerk:", x); return x});

控制倒置

在函数式编程中,您可以通过应用纯(高阶)函数来实现控制的反转。纯函数仅根据其输入生成一个值。没有副作用。调用这种纯函数的调用者可以将生成的值用于进一步处理或丢弃它。调用者决定生成的值如何与程序交互。因此,控制从被调用者反转到调用者。

除此之外,调用者可以使用被调用者的参数将纯函数传递给被调用者。换句话说,调用者可以在被调用者中注入惰性表达式。因此,调用者不仅控制被调用者执行的计算的结果,而且还能够影响该计算本身。

依赖注入

由于范式控制的内在反转,在函数式编程中不需要特定的依赖注入。函数之间的全局依赖关系是完全正确的,只要所有涉及的函数都是纯函数,因此不会修改全局状态。