Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Promiseを使ったループ処理 (TypeScript編)

More than 3 years have passed since last update.

以前も同じような記事を書いていたんですけど、最近TypeScript覚えたし、もう少しいい感じに書けないかな?と思ったので新しい記事にしてみました。

for ループ

まずは普通にループを書く。こんな感じの処理をしたい。

for (let i = 0; i < 10; i++) {
  console.log(i);
}
console.log('END!!!');

まず、ループの各部分を別々の関数として切り出すことを考えます。

for (init(); condition(); increment()) {
  loopBody();
}

次に、これらの関数がPromiseを返すとしたらどのようになるかを考えます。
…考えました。考えた結果がこちらです。

loop.ts
export interface LoopCallback {
  init(): Promise<any>
  condition(): Promise<boolean>
  loopBody(): Promise<any>
  increment(): Promise<any>
}

export function loop(callback: LoopCallback) {
  return callback.init()
    .then(function _loop() {
      return callback.condition()
        .then(result => {
          if (result) {
            return callback.loopBody()
              .then(_ => callback.increment())
              .then(_loop);
          }
        });
    });
}
使用例
import { loop, LoopCallback } from './loop';

let i;
loop({
  init: () => {
    i = 0;
    return Promise.resolve();
  },
  condition: () => {
    return Promise.resolve(i < 10);
  },
  increment: () => {
    i++;
    return Promise.resolve();
  },
  loopBody: () => {
    console.log(i);
    return Promise.resolve();
  }
}).then(() => {
  console.log('END!!!');
})

前回の記事では複数の関数を渡していましたが、今回はinterfaceを定義して一つのオブジェクトを渡すようにしてみました。
この方法のメリットは、ループの過程で変化していく状態などをクラスの中に隠蔽できることです。例えば以下のようにしてカウンター変数をクラスのプライベート変数に書くことができます。

class LoopImpl implements LoopCallback {
  private counter: number;

  init(): Promise<any> {
    this.counter = 0;
    return Promise.resolve();
  }

  condition(): Promise<boolean> {
    return Promise.resolve(this.counter < 10);
  }

  loopBody(): Promise<any> {
    console.log(this.counter);
    return Promise.resolve();
  }

  increment(): Promise<any> {
    this.counter++;
    return Promise.resolve();
  }
}
使用例
loop(new LoopImpl())
  .then(_ => console.log('END!!!'));

配列の中身を処理する場合

普通に配列の中身を同期処理する場合はこのような感じで書けます。

let array: [] = getSomeArray();
array.forEach((data, index) => {
  // 要素ごとの処理
});

しかし要素ごとの処理が非同期処理の場合は、このままでは処理が終わったことを検知できません。

let array: [] = getSomeArray();
array.forEach((data, index) => {
  someMethod(data)
    .then(_=> {
      // ...
    });
});
// arrayの要素全てを処理し終えた場合は??

そこでPromise.all()を使います。Promise.all()はPromiseの配列を受け取り、全てのPromiseがresolveされたときにresolveされるPromiseを返します。

let promises = [...];
Promise.all(promises)
  .then(results => {
    // promises全てが終わった時の処理
  });

つまり、配列の要素一つ一つに関する処理をPromiseを返す関数で表現して、そのPromiseを全て配列に入れてPromise.all()に渡せば、配列全ての要素が終わった時にresolveされるPromiseができます。

let array: [] = getSomeArray();
let promises = [];

array.forEach((data, index) => {
  promises.push(someMethod(data));
});

Promise.all(promises)
  .then(_=>{
    // 配列の要素を全て処理した後
  });

配列の各要素を別の値に置き換えるArray.mapを使うともう少し簡単に書けます。

let array: [] = getSomeArray();
let promises = array.map((data, index) => {
  return someMethod(data);
});

Promise.all(promises)
  .then(_=>{
    // 配列の要素を全て処理した後
  });

ところで、この方法で配列の各要素を処理する場合、各要素の処理(上記の例ではsomeMethod(data))は全て同時に実行されます(厳密に言うとマルチスレッドではないので本当に同時ではないのですが、感覚としては同時です)。
通常はこの方法で問題ないのですが、時には各要素を順番に処理したいときがあります。こういう時は以下のようにPromiseをthenでつなげる必要があります。

someMethod(item1)
  .then(_=>someMethod(item2))
  .then(_=>someMethod(item3))
  ...

Array.reduceを使うと、これがうまく表現できます。うまく表現した結果がこちらです。

function sequentialForEach<T>(array: T[], fn: (item: T, index: number, array: T[]) => Promise<any>): Promise<any> {
  return array.reduce((promise, item, index, array) => {
    return promise.then(_ => fn(item, index, array))
  }, Promise.resolve());
}

上記sequentialForEach関数には配列とコールバック関数を渡します。コールバック関数には配列の要素、インデックス、配列そのものが引数として渡されます。

使用例
// setTimeoutをPromiseでラップするだけの関数
function wait(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time)
  })
}

// 待機時間の配列
let nums = [
  1000, 1500, 2000
]

sequentialForEach(nums, (num) => {
  console.log('start waiting')
  return wait(num)
    .then(_ => console.log('waited ' + num + ' ms'))
})
// 全ての要素を処理し終えた時
.then(_ => console.log('END'))

ついでに、先程の同時に実行するパターンも関数化しておきましょうか。

function parallelForEach<T, R>(array: T[], fn: (item: T, index: number, array: T[]) => Promise<R>): Promise<R[]> {
  let promises = array.map(fn);
  return Promise.all(promises);
}

こちらのparallelForEach関数も使い方はsequentialForEach関数と同じです。

よくよく見ると戻り値の型が違ったりしますけど……sequentialForEachで処理結果の配列を受け取るようにしようと思ったら結構コードが複雑になってしまったのでやめました。こうしたらできるよ!っていうのがありましたら教えてください。

niusounds
REALITYで活動中のスマホアプリエンジニアです。シンフォニックメタルとフォークメタルが好き。 VSCodeおじさん。
https://niusounds.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away