関数宣言をせずに再帰呼出しをする

この記事は、僕が所属する学生サークル「CASる」が発行する雑誌「CASる通信学祭号」に投稿した記事になります。

関数宣言をせずに再帰呼出しをする

CASる通信学祭号の締切に追われて大急ぎでこの記事を書いています。

この記事では、関数宣言をせずに再帰呼出しをしないといけないことがあったので、その経緯と実装の紹介、そして他の実装で関数宣言をせずに再帰呼び出しする必要性がなかったことの説明をします。夏休みのちょっとしたおもしろ体験を気ままに紹介しているつもりなので、気楽に読んでいってください。

経緯

なんか色々あって、TypeScriptとブラウザを操作するライブラリであるPuppeteerでスクレイピングツールを書いていました。対象を確実にスクレイピングするには、ページの読み込み終了を待機する必要があります。そこで、Page.evaluate()という関数を使って待とうと考えました。Page.evaluate()は引数に渡した関数をPuppeteerを通じてブラウザ上で実行するという関数です。JavaScriptでブラウザによるページの読み込みを待つために以下のような実装をしました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
async function ready() {
  return new Promise((resolve) => {
    const checkReadyState = () => {
      if (document.readyState === "complete") {
        resolve();
      } else {
        setTimeout(checkReadyState, 100);
      }
    };
    checkReadyState();
  })
}

setTimeoutを使ってポーリングして読み込みが完了するまで待ちます。ちなみにwhile (true)を使ってポーリングするとメインスレッドをブロックして読み込みが進みません。

しかし、上記のような実装をしたところPuppeteerでは動作しませんでした。後で軽く検証したところ上記の実装でも動作したので原因は不明ですが、当時はなぜか動作しませんでした。

そこで様々な検証を重ねた結果、関数を宣言しても呼び出すときにはスコープから外れているということがわかりました。非常に謎ですが。なので、関数を宣言せずに同様の処理を実現する必要がありました。

実装

ぶっちゃけた話、僕にそのような処理は思いつかなかったのでGitHub Copilotにお願いしました。

GitHub Copilotにやらせてみる GitHub Copilotにやらせてみる GitHub Copilotにやらせてみる

出来上がった処理はこんな感じです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ページの読み込み完了まで待つ
type SelfFn = (self: SelfFn) => void;
await new Promise<void>((resolve) => {
((self: SelfFn) => {
  if (document.readyState === "complete") {
    resolve();
  } else {
    setTimeout(() => self(self), 100);
  }
})((self) => {
  if (document.readyState === "complete") {
    resolve();
  } else {
    setTimeout(() => self(self), 100);
  }
});
});

ご覧の通り、一発では出来なかったので何度もツッコミを入れました。それにしても引数に全く同じ関数を代入してやることで同様の処理が実現できるとかどういう発想したら思いつくんですかね。

この実装は不要だった

先述した通り、この記事を書くに当たって軽い検証をしてみたのですが、どうしてか当時発生していた問題は再現できませんでした。また、この記事を書くに当たってブラウザの読み込みをsetTimeoutでポーリングして待つ実装をGeminiに確認してもらったら、イベントリスナーを追加する実装を提示してもらいました。ポーリングするより効率的な実装だと思います。

1
2
3
4
5
6
7
8
9
async function loadEvent() {
  return new Promise(resolve => {
    if (document.readyState !== "complete") {
      document.addEventListener("load", resolve, { once: true });
    } else {
      resolve();
    }
  });
}

更に言うなら、そもそもPage.evaluate()の中で待つ必要はなくて、Page.waitForNavigation()を利用すればより安全に読み込みを待つことができることに気づきました。当時は既に実装した内容に思考が引っ張られて、Page.waitForNavigation()を使うことを思いつけませんでした。

参考文献