this post was submitted on 14 Jul 2023
9 points (80.0% liked)

Programming

17670 readers
206 users here now

Welcome to the main community in programming.dev! Feel free to post anything relating to programming here!

Cross posting is strongly encouraged in the instance. If you feel your post or another person's post makes sense in another community cross post into it.

Hope you enjoy the instance!

Rules

Rules

  • Follow the programming.dev instance rules
  • Keep content related to programming in some way
  • If you're posting long videos try to add in some form of tldr for those who don't want to watch videos

Wormhole

Follow the wormhole through a path of communities [email protected]



founded 2 years ago
MODERATORS
9
submitted 1 year ago* (last edited 1 year ago) by KaKi87 to c/[email protected]
 

Hi !

Given the following sample items :

| ID | First name | Age | | ---------- | ---------- |


| | xvZwiCpi | Naomi | 42 | | Nzd9UsGT | Naomi | 24 | | QiDXP2wA | Thea | 53 | | JpYeAY7H | Jeremy | 35 |

I can store these in an array :

const data = [
  { id: 'xvZwiCpi', firstName: 'Frederic', age: 42 },
  { id: 'Nzd9UsGT', firstName: 'Naomi', age: 24 },
  { id: 'QiDXP2wA', firstName: 'Thea', age: 53 },
  { id: 'JpYeAY7H', firstName: 'Mathew', age: 35 }
];

Thus access them the same way by ID :

console.log(data.find(item => item.id === 'xvZwiCpi'));

And by properties :

console.log(data.find(item => item.firstName === 'Frederic').id);

Or I can store these in an object :

const data = {
  'xvZwiCpi': { firstName: 'Frederic', age: 42 },
  'Nzd9UsGT': { firstName: 'Naomi', age: 24 },
  'QiDXP2wA': { firstName: 'Thea', age: 53 },
  'JpYeAY7H': { firstName: 'Mathew', age: 35 }
};

Thus more easily access properties by ID :

console.log(data['xvZwiCpi'].firstName);

But more hardly access ID by properties :

console.log(Object.entries(data).find(([id, item]) => item.firstName = 'Frederic')[0]);

I could duplicate IDs :

const data = {
  'xvZwiCpi': { id: 'xvZwiCpi', firstName: 'Frederic', age: 42 },
  'Nzd9UsGT': { id: 'Nzd9UsGT', firstName: 'Naomi', age: 24 },
  'QiDXP2wA': { id: 'QiDXP2wA', firstName: 'Thea', age: 53 },
  'JpYeAY7H': { id: 'JpYeAY7H', firstName: 'Mathew', age: 35 }
};

To slightly simplify that previous line :

console.log(Object.values(data).find(item => item.firstName = 'Frederic').id);

But what if a single variable type could allow doing both operations easily ?

console.log(data['xvZwiCpi'].firstName);
console.log(data.find(item => item.firstName === 'Frederic').id);

Does that exist ?

If not, I'm thinking about implementing it that way :

const data = new Proxy([
  { id: 'xvZwiCpi', firstName: 'Frederic', age: 42 },
  { id: 'Nzd9UsGT', firstName: 'Naomi', age: 24 },
  { id: 'QiDXP2wA', firstName: 'Thea', age: 53 },
  { id: 'JpYeAY7H', firstName: 'Mathew', age: 35 }
], {
    get: (array, property) =>
        array[property]
        ||
        array.find(item => item.id === property)
});

In which case I'd put it in a lib, but how would this be named ?

I'd also make a second implementation that would enforce ID uniqueness and use Map to map IDs with indexes instead of running find : while the first implementation would be fine for static data, the second one would be more suitable for dynamic data.

Would this make sense ?

Thanks

top 18 comments
sorted by: hot top controversial new old
[–] KaKi87 2 points 1 year ago

I just found two packages that do the same thing as I did :

The last one comes with a blog post explaining the same things I did !

[–] [email protected] 2 points 1 year ago (1 children)

Have you considered storing as a Map instead?

MDN Map docs

[–] KaKi87 1 points 1 year ago
[–] [email protected] 2 points 1 year ago (1 children)

Seems like a great idea.
I often find myself using objects when I'm building data (like a horrible CSV processor). It's easy to pull in a row of data, and potentially modify existing data (yeh, really horrible CSV).

But when it gets to processing the data (transformers and database), I prefer arrays.

A lot of it comes back to typescript, where Object.keys().forEach means the key isn't necessarily usable to access the item in the object (the whole, a key becomes a string, but an object can be keyed by string/number/symbol). .
So, whatever you come up with HAS to have first class Typescript support.

[–] KaKi87 2 points 1 year ago (1 children)

I'm not a TypeScript person, sorry. 😅

[–] [email protected] 3 points 1 year ago (1 children)

Fair enough, I can understand that.
I used to enjoy the flexibility that JS provides. And IDEs do a pretty good job of filling the holes!

My last project, I went all in on typescript. And I have caught so many more errors before even compiling.
It's like having tests. It gives a hell of a lot more confidence.

Some days I miss just slinging objects around, and just coercing types tho (without explicitly casting/converting... Object keys being a big one!)

[–] KaKi87 0 points 1 year ago

I used to enjoy the flexibility that JS provides. And IDEs do a pretty good job of filling the holes!

Exactly.

My last project, I went all in on typescript. And I have caught so many more errors before even compiling. It’s like having tests. It gives a hell of a lot more confidence.

I can understand that too. Although, IDEs also catch a lot of type-related errors in vanilla JS.

[–] [email protected] 1 points 1 year ago* (last edited 1 year ago) (1 children)

I'm pretty sure I've been in your situation but haven't created a dictionary/array hybrid.

Without any more details about your use case and situation, I can imagine a few pitfalls with your solution:

  • Serialization might not behave as you would expect (JSON.stringify).
  • 3rd-party functions might not be able to deal with your data structure properly (again, potentially unexpected behavior).
  • You can't easily access array methods (find, filter, map etc).
  • How do you distinguish between ID access and index access? ArrayLike([ {id: "1" }, { id: "2" } ])[1] returns { id: "2" }, how do you access { id: "1" } by ID?
  • It's harder to reason about access time of lookups. However, this might not be a concern of yours.
  • It may cause confusion if you're working with other developers.

That being said, unless you work in an environment where your code should be easily understandable by others, the best way to find out if this is a good idea or not, is to try :)

Me personally, I usually use an associateBy function, when I need a values-by-ID structure. Of course this is not as convenient to use as your approach, but it's good enough for me.

// this WILL drop elements if key is not unique 
function associateBy(array, key) {
  return array.reduce((acc, el) => ({
    ...acc,
    [el[key]]: el
  }), {});
}

associateBy([
  {id: "foo"},
  {id: "bar"}
], "id").foo; // -> {id: "foo"}

Good luck!

[–] KaKi87 1 points 1 year ago (1 children)

dictionary/array hybrid

That's a name, thanks !

Serialization might not behave as you would expect (JSON.stringify)

Actually, my implementation idea relying on Proxy don't have this issue.

As for other implementations, they do have this issue, but as they say, hybrid array use cases don't generally involve the need to stringify, and even when they do, using Object.defineProperty with { enumerable: false } (or replacing the toJSON method) would fix it.

3rd-party functions might not be able to deal with your data structure properly (again, potentially unexpected behavior). You can’t easily access array methods (find, filter, map etc).

Actually, it would still be an Array, so no, there shouldn't be any problems, and yes, those methods definitely work, which is precisely what said I want to achieve.

How do you distinguish between ID access and index access?

If your IDs are integers then there is no need for an hybrid at all, precisely because all you have to do is put each item at the same position as their ID.

It’s harder to reason about access time of lookups. However, this might not be a concern of yours.

I'll definitely run benchmarks so that users would be aware of performance losses, if any. But use cases of hybrid arrays are mostly small datasets so it usually shouldn't be a concern indeed.

It may cause confusion if you’re working with other developers.

If I implement this, it will be as a documented and commented package, so it shouldn't be that much of a problem.

[–] [email protected] 0 points 1 year ago (1 children)

I think I misunderstood your initial post (and definitely didn't read it as carefully as I should have 😅).

Do I understand your correctly that your goal is a companion object for your arrays that simplifies access? Not a new data structure that you'd user instead of arrays? If so, most of my points are moot.

If your IDs are integers then there is no need for an hybrid at all, precisely because all you have to do is put each item at the same position as their ID.

If you don't treat IDs as opaque values, this is true.

I'll definitely run benchmarks so that users would be aware of performance losses, if any. But use cases of hybrid arrays are mostly small datasets so it usually shouldn't be a concern indeed.

I think my point is actually wrong (it was really late when I was writing my initial response). Performance would be O(n), since that's the worst case scenario.

Anyways, I hope you could take something useful from my answer.

Happy hacking :D

[–] KaKi87 2 points 1 year ago

You may have initially misunderstood my idea, but you did help.

And I implemented it in the meantime, as a library named hybrid-array (after your suggestion).

Not all transformative array methods have been checked yet, no unit testing nor comments have been written yet, no benchmarks have been performed yet, but these will happen.

Thanks.

[–] [email protected] 1 points 1 year ago* (last edited 1 year ago) (1 children)

If you're not married to functional code, for searching a keyed object by property for the key, you can always write:

for (const key in data) {
  if (data[key].firstName === 'Frederic') {
    console.log(key);
  }
}

No need to transform the whole object with Object.entries, and you could turn that into a function reasonably easy, something like findKey(object, property, value) or some such.

[–] KaKi87 0 points 1 year ago

True, but less convenient than using an array in the first place.

[–] [email protected] 1 points 1 year ago (1 children)

If you have IDs I’d use a Map.

[–] KaKi87 1 points 1 year ago* (last edited 1 year ago)

My goal is to be able to both easily get an item by ID (using data[id]) and easily get an item by properties (using data.find()).

Meanwhile, just like Object requires Object.values(object) before calling find, Map requires [...map.values()], and it also have the additional inconvenient of requiring a call to map.get to get an item by ID, so no, it's even worse for my goal, but thanks.

[–] [email protected] -2 points 1 year ago* (last edited 1 year ago) (2 children)

Shouldnt you be asking this on StackOverflow? I am here to read programming news..

[–] [email protected] 1 points 1 year ago

Well, I'm here for programming discussions.
I'll concede this might be more suited for a different community, if you have a suggestion

[–] KaKi87 1 points 1 year ago

SO was the first place I thought about asking this, then I realized it wouldn't be accepted, as my question is subjective : I'm not asking for help to implement it, but I'm asking whether it would be good or bad to do implement it.

However, I understand your complaint, so I reviewed the community's sidebar in case I was also wrong to post here : turns out I'm not, although I didn't prepend my post title with [Help] like mentioned, so I just fixed that.

Thanks

load more comments
view more: next ›