Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

zx を使っていい感じに SIGINT を捌く

シェルスクリプトで ctrl + C を押して中断したときのクリーンアップ処理を書くのはそこそこ大変です(頑張ればできますが)。今回は zx を使った、クリーンアップ処理を含むコードの書き方を紹介します。

そもそも zx とは?

alt シェルスクリプト的なものです。JS ファイル内にシェルスクリプトを書くことができます。

import { $ } from 'zx';
const has = (command) => $`type "${command}" > /dev/null 2>&1`.then(() => true, () => false)

if (await has('rbenv')) {
  await $`rbenv init -`;
}

…まあ上のコードが読みやすいかはともかく、 async/await が使えたり、JS の各種メソッドが使えるので非常に便利です。

zx を使って SIGINT を捌いてみる

この processWithCleanup という関数を使ってコードを書いていきます。

import { $ } from 'zx';
import process from 'process';
const processWithCleanup = async (f, cleanup) => {
  process.on('SIGINT', () => {});
  try {
    await f();
  } catch (e) {
    await cleanup();
    throw e;
  }
};

この関数は、受け取った関数 f を実行し、SIGINT を受信したら cleanup を実行します(SIGINT かどうか判別してないですが)。cleanup 後に throw e しているので、このあとどう処理するか任せることができます。

実例

const main = async () => {
  await processWithCleanup(
    async () => {
      await $`git stash`;
      await $`docker build -t hoge ./`;
    },
    () => $`git stash pop`
  );
  $`git stash pop`;
  await $`docker push hoge`;

  await $`deploy`;
};
main().catch(console.error);

この例ではビルドして Docker Hub へプッシュし、最後にデプロイコマンドを実行します。ビルド中にエラーが起きたらデプロイは走りません。

pubsub パターンに対する思いと現時点でのベタープラクティス

なんらかの payload を渡したいとき、インターフェイスを定められない。これに尽きる。静的型付けとも相性が悪い。

payload と言ってるのは要するに window.addEventListener(eventName, event => {}) の event のことだ。TypeScript はかなり頑張って型付けしてくれているが、もし pubsub.addEventListener(event, () => {}) を自作したら型を付けるのがしんどいことは想像に難くない。window.addEventListener もカスタムイベントの event に型をつけるのは面倒だ。

多分ベタープラクティス

ベストではない気がするのでベター。

// 外部にエクスポートしない。
class PubSub {
  private listeners: { [key in PropertyKey]?: ((payload?: any) => void)[] } =
    {};
  addEventListener(eventName: string, callback: (payload?: any) => void) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName]?.push(callback);
  }
  removeEventListener(eventName: string, callback: (payload?: any) => void) {
    this.listeners[eventName] = this.listeners[eventName]?.filter(
      (item) => item !== callback
    );
  }
  dispatchEvent(eventName: string, payload?: any) {
    for (const listener of this.listeners[eventName] || []) {
      listener(payload);
    }
  }
}

const clickEvent = 'BUTTON_CLICK';
const buttonPubsub = new PubSub();
export const buttonObserver = {
  click: () => buttonPubsub.dispatchEvent(clickEvent),
  addClickListener: (callback: () => void) => {
    buttonPubsub.addEventListener(clickEvent, callback);
    return () => buttonPubsub.removeEventListener(clickEvent, callback);
  },
};

このように、buttonOberver を介して PubSub を操作することで、addEventListener と dispatchEvent に対して適切な型をつけられる。さらに、PubSub インスタンスを都度生成すれば、他の Observer とカスタムイベント名が衝突しない。

ただし、PubSub クラスを外部へ露出させない都合上、モジュールへの切り出しが行えないデメリットがある。eslint-plugin-import などでアクセス制限を敷いても良いが、規模感次第だろう。また、テストをする際もステートを都度リセットしなければならず、若干コード量が増える。

in-place merge sort を実装してみた

こちらの論文を参考に書きました。

http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=D04E90C1CB92030C1B92452FB9E192A0?doi=10.1.1.22.8523&rep=rep1&type=pdf

実装

さくっと実装をまずお見せします。

const mergeSort = (arr: number[], L = 0, R = arr.length) => {
  if (R - L <= 1) {
    return;
  }
  const mid = L + Math.floor((R - L) / 2);
  mergeSort(arr, mid, R); // Qをソート

  for (let r = mid; L < r; ) {
    if (r - L === 1) {
      // Pのサイズが1のときは愚直にバブルさせる
      for (let i = L; i + 1 < R && arr[i] > arr[i + 1]; i++) {
        [arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
      }
      break;
    }
    const p1Size = L + Math.floor((r - L) / 2),
      p2Size = L + Math.ceil((r - L) / 2);
    mergeSort(arr, L, p1Size); // P1 をソート
    // P2 をワーキングエリアにしてソートしていく
    for (let i = p1Size, p1 = L, p2 = p2Size, q = r; i > 0; ) {
      if (q < R && arr[q] < arr[p1]) {
        [arr[p2++], arr[q++]] = [arr[q], arr[p2]];
        continue;
      }
      [arr[p1++], arr[p2++]] = [arr[p2], arr[p1]];
      i--;
    }
    // P1 + Q が新たなQとなるので、rを更新
    r = p2Size;
  }
};

なにをやっているのか

  1. 配列を半分にわけ、左をP、右をQとする。
  2. Q をソート
  3. P をさらに半分にわけ、左をP1、右をP2とする。
  4. P1 をソート
  5. P2 を利用して P1 と Q をマージ(詳しくは後述)
  6. マージすると、[P2, P1 と Q をマージしたもの] という並びになるので、P2 を新しいP、 P1+Q を新しい Q として3から繰り返す
  7. P のサイズが1になったら愚直にバブルソートを行い終了

こんな感じで、作業スペースをつくることでうまく in-place でマージできます。

マージ処理について

マージはそこまで難しくないです。

  1. P1 と Q の先頭の要素を比較する
  2. 小さいほうと P2 の先頭の要素をスワップ(ただし、P1 と P2 のサイズが異なる場合、スワップするのはP2の最初から2番目の要素)
  3. これを [P2, P1 と Q をマージしたもの] となるまで繰り返す

こんな感じです。

論文ではさらに賢いやり方が紹介されています(スワップ操作の計算量を落とすテクなど)。ただ、in-place マージソートの実装ができればいいやと思ってあんまりちゃんと読んでないです…

lookahead lookbehind(先読み、後読み)まとめ

  • lookahead (?=pattern)
  • negative lookahead (?!pattern)
  • lookbehind (?<=pattern)
  • negative lookbehind (?<!pattern)

lookahead はこの後の文章から判断する。lookbehind はここまでの文章から判断する。以上。

(?<!Promise<)void(?!>)
(?<=Promise<)void(?=>)

1年に100回くらい「先読みとあと読みってどっちがどっちだ?どっちがどういう記法だっけ?」って悩むのでほんといい加減にしてほしい

軽量な summary/details を作りたい

detailsを閉じているときに DOM を消しておきたいので作りました(作成時間5分)

const Details: VFC<{ summary: ReactNode; detail: () => ReactNode }> = ({
  summary,
  detail,
}) => {
  const [showsDetail, setShowsDetail] = useState(false);
  return (
    <details open={showsDetail} onToggle={() => setShowsDetail((v) => !v)}>
      <summary tw="cursor-pointer select-none">{summary}</summary>
      {showsDetail && detail()}
    </details>
  );
};

<Details summary="サマリーの文言" detail={()=> <div>detail</div>}>

特に解説することもないです。