Inject functions with side-effects

ぃ、小莉子 提交于 2019-12-12 04:25:46

问题


I'm having an issue when using higher-order functions. Let's say I have the following code that doesn't use them (instead call global 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)

And now, the same example, by using higher-order functions (inject side-effects as function arguments):

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)

Imagine this with a larger tree of functions calling each other. The last example would become a big mess of functions passing functions down to each other.

Is this the correct way to isolate side-effects? Am I missing something?


回答1:


I'm having an issue when using higher-order functions. Let's say I have the following code that doesn't use them (instead call global functions):

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

But this is a terrible starting point, to be honest

  • c is just an eta conversion of console.log
  • b is just an eta conversion of c
  • a is just an eta conversion of b

In other words, a === b === c === console.log – If you're going to understand higher-order functions, you need a better starting point


The common example: map

People love to demonstrate higher-order functions using 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]

What's happening here? It's actually pretty neat. We can take an input array xs and create a new array where each element is the transformation of an element in xs using a higher-order function. The higher-order function could be a one-time use lambda, or it could be a named function that was already defined elsewhere.

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

The bigger picture

OK, so that's a very practical example of using higher-order functions in JavaScript, but ...

Am I missing something?

Yes. Higher-order functions have an immensely deep and meaningful power. Let me pose another set of questions:

  • What if there was no such thing as an Array?
  • How would we group values together in a meaningful way?
  • Without a group of values, surely we couldn't map over them, right?

What if I told you only functions were required to do all of it?

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

I you look closely, you'll see this code doesn't use any native data structures provided by JavaScript (with the exception of Number for example data and String for purposes of outputting something to see). No Objects, no Arrays. No tricks. Just Functions.

How does it do that? Where is the data?

In short, the data exists in partially applied functions. Let's focus on one specific piece of code so I can show you what I mean

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

When we create pair using cons(1,2) look carefully how the data gets stored in pair. What form is it in? cons returns a function with x and y bound to the values 1 and 2. This function, we'll call it p, is waiting to be called with another function f, which will apply f to x and y. car and cdr provide that function (f) and return the desired value – in the case of car, x is selected. in the case of cdr, y is selected.

So let's repeat...


"Am I missing something?"

Yes. What you just witnessed with the genesis of a versatile data structure out of nothing but (higher-order) functions.

Is your language missing a particular data structure you might need? Does your language offer first class functions? If you answered 'yes' to both of those, there is no problem because you can materialize the particular data structure you need using nothing but functions.

That is the power of higher-order functions.


Reluctantly convinced

OK, so maybe you're thinking I pulled some tricks there making a replacement for Array above. I assure you, there are no tricks involved.

Here's the API we will make for a new dictionary type, dict

dict (key1, value1, key2, value2, ...) --> d
read (d, key1) --> value1
read (d, key2) --> value2

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

Below, I will keep the same promise of not using anything except functions (and strings for demo output purposes), but this time I will implement a different data structure that can hold key/value pairs. You can read values, write new values, and overwrite existing value based on a key.

This will reinforce the concept of higher-order data, that is data that is an abstraction of a lower abstraction – ie, dict is implemented using node which is implemented using list which is implemented using cons, etc

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

Surely now you see the power of higher-order functions, right ? ^_^




回答2:


Higher order functions

Higher order functions are used to abstract from functions application. A common HOF takes at least one function f and at least one additional argument x and applies f to x. Here is the simplest example:

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

Not particularly interesting. The crucial point is that each meaningful HOF does something extra. It applies for example the given function iteratively (map, reduce). It composes two functions:

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

const inc = x => x + 1;

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

It applies a function to an Object even though this function exclusively accepts Numbers:

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
);

Or it applies a function partially:

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
);

Continuation passing style

Your code pattern _a = (x, b, c) => b(x, c) is actually from continuation passing style. It is a special form of higher order functions where the last argument of a function must always be another function, which represents the continuation, that is, the rest of the current computation. The continuation function isn't fed with an argument of its surrounding function, but with the result of its computation. The continuation function or rather its application is the replacement of the return statement so to speak.

While you will encounter HOFs all along in functional Javascript, continuation passing style is quite rare. When you take a look at the following example, you'll know why:

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});

Inversion of control

In functional programming you achieve inversion of control by applying pure (higher order) functions. A pure function produces a value solely depending on its input. There are no side effects. A caller that invokes such a pure function can either take the produced value for further processing or discard it. The caller decides how the produced value interacts with the program. Hence the control is inverted from the callee to the caller.

Beyond that, the caller can pass pure functions to the callee using the arguments of the callee. In other words, the caller can inject lazy expressions in the callee. Consequently the caller does not only control the result of a computation performed by the callee, but is also able to influence this computation itself.

Dependency Injection

There is no need for a specific dependency injection in functional programming, because of the intrinsic inversion of control of the paradigm. Global dependencies between functions are totally fine, as long as all involved functions are pure and thus don't modify global state.



来源:https://stackoverflow.com/questions/42179513/inject-functions-with-side-effects

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!