jjabba

jjabba

Implement custom 'compare' for Ecto models used in MapSet.difference()

I’m working with Ecto modules and want to use MapSet and in particular MapSet.difference(s2, s1) to find which members of prime number collectors cards first edition I’m missing.
Let me exemplify:

my_cards = MapSet.new([{1, 2},{1, 3}, {1, 7}, {1, 11}])
full_collection = MapSet.new([{1, 2},{1, 3},{1, 5},{1, 7}, {1, 11}, {1, 13}])
missing_cards = MapSet.difference(full_collection, my_cards)
# MapSet<[{1, 5}, {1, 13}]>

Now I persist the full collection using an Ecto schema.
When trying to do the same math using instances of this model, it will fail, as the schema will have other members such as primary key {id}, timestamps and other.

How Can I augment either MapSet or the schema so that &MapSet.difference/2 is still useful using %PrimeNumberCollectorsCard{edition: 1, prime: 2} instances???

Most Liked

Qqwy

Qqwy

TypeCheck Core Team

Interesting question!

Only indirectly, by transforming your %PrimeNumberCollectorsCard{}-struct into something that has an identical structure for equivalent datatypes.

Let me first show you a simple ‘solution’ that directly uses MapSet.difference, but still has a problem:

def prime_card_identity(card = %PrimeNumberCollectorsCard{}) do
  {card.edition, card.prime}
end

def missing_cards(your_collection = %MapSet{}, full_collection = %MapSet{}) do
  your_collection_identities = MapSet.new(your_collection, &prime_card_identity/1)
  full_collection_identities = MapSet.new(full_collection, &prime_card_identity/1)
  MapSet.difference(your_collection_identities, full_collection_identities)
end

This works mostly, but the return result of missing_cards/2 will be a MapSet containing identity-tuples like{1, 2}. You could write a function to recover the original %PrimeNumberCollectorsCard{}-struct, by constructing one (but then you do not have the fields that are missing from the identity representation), or by looking it up again in the database (which is slow since we basically discard results from an earlier query, and now do at least one (but possibly N) new queries).

Instead of using MapSet.difference, we might be able to use a Map directly: In a map, the keys have to be unique, but under each key we can track any value we like (whose uniqueness is not enforced by the map). This would make our code look as follows:

def missing_cards(your_collection = %MapSet{}, full_collection = %MapSet{}) do
  your_collection_keys = Enum.map(your_collection, &prime_card_identity)
  
  full_collection
  |> Map.new(&({prime_card_identity(&1), &1}))
  |> Map.drop(your_collection_keys)
  |> Map.values
  |> MapSet.new
end

Interestingly, MapSet.difference/2 is implemented much in the same way under the hood.

Qqwy

Qqwy

TypeCheck Core Team

No worries: This has nothing to do with OOP vs functional programming.
Instead, MapSet was built in the way it is for (a) simplicity and (b) low memory (both RAM and when serialized on disk) usage. The trade-off has been made that elements in a MapSet are always expected to be unique according to their structural equality, see Erlang term comparisons’ “exactly equal to”.

When adding a separate ‘element’ vs ‘element identity’ representation, you’ll have to either:

  • keep track of them separately in the datastructure, thus using more memory.
  • recalculate the identity all the time when manipulating the set, thus being significantly slower (with operations going from amortized constant time or logarithmic time to linear time).

And with the alternate approach of using a ‘comparer’ function and storing the elements internally in a binary tree, we still reduce amortized constant running time to logarithmic running time, as well as needing n * log n amount of space for the tree.

Neither of these approaches are bad, but they make different trade-offs than the ones made for MapSet.
I definitely do think that there is a place for sets that allow either custom identities or a custom comparer function. Hopefully, this will be filled by a library at some point, because re-inventing this wheel for every project definitely is not fun and not productive.

If you are wondering by the way, the difference between these two approaches (a comparer function vs custom identities) is similar to the difference between Enum.sort/2 and Enum.sort_by/2.

Where Next?

Popular in Questions Top

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement