dart-sass icon indicating copy to clipboard operation
dart-sass copied to clipboard

map.deep-merge() function produces unexpected ordering

Open starkis opened this issue 4 years ago • 5 comments

The map.deep-merge($map1, $map2) function returns a map where all entries from $map2 appear first, and any unique entries from $map1 are pushed to the end. This is the opposite behaviour to the map.merge($map1, $map2) function, where the order of $map1 is preserved, and any unique entries in $map2 are added to the end.

An example to illustrate the inconsistency between these functions:

$map1: (
	first: 1,
	second: 2,
	third: (
		first-nested: 1,
		second-nested: 2,
		third-nested: 3,
	),
);
$map2: (
	zeroth: 0,
	first: 1,
	third: (
		first-nested: 1.1,
		third-nested: 3.1,
		fourth-nested: 4.1,
	),
	fourth: 4,
);

@debug map.merge($map1, $map2);
// (first: 1, second: 2, third: (first-nested: 1.1, third-nested: 3.1, fourth-nested: 4.1), zeroth: 0, fourth: 4)

@debug map.deep-merge($map1, $map2);
// (zeroth: 0, first: 1, third: (first-nested: 1.1, third-nested: 3.1, fourth-nested: 4.1, second-nested: 2), fourth: 4, second: 2)

With the deep merge, we see that our second entry from $map1 (and its nested map) has been pushed to the end, even though it never changed.

I would argue that the map.merge() behaviour is the more intuitive. Since $map2 overwrites $map1 when there's a matching key, we can think of $map1 as the initial state and $map2 as containing subsequent updates. In most cases, we expect new data to get added to the end of a record.

Does it even matter? After all, a Sass map looks a lot like a hash table, which is an unordered data structure in other languages. I say, if you can make it more intuitive, why not? I use Sass maps to generate CSS declarations, so it's a little frustrating to see the order getting messed up. Yes, I could run a linter to sort things out later, but I decided to write my own version of the deep-merge() function instead:

@function map-ordered-deep-merge($map1, $map2) {
	$merged-map: $map1;
	@each $key, $map2-value in $map2 {
		$map1-value: map.get($map1, $key);
		@if type-of($map1-value) == map and type-of($map2-value) == map {
			// Merge identically named nested maps.
			$sub-merged-map: map-ordered-deep-merge($map1-value, $map2-value);
			$merged-map: map.set($merged-map, $key, $sub-merged-map);
		} @else {
			$merged-map: map.set($merged-map, $key, $map2-value);
		}
	}
	@return $merged-map;
}

@debug map-ordered-deep-merge($map1, $map2);
// (first: 1, second: 2, third: (first-nested: 1.1, second-nested: 2, third-nested: 3.1, fourth-nested: 4.1), zeroth: 0, fourth: 4)

As you can see, the ordering is much more consistent with the results from map.merge().

What do you think? Is there any reason why the Dart deep-merge() function shouldn't behave the same way?

starkis avatar Jun 30 '21 03:06 starkis

You're right. The deep-merge() was implemented this way for performance reasons. However, this use case is compelling enough to prioritize usability over performance.

As you've pointed out in your example, we should align deep-merge() ordering with merge(). I've opened up an issue in the language repo to track the needed spec updates: https://github.com/sass/sass/issues/3092

Awjin avatar Jun 30 '21 21:06 Awjin

Thanks @Awjin. I had wondered if it was a performance tweak. (I guess you'd need a truly massive codebase to notice it.) Is the backwards ordering of selectors when using @extend for the same reason? This one bothers my slightly obsessive tendencies too, but I seldom use @extend anyway and can live with it. 😊

starkis avatar Jun 30 '21 22:06 starkis

Predictable ordering similar to how merge works would actually be really crucial for a project I'm working on (Uniform CSS).

In this project, I compile an entire set of utility classes as a map in a very specific ordering to avoid conflicting issues, however, whenever one of those child elements change or gets overwritten, then the specific ordering would change and cause conflicts.

jinsupark avatar Dec 16 '21 14:12 jinsupark

Your solution works perfect @starkis!

jinsupark avatar Dec 16 '21 14:12 jinsupark

@jinsupark, you're welcome, and good luck with your framework! I wrote map-ordered-deep-merge() for my own framework. It's part of the typography system, where styles are set out in neatly organised maps that override a set of base styles. In my case, the mucked up ordering still would have got the job done, but it bothered me enough to fix it. :-)

starkis avatar Dec 16 '21 21:12 starkis