JavaScriptのreduceがゆるゆる型であることに助けられた話

某slackにて以下のようなお題が出されました。

お題

["a","b","c","d"] みたいな配列があった時に

{
  "a": {
    "b": {
      "c": "d"
    }
  }
}

みたく変換する良い方法を JavaScript で募集します

わたしの回答

const reducer = (acc, el) => {
  const ret = {};
  ret[el] = acc;
  return ret;
};

["a", "b", "c", "d"].reverse().reduce(reducer);

与太話

普通(普通ってなんだ?)、reduceは配列の要素をひとつひとつ舐めて、変換をしながらaccumulatorに蓄積していくような操作になりますが、静的型付け言語の場合は以下のようなシグネチャになることが多いでしょう。(架空の言語です)

class Array[A] {
  def reduce[B](reducer: (acc: B, element: A) => B, initialAcc: B): B = ???
}

畳み込みはつまり Array of AB に変換する仕組みですから、reducerの型が「(acc: B, element: A)をとり、Bを返す関数」になるのは自然なことです。さらに、accumlatorの初期状態を必要とするので、reducereducerの他に「accumlatorの初期状態」を引数にとります。

しかし、JavaScriptreduceはこの「accumlatorの初期状態」を省略することができます。

[1, 2, 3].reduce((acc, el) => acc + el); => 6

省略した場合、一回目のreducerの引数にはArrayの1要素目と2要素目が渡ってくるという親切(!)な仕様です。

しかし、この挙動は、Arrayの要素の型とaccumlatorの型(=reduceの返り値の型)が同一の場合はうまくいきますが、異なる場合は奇妙な挙動をすることになります。

たとえば、Arrayの要素の型がnumberで、accumlatorの型がobjectの場合を考えてみましょう。

一回目のreducerの引数には、numbernumberが渡ってくることになりますが、二回目以降のreducerには、objectnumberが渡ってくることになります。

これはかなり奇妙な挙動で、普段ならばバグの温床になりそうなものです。

が、今回に限ってはこの挙動に助けられました。

お題で得たいオブジェクト

{
  "a": {
    "b": {
      "c": "d"
    }
  }
}

の形をよくみてください。厄介そうなやつがいますね。そうです、一番深いネストの、"c": "d"の部分です。これが、

{
  "a": {
    "b": {
      "c": {
        "d": {}
      }
    }
  }
}

という形であれば、配列のすべての要素をキーに変換していけばいいだけなので話は簡単なのですが、お題では、ネストの一番深い部分だけが「キーが文字列、値も文字列」で、ほかの部分は「キーは文字列、値はオブジェクト」です。これは困りましたね。普通のreduceでは対応するのはかなり難しそうです…… 。

しかし、JavaScriptreduceは「ちょっと普通じゃない」のです!「accumlatorの初期状態を省略すると最初の一回だけは配列の1番め、2番目の要素を渡してくる」という仕様があるのでした!こいつを悪用してやれば、「一番奥のネストは文字列と文字列のペア」「その他の部分は文字列とオブジェクトのペア」というのをうまく表現できそうです!

という発想で、前掲した解にたどりついたのでした。

JavaScriptの挙動を悪用したハックっぽい感じで、静的型付きの世界からするとかなりお行儀の悪い書き方に感じますが、たまにこういうゆるい型がうまくはまるというか悪用できるケースにあたるとガリガリくんの「当たり」をみつけたような気持ちになりますね。

こちらからは以上です。