3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Javascript:オブジェクトをマージする関数mergeを考えてみた

Last updated at Posted at 2019-11-23

連想配列(辞書オブジェクト)n個の共通配列を上書きなしで結合マージするを読んであれこれやってみたのが勉強になったので、あちらのコメントにも書いたが、あんまり書くと先方にも迷惑なのでこちらに書きます。

こうなったらいいな

//こんなオブジェクトがあったとして:
const hira = {
  'dog': 'いぬ',
  'cat': ['ねこ'],
  'human': 'ひと',
}
const kana = {
  'dog': ['イヌ'],
  'cat': ['ネコ', 'ニャンコ'],
}
const kan = {
  'dog': [],
  'cat': '',
  'human': '',
  'car' : '',
}

//こうなったらいいな...
merge(hira,kana,kan,{})
/*
{
  dog: [ 'いぬ', 'イヌ' ],
  cat: [ 'ねこ', 'ネコ', 'ニャンコ', '猫' ],
  human: [ 'ひと', '人' ],
  car: [ '車' ]
}
*/
  • 同じキーがあったら値を配列にまとめます。
  • 値は配列になってたり、なってなかったりします。

ってことです。(元記事とはちょっと変えてあります。)

こんな感じにしてみました

const merge = ( ...objs ) => 
  objs.reduce( updateMerge, {} )

const updateMerge = (acc, obj) =>  
  reduceObj( updateObj )( acc )( obj )

const reduceObj = updateObj => acc => obj => 
  Object.entries( obj ).reduce( updateObj, acc )

const updateObj = (acc, [k, v]) => 
  acc.hasOwnProperty(k) ? { ...acc, [k]:[ acc[k], v ].flat() }
  : { ...acc, [k]: [v].flat() }

こう考えた

おおざっぱに言って、merge はどんな関数でしょう?

  • オブジェクトを複数、引数にし、オブジェクトを返す
  • 引数のオブジェクトを いい感じで空のオブジェクトに足していけばいいのでは?

reduceのことを知っていれば、まさにreduce案件だと気がつきます。
とりあえず、こんな風に書いてみます。

const merge = ( ...objs ) => 
  objs.reduce( updateMerge, {} )

updateMerge というのが出てきましたが、とりあえず置いただけです。中味はまだ決まってませんが「いい感じで足していく」ものです。
さて、updateMergeってどんな関数でしょう?

  • アキュムレータと配列の要素(ここではオブジェクト)を引数にとって、新しいアキュムレータを返す
  • そのオブジェクトの各プロパティを、いい感じでそのアキュムレータに足していけばいいのでは?

これも reduce 案件のようです。
しかしオブジェクトに直接 reduce は使えないので、とりあえずこうしておきます。

const updateMerge = (acc, obj) =>  
  reduceObj( updateObj )( acc )( obj )

新しい関数がまたふたつ出てきました。これもとりあえず置いただけです。

  • reduceObj はオブジェクトで reduce っぽいことをする関数です。
  • updateObj も「いい感じで足していく」ものです。

まず、reduceObj を考えます。
これは簡単です。Object.entries()を使ってオブジェクトを [(キー), (値)] の配列にして、reduce すればいい。

const reduceObj = updateObj => acc => obj => 
  Object.entries( obj ).reduce( updateObj, acc )

つぎに、updateObj を考えます。

  • アキュムレータと配列の要素(ここでは [(キー), (値)] )を引数にとって、新しいアキュムレータを返す
  • アキュムレータに既にそのプロパティがあれば、値を値の配列の最後部に加える
  • まだそのプロパティがなければ、新しく作って追加する
  • 値は配列にはいってたりはいってなかったりするので、いい感じにする

こうしてみました。

const updateObj = (acc, [k, v]) => 
  acc.hasOwnProperty(k) ? { ...acc, [k]:[ acc[k], v ].flat() }
  : { ...acc, [k]: [v].flat() }

flat()を使うと配列の中のかっこを一個へらしていい感じでフラットにしてくれます。

これで、「とりあえず置いた」ものはなくなりました。
全部並べて、動くかな? -> 動いた! わーい!

まとめ

  • トップダウンで考えると見通しがいい場合がある
  • 関数内から別の関数を呼ぶときは、どっちが上でもよいらしい

追記

最近 flatMap のありがたさがわかってきて、@nagitkk のコメントのが本当にいいので写経。


const merge = ( ...objs ) => 
  objs.flatMap(Object.entries)
  .reduce( updateObj, {} )

const updateObj = (acc, [k, v]) => 
  acc.hasOwnProperty(k) ? { ...acc, [k]: [ acc[k], v ].flat() }
  : { ...acc, [k]: [v].flat() }

自分のは元の構造を保ったまま畳み込みしてたが、どうせ足すんならバラして一気にやっちゃえよ、ってことかな? flatMapならいい感じで空の配列が消えるんでそれも良い。

@standard-softwareマージの仕方を関数に切り出して汎用にする、というアイディアも良い。
自前でやってみる。

const mergeWith = mergingRule => ( ...objs ) => 
  objs.flatMap( Object.entries )
  .reduce( updateObj(mergingRule), {} )

const updateObj = mergingRule => (acc, [k, v]) => ( 
  { ...acc
  , [k]: mergingRule( acc.hasOwnProperty(k) )( acc[k] )( v ) 
  } 
)

// 使用例: 
mergeWith(
  keyExists => existing => coming =>
    keyExists ? [existing, coming].flat()
    : [coming].flat()
)(
  {'dog': 'いぬ', 'cat': ['ねこ'], 'human': 'ひと'}
  , {'dog': ['イヌ'], 'cat': ['ネコ', 'ニャンコ']}
  , {'dog': [], 'cat': '', 'human': '', 'car' : ''}
  , {}
)
/*
{
  dog: [ 'いぬ', 'イヌ' ],
  cat: [ 'ねこ', 'ネコ', 'ニャンコ', '猫' ],
  human: [ 'ひと', '人' ],
  car: [ '車' ]
}
*/

mergeWith(
  keyExists => existing => coming =>
    keyExists ? existing + coming
    : coming
)(
 {key1: 100, key2: 200, key3: 300}
 , {key1: 100, key2: 150, key3: 100}
 , {key1: 100,            key3: 200, key4: 100}
)
// { key1: 300, key2: 350, key3: 600, key4: 100 }

ロジックの一端を使用者に渡すことになるので、このまま使ってもらうのはかなりバギーかも?
ただバリエーションが簡単に作れるので、発想の源としては良い。

さらに追記

こうかな?
より安全にするために、ルールをさらに分割して使用者にロジックを渡さないようにする。
汎用性を考えてここから可変長引数でなく配列を引数に。

const mergeWith = doIfKeyExists => elseDo => objs => 
  objs.flatMap( Object.entries )
  .reduce( updateObj(doIfKeyExists)(elseDo), {} )

const updateObj = doIfKeyExists => elseDo => (acc, [k, v]) =>  
  acc.hasOwnProperty(k) ? { ...acc, [k]: doIfKeyExists( acc[k] )( v ) } 
  : { ...acc, [k]: elseDo( v ) }

// 使用例: 
mergeWith(
 existing => coming =>
  [existing, coming].flat()
)(
  coming =>
    [coming].flat()
)(
  [ {'dog': 'いぬ', 'cat': ['ねこ'], 'human': 'ひと'}
  , {'dog': ['イヌ'], 'cat': ['ネコ', 'ニャンコ']}
  , {'dog': [], 'cat': '', 'human': '', 'car' : ''}
  , {}
  ]
)
/*
{
  dog: [ 'いぬ', 'イヌ' ],
  cat: [ 'ねこ', 'ネコ', 'ニャンコ', '猫' ],
  human: [ 'ひと', '人' ],
  car: [ '車' ]
}
*/

mergeWith(
  existing => coming =>
    existing + coming
)(
  coming =>
    coming
)(
  [ {key1: 100, key2: 200, key3: 300}
  , {key1: 100, key2: 150, key3: 100}
  , {key1: 100,            key3: 200, key4: 100}
  ]
)
// { key1: 300, key2: 350, key3: 600, key4: 100 }
3
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?