还行!
下面的代码是使用ES6语法编写的,但可以很容易地用ES5甚至更少。ES6不需要创建“循环x次的机制”
如果回调中不需要迭代器,这是最简单的实现
const times = x => f => {
if (x > 0) {
f()
times (x - 1) (f)
}
}
// use it
times (3) (() => console.log('hi'))
// or define intermediate functions for reuse
let twice = times (2)
// twice the power !
twice (() => console.log('double vision'))
如果确实需要迭代器,则可以使用带有计数器参数的命名内部函数来迭代
const times = n => f => {
let iter = i => {
if (i === n) return
f (i)
iter (i + 1)
}
return iter (0)
}
times (3) (i => console.log(i, 'hi'))
如果您不喜欢学习更多的东西,请停止阅读...
但是应该对那些人感到有些不对劲...
- 单分支语句很丑陋 — 另一个分支上会发生什么?
if
- 函数体中的多个语句/表达式 — 过程问题是否混合在一起?
- 隐式返回 — 指示不纯的副作用函数
undefined
“难道没有更好的方法吗?”
有。让我们首先重新审视我们的初始实现
// times :: Int -> (void -> void) -> void
const times = x => f => {
if (x > 0) {
f() // has to be side-effecting function
times (x - 1) (f)
}
}
当然,这很简单,但请注意我们如何打电话,不要用它做任何事情。这确实限制了我们可以重复多次的函数类型。即使我们有可用的迭代器,也不是更通用的。f()
f(i)
如果我们从一种更好的函数重复过程开始呢?也许是更好地利用输入和输出的东西。
泛型函数重复
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// power :: Int -> Int -> Int
const power = base => exp => {
// repeat <exp> times, <base> * <x>, starting with 1
return repeat (exp) (x => base * x) (1)
}
console.log(power (2) (8))
// => 256
上面,我们定义了一个泛型函数,该函数需要一个额外的输入,用于启动单个函数的重复应用。repeat
// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)
// is the same as ...
var result = f(f(f(x)))
重复
实现时间
好吧,现在这很容易;几乎所有的工作都已经完成。
// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
if (n > 0)
return repeat (n - 1) (f) (f (x))
else
return x
}
// times :: Int -> (Int -> Int) -> Int
const times = n=> f=>
repeat (n) (i => (f(i), i + 1)) (0)
// use it
times (3) (i => console.log(i, 'hi'))
由于我们的函数作为输入并返回,这有效地充当了我们每次传递给的迭代器。i
i + 1
f
我们也修复了问题的项目符号列表
- 不再有丑陋的单分支语句
if
- 单一表达主体表示分离的关注点
- 不再无用,隐式返回
undefined
JavaScript 逗号运算符,
如果你在看到最后一个例子是如何工作的时遇到困难,这取决于你对JavaScript最古老的战斧之一的认识;逗号运算符 – 简而言之,它从左到右计算表达式并返回上次计算表达式的值
(expr1 :: a, expr2 :: b, expr3 :: c) :: c
在上面的例子中,我使用
(i => (f(i), i + 1))
这只是一种简洁的写作方式
(i => { f(i); return i + 1 })
尾部呼叫优化
尽管递归实现很性感,但在这一点上,我推荐它们是不负责任的,因为我能想到的JavaScript VM都不支持适当的尾部调用消除 - babel用于转译它,但它已经处于“破碎;将重新实现“状态超过一年。
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
因此,我们应该重新审视我们的实现,以使其堆栈安全。repeat
下面的代码确实使用了可变变量,但请注意,所有突变都局限于函数 - 从函数外部看不到状态变化(突变)。n
x
repeat
// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
{
let m = 0, acc = x
while (m < n)
(m = m + 1, acc = f (acc))
return acc
}
// inc :: Int -> Int
const inc = x =>
x + 1
console.log (repeat (1e8) (inc) (0))
// 100000000
这会让你很多人说“但那不起作用!”——我知道,放松一下。我们可以使用纯表达式实现 Clojure 样式 /接口,用于常量空间循环;没有这些东西。loop
recur
while
在这里,我们抽象出我们的函数 - 它寻找一种特殊的类型来保持循环运行。遇到非类型时,循环完成并返回计算结果while
loop
recur
recur
const recur = (...args) =>
({ type: recur, args })
const loop = f =>
{
let acc = f ()
while (acc.type === recur)
acc = f (...acc.args)
return acc
}
const repeat = $n => f => x =>
loop ((n = $n, acc = x) =>
n === 0
? acc
: recur (n - 1, f (acc)))
const inc = x =>
x + 1
const fibonacci = $n =>
loop ((n = $n, a = 0, b = 1) =>
n === 0
? a
: recur (n - 1, b, a + b))
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100)) // 354224848179262000000