switch文が実はもう少し使える子だった？

## はじめに

JSに関する小ネタです。
以下のように感じる人はもしかしたらこの記事を読む価値があるかもしれません。

- `if-else`の羅列はやっぱ読みづらいし面倒だなー
- `switch`文でパターンマッチみたいなの使いたいなー

ただしパターンマッチを実現するという内容の記事ではないので期待はしすぎないでください。

## 注意

- サンプルコードは[Node.js](https://nodejs.org/en/)の10.13.0で試してます
- ほとんどのブラウザで使える内容だとは思いますが、検証しきれていません
- 間違ってるとこがありましたら指摘お願いします

## `switch`文が使いにくいと感じる場面

`"/hoge/fuga"`のような URL パスっぽいものがあったときに、その値に応じて処理や返り値を変えたいとする。
単純に完全一致だけで扱えるケースであれば`switch`文を使える。

```js
function returnSomething(path) {
  switch (path) {
    case '/': {
      return 'top';
    }
    case '/hoge': {
      return 'hoge';
    }
    case '/fuga': {
      return 'fuga';
    }
    case '/hoge/fuga': {
      return 'hoga';
    }
    case '/hoge/piyo': {
      return 'hoyo';
    }
    default: {
      return 'anything else';
    }
  }
}

// 読み飛ばしてもよいコメント
// case にわざわざ "{}" をつけていますが、この例では必要ありません。
// ただし変数の名前空間を分けることができるので、つけとくと便利な場面もあります。
```

だけど完全一致だけじゃない場合はよくあると思う。(前方一致とかワイルドカードみたいなことをしたい)
そんなとき今までなら諦めて`if-else`を使ってました。

```js
function returnSomething(path) {
  if (path === '/') {
    return 'top';
  } else if (path === '/hoge') {
    return 'hoge';
  } else if (path === '/fuga') {
    return 'fuga';
  } else if (path.startsWith('/hoge/')) {
    return 'hogex';
  } else {
    return 'anything else';
  }
}
```

([`String.prototype.startsWith()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith)は IE11 では使えません。polyfill とかでなんとかすると良いと思います。)

`if`と`else if`でalignmentがずれたり、`path ===`と何回も書いたり。
個人的にはこういう読みづらくミスを誘発しやすいものを避けたい。

もちろん`case`を連続で並べる方法も数が少ない場合は良いですが、
数が多くなったりワイルドカードみたいなものを実現したいときには現実的な対応策になりません。

```js
// (省略)
    case '/hoge/fuga':
    case '/hoge/piyo': {
      return 'hogex';
    }
// (省略)
```

そんなときに同僚のコードレビューしていたら、とても面白いコードができあがったのでそれを紹介します。

## `switch`文をちょっと工夫して使う

上記の`if-else`の例を`switch`で実現したものを先に示しときます。

```js
function returnSomething(path) {
  switch (path) {
    case '/': {
      return 'top';
    }
    case '/hoge': {
      return 'hoge';
    }
    case '/fuga': {
      return 'fuga';
    }
    case path.startsWith('/hoge/') && path: {
      return 'hogex';
    }
    default: {
      return 'anything else';
    }
  }
}
```

正直`if-else`と大差ないとか思うかもしれないですが、まあマシになったということにしときます。
あとは`case path.startsWith('/hoge/') && path:`の動きを理解するだけです。

## ちょっとした解説

1. `path.startsWith('/hoge/')`は`Boolean`を返すので、`true`と`false`という具体的な値で考えてみる
2. `path.startsWith('/hoge/') && path`という式は以下のようになる
  - `true && path`: `path`を返す
  - `false && path` `false`を返す
4. `switch (path)`としているので当然比較は以下のようになる
  - `path === path`: これは`NaN`以外なら必ず成り立つ
  - `path === false`: これは`path`が`false`でない限りは成り立たない

つまり`真偽値 && path`とすることで"自分自身"と、もしくは"真偽値の偽"との比較が最終的に行われるということでした。
なので`case path.match(/^\/hoge\/.*/) && path:`みたいなこともできます。
ただし`switch`に渡す変数次第で、"真偽値の偽"との比較が真になってしまうこともあるため、
注意して使用したほうが良さそうです。

## 蛇足

`let`で変数をあらかじめ宣言しておいて、`case`で代入も行うという荒業を使えば、
正規表現の結果を利用するといったことも可能です。
(が、個人的にはあんまり多用したくない印象)

```js
function returnSomething(path) {
  let matched;
  switch (path) {
    case '/': {
      return 'top';
    }
    case '/hoge': {
      return 'hoge';
    }
    case (matched = path.match(/\/hoge\/(.*)/)) && path: {
      return `hoge + ${matched[1]}`;
    }
    default: {
      return 'anything else';
    }
  }
}
```

※ IE では `let` を使えないのでご注意ください。

## さいごに

今回は`switch`文の少し変わった使い方を紹介しました。
はやく[パターンマッチ](https://github.com/tc39/proposal-pattern-matching)が正式導入されるといいですね。

明日は @forl_head_officer さんの記事です！