This solution works but uses state, global variables and function object trickery which is not ideal.
Good hunch. There's certainly some weird stuff going on with your current implementation.
My question here is whether there is a way to solve the problem in a purely recursive way.
Yep! You certainly can. It's a little tricky but we can make light work of it using a couple helper functions identity
and sumk
.
sumk
uses a continuation to keep a stack of the pending add computations and unwinds the stack with 0
whenever the first ()
is called.
const identity = x => x
const sumk = (x,k) =>
x === undefined ? k(0) : y => sumk(y, next => k(x + next))
const sum = x => sumk(x, identity)
console.log(sum()) // 0
console.log(sum(1)()) // 1
console.log(sum(1)(2)()) // 3
console.log(sum(1)(2)(3)()) // 6
console.log(sum(1)(2)(3)(4)()) // 10
console.log(sum(1)(2)(3)(4)(5)()) // 15
To make sense of this, remember sumk
takes a continuation as an argument. When a Number is given, we recurse sumk
with a newly created a continuation that is the sum of the given Number and whatever number comes next. When the Number input is finally undefined
, we end the chain of additions with an empty Number (0). Finally the computation is complete and sent to the original continuation provided by sum
, the identity
function. Since identity
just reflects its input, the computed sum will be the final return value.
I think a line-by-line evaluation really helps understand the process of a function. I'll walk you thru a the evaluation of the sum of 3 numbers. When I use the substitution model, notice I'm alpha renaming the parameter generated in lambda.
// instead of:
next => k(x + next)
// you'll see
A => k(x + A)
B => k(x + B)
C => k(x + C)
This renaming of the bound variable just helps you read the code better when the lambdas become nested.
OK, so here we go !
sum(1)(2)(3)()
= sumk(1, identity)(2)(3)()
= (y => sumk(y, A => identity(1 + A)))(2)(3)()
= sumk(2, A => identity(1 + A))(3)()
= (y => sumk(y, B => (A => identity(1 + A))(2 + B)))(3)()
= sumk(3, B => (A => identity(1 + A))(2 + B))()
= (y => sumk(y, C => (B => (A => identity(1 + A))(2 + B))(3 + C)))()
= sumk(undefined, C => (B => (A => identity(1 + A))(2 + B))(3 + C))
= (C => (B => (A => identity(1 + A))(2 + B))(3 + C))(0)
= (B => (A => identity(1 + A))(2 + B))(3 + 0)
= (B => (A => identity(1 + A))(2 + B))(3)
= (A => identity(1 + A))(2 + 3)
= (A => identity(1 + A))(5)
= identity(1 + 5)
= identity(6)
= 6
And finally, if you're not too keen on having sumk
in the global scope, you can nest it as an auxiliary function inside sum
itself
const identity = x => x
const sum = x => {
const aux = (x,k) =>
x === undefined ? k(0) : y => aux(y, next => k(x + next))
return aux(x, identity)
}
sum(1)(2)(3)() // 6
This was a really fun question and I hope you learn a lot from the answer. If you need any other help, just ask ^_^
EDIT: I see another answer uses currying to achieve the same goal. I didn't originally think to solve the problem this way, so it's cool to see multiple approaches being used. To iterate on that implementation, I might do it something like this
// credit to alebianco for the currying idea
const sum = x => y =>
y === undefined ? x : sum (x + y)
console.log(sum()) // OOPS!
console.log(sum(1)()) // 1
console.log(sum(1)(2)()) // 3
console.log(sum(1)(2)(3)()) // 6
console.log(sum(1)(2)(3)(4)()) // 10
console.log(sum(1)(2)(3)(4)(5)()) // 15
That ends up being quite elegant. But this currying solution actually has a problem with the following corner case
// should return 0, but always returns a function
console.log(sum())
// y => y === undefined ? x : sum (x + y)
Not really a big issue, but the sumk
solution I provided above does not suffer from this.