问题
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 ofconsole.log
b
is just an eta conversion ofc
a
is just an eta conversion ofb
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 Object
s, no Array
s. No tricks. Just Function
s.
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 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
);
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