1. ホーム
  2. ジャバスクリプト

[解決済み】ES6(ECMAScript 6)でミュータブル変数を使わずにx回ループする仕組みはありますか?

2022-04-11 13:08:11

質問

ループの典型的な方法 x 回をJavaScriptで作成します。

for (var i = 0; i < x; i++)
  doStuff(i);

しかし、私は ++ 演算子やミュータブル変数を一切使用しません。では、ES6で、ループする方法はあるのでしょうか? x の回数は別の方法なのでしょうか?Rubyの仕組みが好きなんです。

x.times do |i|
  do_stuff(i)
end

JavaScript/ES6で似たようなものはありますか?私はちょっとズルして自分のジェネレータを作ることができました。

function* times(x) {
  for (var i = 0; i < x; i++)
    yield i;
}

for (var i of times(5)) {
  console.log(i);
}

もちろん、今でも i++ . 少なくとも見えないところではありますが :) 、ES6でもっと良い仕組みがあることを期待しています。

どのように解決するのか?

OK!

以下のコードは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 この関数は、1つの関数の繰り返し適用を開始するために使用される追加の入力を取ります。

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


実装について timesrepeat

まあ、これはもう簡単なことで、ほとんどすべての作業は終わっているのです。

// 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はそれをトランスパイルしていましたが、それは1年以上も"broken; will reimplement"の状態になっています。

repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded

の実装を見直す必要があります。 repeat をスタックセーフにする。

以下のコード が行います。 ミュータブル変数を使用する nx に局在しているが、すべての変異が 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