🍔

複数の条件によって変数の値を上書きする処理をletを使わずに書く

2022/11/14に公開約3,000字2件のコメント

JavaScriptで変数を宣言するときは可能な限り let ではなく const を使うようにしましょう、と言われて久しい昨今です。
そして let で宣言した変数に再代入を行うようなコードに対しても、少し工夫して const だけを使うように書き換えられるパターンも存在します。

例えばこんなコードを考えてみましょう:

let message;

if (Math.random() < 0.5) {
  message = "low";
} else {
  message = "high";
}

console.log(message);

条件によって message の値を変えるというコードです。これを let を用いずに実現するなら、まず1つの案としては三項演算子を使う方法が挙げられます:

const message = Math.random() < 0.5 ? "low" : "high";
console.log(message);

またJavaScriptの if は式ではなく文ですが、if文の部分を即時実行関数式(IIFE)にしてしまうという手もあります:

const message = (() => {
  if (Math.random() < 0.5) {
    return "low";
  } else {
    return "high";
  }
})();

console.log(message);

複数の条件がある場合

複数の条件によって変数の値を上書きしていくような場合は一筋縄では行きません。
例えばこんなコードを考えてみましょう:

let numbers = [1, 2, 3, 4, 5];

if (Math.random() < 0.5) {
  numbers = numbers.map((n) => n * 2);
}

if (Math.random() < 0.3) {
  numbers = numbers.filter((n) => n > 3);
}

console.log(numbers);

これを let を使わずに書くとしたら、方法の1つとしてはIIFEがコールバック関数を引数に取るようにして後続の条件を無名関数として渡す、といった書き方があるでしょう:

const numbers = ((cb) => {
  const initialNumbers = [1, 2, 3, 4, 5];
  const numbers =
    Math.random() < 0.5 ? initialNumbers.map((n) => n * 2) : initialNumbers;

  return cb(numbers);
})((numbers) => {
  return Math.random() < 0.3 ? numbers.filter((n) => n > 3) : numbers;
});

ただ、この方針だと条件が3つ、4つと増えていくと若干テクいコードになっていきます:

const numbers = ((cb) => {
  const initialNumbers = [1, 2, 3, 4, 5];
  const numbers =
    Math.random() < 0.5 ? initialNumbers.map((n) => n * 2) : initialNumbers;

  return cb(numbers);
})((numbers) => {
  return (cb) =>
    cb(Math.random() < 0.3 ? numbers.filter((n) => n > 3) : numbers);
})((numbers) => {
  return Math.random() < 0.9 ? numbers.map((n) => n + 10) : numbers;
});

console.log(numbers);

特に最後の関数だけは最終的な値を返す(コールバック関数を受け取る前提の関数を返すわけではない)ため対称性が悪く、条件を増やす時に修正が必要という面倒な点があります。

reduce

そこで条件(を表現する関数)を可変長引数として受け取って reduce してしまう、という手法が考えられます。つまりこういうことです:

const numbers = ((...converters) => {
  const initialNumbers = [1, 2, 3, 4, 5];
  return converters.reduce((acc, converter) => converter(acc), initialNumbers);
})(
  (numbers) => (Math.random() < 0.5 ? numbers.map((n) => n * 2) : numbers),
  (numbers) => (Math.random() < 0.3 ? numbers.filter((n) => n > 3) : numbers),
  (numbers) => (Math.random() < 0.9 ? numbers.map((n) => n + 10) : numbers)
);

console.log(numbers);

こうすれば条件が増えていったとしても単純に引数として追加していくだけで済みます。

ここまで来ると汎用の関数として独立させても良いかもしれません。TypeScriptで書くとこういう感じです:

type Converter<T> = (input: T) => T;

const multiConvert = <T>(
  initialValue: T,
  ...converters: ReadonlyArray<Converter<T>>
): T => converters.reduce((acc, converter) => converter(acc), initialValue);

const numbers = multiConvert(
  [1, 2, 3, 4, 5],
  (numbers) => (Math.random() < 0.5 ? numbers.map((n) => n * 2) : numbers),
  (numbers) => (Math.random() < 0.3 ? numbers.filter((n) => n > 3) : numbers),
  (numbers) => (Math.random() < 0.9 ? numbers.map((n) => n + 10) : numbers)
);

console.log(numbers);

まとめ

用法・用量を守って正しくお使いください。

GitHubで編集を提案

Discussion

関数に閉じてしまうのなら、関数の呼び出し側から内部を知る必要がないので、内部でlet使っても問題なさそうというか、読みやすい方がいいのではないでしょうか...

多分下記のコードで同じですよね。

const multiConvert = (array, ...fs) => {
  let result = [...array];
  for (const f of fs) {
    result = f(result);
  }
  return result;
}

const numbers = multiConvert(
  [1, 2, 3, 4, 5],
  (numbers) => (Math.random() < 0.5 ? numbers.map((n) => n * 2) : numbers),
  (numbers) => (Math.random() < 0.3 ? numbers.filter((n) => n > 3) : numbers),
  (numbers) => (Math.random() < 0.9 ? numbers.map((n) => n + 10) : numbers)
);

console.log(numbers)

https://jsbin.com/wuhetucame/edit?html,js,console


あと、こういうコードもシンプルですよ。私はよく使ってます。

let _message;
if (Math.random() < 0.5) {
  _message = "low";
} else {
  _message = "high";
}
const message = _message;

console.log(message);

ありがとうございます。
おっしゃる通り、短く限定された範囲内にスコープを切れるのであればletを使っても全く問題ない(読みやすい方が良い)と思います。
どんなコードが「読みやすい」とされるかについてはチーム開発であれば同僚のレビューに通るかどうかも重要な判断基準になると思うので、let使わない縛りをどこまでやるかは現実的にはそのあたりも考慮しつつ、という感じになるだろうと思います。

ログインするとコメントできます