某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 A
を B
に変換する仕組みですから、reducer
の型が「(acc: B, element: A)
をとり、B
を返す関数」になるのは自然なことです。さらに、accumlator
の初期状態を必要とするので、reduce
はreducer
の他に「accumlator
の初期状態」を引数にとります。
しかし、JavaScriptのreduce
はこの「accumlator
の初期状態」を省略することができます。
[1, 2, 3].reduce((acc, el) => acc + el); => 6
省略した場合、一回目のreducer
の引数にはArray
の1要素目と2要素目が渡ってくるという親切(!)な仕様です。
しかし、この挙動は、Array
の要素の型とaccumlator
の型(=reduce
の返り値の型)が同一の場合はうまくいきますが、異なる場合は奇妙な挙動をすることになります。
たとえば、Array
の要素の型がnumber
で、accumlator
の型がobject
の場合を考えてみましょう。
一回目のreducer
の引数には、number
とnumber
が渡ってくることになりますが、二回目以降のreducer
には、object
とnumber
が渡ってくることになります。
これはかなり奇妙な挙動で、普段ならばバグの温床になりそうなものです。
が、今回に限ってはこの挙動に助けられました。
お題で得たいオブジェクト
{ "a": { "b": { "c": "d" } } }
の形をよくみてください。厄介そうなやつがいますね。そうです、一番深いネストの、"c": "d"
の部分です。これが、
{ "a": { "b": { "c": { "d": {} } } } }
という形であれば、配列のすべての要素をキーに変換していけばいいだけなので話は簡単なのですが、お題では、ネストの一番深い部分だけが「キーが文字列、値も文字列」で、ほかの部分は「キーは文字列、値はオブジェクト」です。これは困りましたね。普通のreduce
では対応するのはかなり難しそうです…… 。
しかし、JavaScriptのreduce
は「ちょっと普通じゃない」のです!「accumlatorの初期状態を省略すると最初の一回だけは配列の1番め、2番目の要素を渡してくる」という仕様があるのでした!こいつを悪用してやれば、「一番奥のネストは文字列と文字列のペア」「その他の部分は文字列とオブジェクトのペア」というのをうまく表現できそうです!
という発想で、前掲した解にたどりついたのでした。
JavaScriptの挙動を悪用したハックっぽい感じで、静的型付きの世界からするとかなりお行儀の悪い書き方に感じますが、たまにこういうゆるい型がうまくはまるというか悪用できるケースにあたるとガリガリくんの「当たり」をみつけたような気持ちになりますね。
こちらからは以上です。