I am trying to understand how compose works by recreating compose. As part of that I've created a simple calculator to take a value and based on that value return interest.
https://medium.com/javascript-scene/reduce-composing-software-fe22f0c39a1d#.rxqm3dqje
Essentially ultimate goal is to create a function that can do below.
https://github.com/ngrx/example-app/blob/master/src/app/reducers/index.ts#L150
Nice to have: been able to pass multiple deposit values, and and may be calculate compound interest over time.
It would be good have some comments so I understand what is going on from Functional programming approach.
(()=>{
// Generic compose function to handle anything
const compose = (...fns) => (x) => {
return fns.reduceRight((acc,fn)=>{
return fn(acc);
}, x)
};
const getInterest = (value) => {
if (value < 1000) {
return 1 / 100
}
if (value < 10000) {
return 2 / 100
}
return 3 / 100;
};
const getDeposit = (value) => {
return value;
};
const calculator = compose(getDeposit, getInterest)
console.log(calculator(1000)) // Should return value of 1000 * interest rate. I currently get 0.03
})();
The issue is that you never multiply the two values: value and interest.
You should therefore pass another function into the composition, which will multiply the two previous results.
This means that this function will need to get 2 arguments, while the other two only take one. In general a function could need any number of arguments. So the composer should be able to pass enough arguments to each function. Furthermore, functions may also return more than one value -- in the form of an array. These values should be made available as arguments for the next function in the chain, while keeping any previous returned values available as well.
Another thing is that although you have implemented compose to execute the functions from right to left, the sequence of function you pass seem to suggest you expect them to execute from left to right, first getDeposit, and then getInterest, even though in your case it works both ways. Still, I would suggest to switch their positions.
So here is how you can make all that work:
(()=>{
// Generic compose function to handle anything
const compose = (...fns) => (...args) => {
return fns.reduceRight((acc,fn)=>{
// Call the function with all values we have gathered so far
let ret = fn.apply(null, acc);
// If function returns a non-array, turn it into an array with one value
if (!Array.isArray(ret)) ret = [ret];
// Queue the returned value(s) back into the accumulator, so they can
// serve as arguments for the next function call
acc.unshift(...ret);
return acc;
}, args)[0]; // only return the last inserted value
};
const getInterest = (value) => {
return value < 1000 ? 0.01
: value < 10000 ? 0.02
: 0.03;
};
const multiply = (a, b) => a * b;
const getDeposit = (value) => value;
// Be aware the the rightmost function is executed first:
const calculator = compose(multiply, getInterest, getDeposit);
console.log(calculator(1000)) // Returns 20, which is 0.02 * 1000
})();
Alternative: pass along an object
The above implementation is not a pure compose implementation, since it passes not only the previous function result on to the next, but all previous functions results. This is not disturbing, and opens doors for more complex functions, but if you wanted to stick more to the original compose idea, you have a problem to solve:
As you want to have a function in the chain that only returns the rate, the next function in the chain will then only get the rate -- nothing else. With just that one piece of information it is of course not possible to calculate the result, which also needs the value as input.
You could "solve" this, by letting getInterest return an object, that not only has the rate in it, but also the value that was passed to it. You could also implement this with an array.
Here it is with an object implementation:
(()=>{
// Straightforward implementation:
const compose = (...fns) => (...args) => {
return fns.reduceRight((acc,fn)=>{
return fn(acc);
}, args);
};
// Return an object with two properties: value & interest
const getInterest = (value) => ({
value,
interest: value < 1000 ? 0.01
: value < 10000 ? 0.02
: 0.03
});
// Expect an object as argument, with two properties:
const getInterestAmount = ({value, interest}) => value * interest;
const getDeposit = (value) => value;
// Be aware the the rightmost function is executed first:
const calculator = compose(getInterestAmount, getInterest, getDeposit);
console.log(calculator(1000)) // Returns 20, which is 0.02 * 1000
})();
With this approach you can pass along objects that have many more properties, and so anything becomes possible.
I actually liked your simple compose function (it just only works for unary functions), and I think you can also make it work for now by making these changes:
rename getInterest to ...Rate, since it returns a multiplier for a value.
add a new getInterest function that takes a "rate getter" and a "value" in curried form: getRate => x => getRate(x) * x
swap the order of your calculator arguments in compose
I think compose usually works from right to left (f => g => x
=> f(g(x))), and pipe works from left to right (f => g => x => g(f(x)))
(()=>{
// Generic compose function to handle anything
const compose = (...fns) => (x) => {
return fns.reduceRight((acc,fn)=>{
return fn(acc);
}, x)
};
const defaultRate = (value) => {
if (value < 1000) {
return 1 / 100
}
if (value < 10000) {
return 2 / 100
}
return 3 / 100;
};
const getInterest = getRate => x => getRate(x) * x;
const getDeposit = x => 1000;
const calculator = compose(getInterest(defaultRate), getDeposit);
console.log(calculator());
})();
Related
Can somebody explain to me how Array.reduce can put functions as arguments in function composition like this:
const composeB = (f, g) => x => f(g(x))
const add = a => b => a + b
const add5 = add(5)
const double = a => a * 2
const add5ThenDouble = [double, add5].reduce(composeB)
console.log(add5ThenDouble(6)); // 22
So, according to my knowledge (which is not enough) of reduce function is that Array.reduce iterate through an array like this - it takes each of array values and puts them through callback function with another argument (lets call it accumulator). The next array value will undergo the same callback function, but with (eventually) changed accumulator value.
What confuses me in code example above is:
1) Array is list of functions [double, add5].
2) In first iteration, composeB will receive arguments: f=accumulator (empty value), g=double(function). ComposeB should return emptyvalue(double(6)) (or maybe not??)
I know that I am missing something, but can someone explain me what?
The documentation for reduce says that the first argument is
A function to execute on each element in the array (except for the first, if no initialValue is supplied).
So in this case, you have not supplied an initialValue and so compose is only called once (with arguments double and add5).
var inc = (x) => ++x, // increment +1
x2 = (x) => x*2, // multiply by 2
sq = (x) => x*x; // square
var compose = (f,g) => (_) => g(f(_));
var f1 = [ sq, inc, inc ].reduce(compose, (_) => _);
f1(10); // 102
var f2 = [ inc, sq, inc ].reduce(compose, (_) => _);
f2(10); // 122
See the code above, notice:
identity function (_) => _ as default value (second argument) for reduce
compose will NOT return a number, it will return a FUNCTION that is a composition of all the functions that were passed in before... So - only when you CALL the (say) f1, only then the functions will be executed.
we are putting all the functions from list [a,b,c,d,e] into a chain of e,d,c,b,a (reverse order!) and then execute as e(d(c(b(a(10))))) getting the order we actually wanted.
(f,g) => (_) => g(f(_)) <-- function arguments are actually reversed when calling them. Longer version: function compose (f, g) { return function (z) { return g(f(z)); }; }
p.s.: i use var because i can ;)
I am trying to write a simple compose function that takes a series of functions, and composes them like so:
compose(func1, func2, func3)(n) === func1(func2(func3(n)))
I do so by recursing through a rest parameter,
var compose = function(...args) {
return (n) => {
if (args.length) {
return args[0](compose(args.slice(1)));
} else {
return n;
}
};
};
I then attempt to compose a new function made up of a series of other functions,
var plusOneAndTwo = compose((n) => n + 1, (n) => n + 2);
plusOneAndTwo(1);
Instead of returning 4, I get back the body of the inner, anonymous function inside of compose as a string,
"(n) => {
if (args.length) {
return args[0](compose(args.slice(1)));
} else {
return n;
}
}1"
Note the "1" at the end of the string! I have no idea why this is happening, and in particular I'm perplexed by how a 1 is getting appended to the end of that.
Any clarification is appreciated, thanks!
The problem occurs in the recursive call to compose.
In particular, you are not passing the parameter n to it (as also suggested by others above). Furthermore, you need to expand the rest parameter in the call.
You should use something like:
return args[0](compose(...args.slice(1))(n))
In your case, you are simply returning:
return args[0](compose(args.slice(1)));
In your example you call compose((n) => n + 1, (n) => n + 2);. Compose then returns a function taking n as a parameter. In this function, args.length becomes 1 (i.e. true-is), args[0] becomes (n) => n + 1 and args.slice(1) becomes [(n) => n + 2].
Next, you call this returned function with the parameter n = 1. As args.length is 1, the if() statement will go into the if() case. In this if case, it will call args[0] with the argument compose(args.slice(1)).
In this recursive call, compose(args.slice(1)) is evaluated to a function, taking n as a parameter and the same function body.
This function is then given as the parameter n to args[0] (in the outer call). Recall that args[0] in this scenario is the function (n) => n + 1.
Thus the call as a whole is equivalent to:
// returned from the recursive call to compose(args.slice(1))
var n = (n) => {
if (args.length) {
return args[0](compose(args.slice(1)));
} else {
return n;
}
}
// applied to arg[0] == (n) => n + 1
return n + 1
This means that the code will attempt to add a function with the number 1.
In JavaScript adding a function and a number results in both objects coerced into a string. Casting a number into a string is trivial, casting a function into a string returns the function source code. These strings are then added to give the return value you saw: The function body as a string with the 1 at the end.
You just have to call the composed function:
return args[0](compose(...args.slice(1))(n));
Or without recursion it'll be:
const compose = (...fns) => start => fns.reduceRight((acc, fn) => fn(acc), start);
You could take a different approach by returning a new function or returning the last function for calling with arguments.
const
compose = (...fns) => fns.length
? v => compose(...fns.slice(0, -1))(fns.pop()(v))
: v => v,
fn1 = n => n * 5,
fn2 = n => n + 2,
fn3 = n => n * 7;
console.log(fn1(fn2(fn3(1))));
console.log(compose(fn1, fn2, fn3)(1));
In a recent interview, I was asked to write a function that adds numbers and accepts parameters like this:
add(1)(2)(3) // result is 6
add(1,2)(3,4)(5) // result is 15
The number of parameters is not fixed, and the arguments can be either passed in sets or individually.
How can I implement this add function?
Given your examples, the number of parameters is fixed in some ways.
As #ASDFGerte pointed out, your examples seem to return the result after three invocations. In this case a simple implementation without introducing terms like variadic and currying could be
function add(...args1){
return function(...args2){
return function(...args3){
return args1.concat(args2).concat(args3).reduce((a,b)=>a+b)}}}
console.log(add(1)(2)(3))
console.log(add(1,2)(3,4)(5))
Every invocation accepts a variable number of parameters.
However it would be nice to generalize the construction of this nested functions structure and you can accomplish that with currying.
But if you want to allow an arbitrary number of invocations, when you should stop returning a new function and return the result? There is no way to know, and this is a simple, unaccurate and partial explanation to give you the idea of why they said you cannot accomplish what they asked you.
So the ultimate question is: is it possible that you misunderstood the question? Or maybe it was just a trick to test you
Edit
Another option would be to actually invoke the function when no arguments are passed in, change the call to add(1)(2)(3)()
Here an example recursive implementation
function sum (...args) {
let s = args.reduce((a,b)=>a+b)
return function (...x) {
return x.length == 0 ? s : sum(s, ...x)
};
}
console.log(sum(1,2)(2,3,4)(2)())
At every invocation computes the sum of current parameters and then return a new function that:
if is invoked without parameters just return the current sum
if other numbers are passed in, invokes recursively sum passing the actual sum and the new numbers
I'm a bit late to the party, but something like this would work (a bit hacky though in my opinion):
const add = (a, ...restA) => {
const fn = (b, ...restB) => {
return add([a, ...restA].reduce((x, y) => x + y) + [b, ...restB].reduce((x, y) => x + y))
};
fn.valueOf = () => {
return [a, ...restA].reduce((x, y) => x + y)
};
return fn;
}
This function returns a function with a value of the sum. The tests below are outputing the coerced values instead of the actual functions.
console.log(+add(1,2)(3,4)(5)); // 15
console.log(+add(1)) // 1
console.log(+add(1)(2)) // 3
console.log(+add(1)(2)(3)) // 6
console.log(+add(1)(2)(3)(4)) // 10
Since it's a currying function, it will always return another function so you can do something like this:
const addTwo = add(2);
console.log(+addTwo(5)); // 7
using reduce and spread it can be done as below
function calc(...args1){
return function (...args2){
return function (...args3){
let merge = [...args1, ...args2, ...args3]
return merge.reduce((x ,y)=> x + y) ;
}
}
}
let sum = calc(10)(1)(4);
console.log("sum",sum);
They probably wanted to know how comfortable you were with "javascript internals", such as how and when methods like Function#toString and Function#valueOf, Function#[Symbol.toPrimitive] are called under the hood.
const add = (...numbers) => {
const cadd = (...args) => add(...args, ...numbers);
cadd[Symbol.toPrimitive] = () => numbers.reduce((a, b) => a + b);
return cadd;
}
console.log(
`add(1,2)(3,4)(5) =>`, add(1,2)(3,4)(5),
); // result is 15
console.log(
`add(1,2) =>`, add(1,2),
); // result is 3
console.log(
`add(1,2)(5)(1,2)(5)(1,2)(5)(1,2)(5) =>`, add(1,2)(5)(1,2)(5)(1,2)(5)(1,2)(5),
); // result is 32
import {flow, curry} from 'lodash';
const add = (a, b) => a + b;
const square = n => n * n;
const tap = curry((interceptor, n) => {
interceptor(n);
return n;
});
const trace2 = curry((message, n) => {
return tap((n) => console.log(`${message} is ${n}`), n);
});
const trace = label => {
return tap(x => console.log(`== ${ label }: ${ x }`));
};
const addSquare = flow([add, trace('after add'), square]);
console.log(addSquare(3, 1));
I started by writing trace2 thinking that trace wouldn't work because "How can tap possibly know about n or x whatever?".
But trace does work and I do not understand how it can “inject” the x coming from the flow into the tap call. Any explanation will be greatly appreciated :)
Silver Spoon Evaluation
We'll just start with tracing the evaluation of
addSquare(3, 1) // ...
Ok, here goes
= flow([add, trace('after add'), square]) (3, 1)
add(3,1)
4
trace('after add') (4)
tap(x => console.log(`== ${ 'after add' }: ${ x }`)) (4)
curry((interceptor, n) => { interceptor(n); return n; }) (x => console.log(`== ${ 'after add' }: ${ x }`)) (4)
(x => console.log(`== ${ 'after add' }: ${ x }`)) (4); return 4;
console.log(`== ${ 'after add' }: ${ 4 }`); return 4;
~log effect~ "== after add: 4"; return 4
4
square(4)
4 * 4
16
= 16
So the basic "trick" you're having trouble seeing is that trace('after add') returns a function that's waiting for the last argument. This is because trace is a 2-parameter function that was curried.
Futility
I can't express how useless and misunderstood the flow function is
function flow(funcs) {
const length = funcs ? funcs.length : 0
let index = length
while (index--) {
if (typeof funcs[index] != 'function') {
throw new TypeError('Expected a function')
}
}
return function(...args) {
let index = 0
let result = length ? funcs[index].apply(this, args) : args[0]
while (++index < length) {
result = funcs[index].call(this, result)
}
return result
}
}
Sure, it "works" as it's described to work, but it allows you to create awful, fragile code.
loop thru all provided functions to type check them
loop thru all provided functions again to apply them
for some reason, allow for the first function (and only the first function) to have special behaviour of accepting 1 or more arguments passed in
all non-first functions will only accept 1 argument at most
in the event an empty flow is used, all but your first input argument is discarded
Pretty weird f' contract, if you ask me. You should be asking:
why are we looping thru twice ?
why does the first function get special exceptions ?
what do I gain with at the cost of this complexity ?
Classic Function Composition
Composition of two functions, f and g – allows data to seemingly teleport from state A directly to state C. Of course state B still happens behind the scenes, but the fact that we can remove this from our cognitive load is a tremendous gift.
Composition and currying play so well together because
function composition works best with unary (single-argument) functions
curried functions accept 1 argument per application
Let's rewrite your code now
const add = a => b => a + b
const square = n => n * n;
const comp = f => g => x => f(g(x))
const comp2 = comp (comp) (comp)
const addSquare = comp2 (square) (add)
console.log(addSquare(3)(1)) // 16
"Hey you tricked me! That comp2 wasn't easy to follow at all!" – and I'm sorry. But it's because the function was doomed from the start. Why tho?
Because composition works best with unary functions! We tried composing a binary function add with a unary function square.
To better illustrate classical composition and how simple it can be, let's look at a sequence using just unary functions.
const mult = x => y => x * y
const square = n => n * n;
const tap = f => x => (f(x), x)
const trace = str => tap (x => console.log(`== ${str}: ${x}`))
const flow = ([f,...fs]) => x =>
f === undefined ? x : flow (fs) (f(x))
const tripleSquare = flow([mult(3), trace('triple'), square])
console.log(tripleSquare(2))
// == "triple: 6"
// => 36
Oh, by the way, we reimplemented flow with a single line of code, too.
Tricked again
OK, so you probably noticed that the 3 and 2 arguments were passed in separate places. You'll think you've been cheated again.
const tripleSquare = flow([mult(3), trace('triple'), square])
console.log(tripleSquare(2)) //=> 36
But the fact of the matter is this: As soon as you introduce a single non-unary function into your function composition, you might as well refactor your code. Readability plummets immediately. There's absolutely no point in trying to keep the code point-free if it's going to hurt readability.
Let's say we had to keep both arguments available to your original addSquare function … what would that look like ?
const add = x => y => x + y
const square = n => n * n;
const tap = f => x => (f(x), x)
const trace = str => tap (x => console.log(`== ${str}: ${x}`))
const flow = ([f,...fs]) => x =>
f === undefined ? x : flow (fs) (f(x))
const addSquare = (x,y) => flow([add(x), trace('add'), square]) (y)
console.log(addSquare(3,1))
// == "add: 4"
// => 16
OK, so we had to define addSquare as this
const addSquare = (x,y) => flow([add(x), trace('add'), square]) (y)
It's certainly not as clever as the lodash version, but it's explicit in how the terms are combined and there is virtually zero complexity.
In fact, the 7 lines of code here implements your entire program in less than it takes to implement just the lodash flow function alone.
The fuss, and why
Everything in your program is a trade off. I hate to see beginners struggle with things that should be simple. Working with libraries that make these things so complex is extremely disheartening – and don't even get me started on Lodash's curry implementation (including it's insanely complex createWrap)
My 2 cents: if you're just starting out with this stuff, libraries are a sledgehammer. They have their reasons for every choice they made, but know that each one involved a trade off. All of that complexity is not totally unwarranted, but it's not something you need to be concerned with as a beginner. Cut your teeth on basic functions and work your way up from there.
Curry
Since I mentioned curry, here's 3 lines of code that replace pretty much any practical use of Lodash's curry.
If you later trade these for a more complex implementation of curry, make sure you know what you're getting out of the deal – otherwise you just take on more overhead with little-to-no gain.
// for binary (2-arity) functions
const curry2 = f => x => y => f(x,y)
// for ternary (3-arity) functions
const curry3 = f => x => y => z => f(x,y,z)
// for arbitrary arity
const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)
Two types of function composition
One more thing I should mention: classic function composition applies functions right-to-left. Because some people find that hard to read/reason about, left-to-right function composers like flow and pipe have showed up in popular libs
Left-to-right composer, flow, is aptly named because your eyes will flow in a spaghetti shape as your try to trace the data as it moves thru your program. (LOL)
Right-to-left composer, composer, will make you feel like you're reading backwards at first, but after a little practice, it begins to feel very natural. It does not suffer from spaghetti shape data tracing.
I am working in TS but will show the tsc -> ES6 code below.
I have a function 'isDigit' that returns true if the the character code is in the range of digits 0-9. This function (isDigit) must be passed as an argument into a higher order function.
const isDigit = (char, charC = char.charCodeAt(0)) => (charC > 47 && charC < 58);
As part of another higher order function, I need to know if a character is NOT a digit. Of course something like below would work...
const notDigit = (char, charC = char.charCodeAt(0)) => !isDigit(char);
BUT it would be more satisfying if I could compose isDigit with another function (I'll call notFun) to apply a not operator to the result of isDigit to make notDigit. In the code below 'boolCond' serves to control if the not operator is applied or not. The code below 'almost' works, but it returns a boolean not a function which does not work when dealing with higher order functions.
const notFun = (myFun, boolCond = Boolean(condition)) => (boolCond) ? !myFun : myFun;
As is usually the case, while preparing this question I wound up finding an answer, so I will share my answer and see what improvements come from the community.
The issue observed above (getting a boolean instead of a function) is an issue of 'functional composition, I found several optional approaches in the post of Eric Elliot, from which I selected the 'pipe' functional composition method.
see Eric Elliot's excellent post
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
The implementation of this pipe composition function looked like the below TS... For those following along at home, I have included the recursive count while 'recCountWhile' function that is the ultimate consumer of the composed (i.e. piped) function (please excuse the inverted order that these functions appear but this was done for clarity).
export const removeLeadingChars: (str: string) => string =
(str, arr = str.split(''),
dummy = pipe(isDigit, notFun), cnt = recCountWhile(arr, dummy, 0)) =>
arr.slice(cnt).reduce((acc, e) => acc.concat(e),'');
export const recCountWhile: (arr: string[], fun: (char: string) => boolean, sum: number) => number =
(arr, fun, sum, test = (!(arr[0])) || !fun(arr[0]) ) =>
(test) ? sum : recCountWhile(arr.slice(1), fun, sum + 1);
The result is a composed function 'removeLeadingChars' that composes the 'isDigit' with the 'notFun' (using the pipe function ) into 'dummy' function that is passed to the recCountWhile function. This returns the count of 'not digits' (i.e. characters other than digits) that lead the string, these characters that are then 'sliced' from the head of the array, and the array is reduced back to a string.
I would be very keen to hear about any tweaks that may improve on this approach.
Good on you to find your answer and still post the question. I think this is a nice way to learn.
For the sake of a function composition exercise, here's how I might structure your functions.
see Keep it simple below for how I would handle this with practical code
const comp = f => g => x => f(g(x))
const ord = char => char.charCodeAt(0)
const isBetween = (min,max) => x => (x >= min && x <= max)
const isDigit = comp (isBetween(48,57)) (ord)
const not = x => !x
const notDigit = comp (not) (isDigit)
console.log(isDigit('0')) // true
console.log(isDigit('1')) // true
console.log(isDigit('2')) // true
console.log(isDigit('a')) // false
console.log(notDigit('0')) // false
console.log(notDigit('a')) // true
Code review
Btw, this thing you're doing with the default parameters and leaked private API is pretty wonky
// charC is leaked API
const isDigit = (char, charC = char.charCodeAt(0)) => (charC > 47 && charC < 58);
isDigit('9') // true
isDigit('9', 1) // false wtf
isDigit('a', 50) // true wtf
I understand you're probably doing it so you don't have to write this
// I'm guessing you want to avoid this
const isDigit = char => {
let charC = char.charCodeAt(0)
return charC > 47 && charC < 58
}
... but that function is actually a lot better because it doesn't leak private API (the charC var) to the external caller
You'll notice the way I solved this in mine was to use my own isBetween combinator and currying which results in a pretty clean implementation, imo
const comp = f => g => x => f(g(x))
const ord = char => char.charCodeAt(0)
const isBetween = (min,max) => x => (x >= min && x <= max)
const isDigit = comp (isBetween(48,57)) (ord)
More of your code that does this awful default parameters thing
// is suspect you think this is somehow better because it's a one-liner
// abusing default parameters like this is bad, bad, bad
const removeLeadingChars: (str: string) => string =
(str, arr = str.split(''),
dummy = pipe(isDigit, notFun), cnt = recCountWhile(arr, dummy, 0)) =>
arr.slice(cnt).reduce((acc, e) => acc.concat(e),'');
Try to avoid compromising the quality of your code for the sake of making everything a one-liner. The above function is much worse than this one here
// huge improvement in readability and reliability
// no leaked variables!
const removeLeadingChars: (str: string) => string = (str) => {
let arr = str.split('')
let dummy = pipe(isDigit, notFun)
let count = recCountWhile(arr, dummy, 0)
return arr.slice(count).reduce((acc, e) => acc.concat(e), '')
}
Keep it simple
Instead of splitting the string into an array, then iterating over the array to count the leading non digits, then slicing the head of the array based on the count, then finally reassembling the array into an output string, you can... keep it simple
const isDigit = x => ! Number.isNaN (Number (x))
const removeLeadingNonDigits = str => {
if (str.length === 0)
return ''
else if (isDigit(str[0]))
return str
else
return removeLeadingNonDigits(str.substr(1))
}
console.log(removeLeadingNonDigits('hello123abc')) // '123abc'
console.log(removeLeadingNonDigits('123abc')) // '123abc'
console.log(removeLeadingNonDigits('abc')) // ''
So yeah, I'm not sure if the code in your question was merely there for an exercise, but there's really a much simpler way to remove leading non digits from a string, if that's really the end goal.
The removeLeadningNonDigits function provided here is pure function, does not leak private variables, handles all inputs for its given domain (String), and maintains an easy-to-read style. I would suggest this (or something like this) compared to the proposed solution in your question.
Function Composition and "Pipe"
Composing two functions is usually done in right-to-left order. Some people find that hard to read/reason about, so they came up with a left-to-right function composer and most people seem to agree that pipe is a good name.
There's nothing wrong with your pipe implementation, but I think it's nice to see how if you strive to keep things as simple as possible, the resulting code cleans up a bit.
const identity = x => x
const comp = (f,g) => x => f(g(x))
const compose = (...fs) => fs.reduce(comp, identity)
Or if you'd like to work with my curried comp presented earlier in the post
const identity = x => x
const comp = f => g => x => f(g(x))
const uncurry = f => (x,y) => f(x)(y)
const compose = (...fs) => fs.reduce(uncurry(comp), identity)
Each of these functions have their own independent utility. So if you define compose in this way, you get the other 3 functions for free.
Contrast this to the pipe implementation provided in your question:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
Again, it's fine, but it mixes all of these things together in one function.
(v,f) => f(v) is useful function on its own, why not define it separately and then use it by name?
the => x is merging the identity function which has countless uses