-
Notifications
You must be signed in to change notification settings - Fork 1
[WIP] Add BiSeqDict, MultiSeqDict, and MultiBiSeqDict modules #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
1498739
e94d10b
7af91f8
3e65790
3197251
859635a
449d686
eb5ad25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||
| ## Containers | ||||||
|
|
||||||
| This package is a collection of various container-like data structures. Currently only `SeqDict` and `SeqSet` are included. | ||||||
| This package is a collection of various container-like data structures including `SeqDict`, `SeqSet`, and bidirectional/multi-value dictionaries. | ||||||
|
|
||||||
| Install using `lamdera install lamdera/containers`. | ||||||
|
|
||||||
|
|
@@ -19,6 +19,135 @@ For example insertions are `O(log(n))` rather than `O(n)` and fromList is `O(n * | |||||
| <sup>*Non-equatable Elm values are currently: functions, `Bytes`, `Html`, `Json.Value`, `Task`, `Cmd`, `Sub`, `Never`, `Texture`, `Shader`, and any datastructures containing these types.</sup> | ||||||
|
|
||||||
|
|
||||||
| ## BiSeqDict, MultiSeqDict, and MultiBiSeqDict (bidirectional and multi-value dictionaries) | ||||||
|
|
||||||
| These data structures extend the capabilities of `SeqDict` to handle more complex relationships: | ||||||
|
|
||||||
| ### BiSeqDict (Many-to-One) | ||||||
|
|
||||||
| `BiSeqDict` is a bidirectional dictionary that maintains a reverse mapping from values back to keys. This is useful when: | ||||||
| - Multiple keys can map to the same value | ||||||
| - You need efficient lookups in both directions | ||||||
| - You want to find all keys associated with a particular value | ||||||
|
|
||||||
| **Example with opaque types:** | ||||||
| ```elm | ||||||
| import BiSeqDict exposing (BiSeqDict) | ||||||
|
|
||||||
| -- Opaque ID types (not comparable!) | ||||||
| type UserId = UserId Never | ||||||
| type WorkspaceId = WorkspaceId Never | ||||||
|
|
||||||
| -- Multiple users can belong to the same workspace | ||||||
| userWorkspaces : BiSeqDict (Id UserId) (Id WorkspaceId) | ||||||
| userWorkspaces = | ||||||
| BiSeqDict.empty | ||||||
| |> BiSeqDict.insert aliceId workspace1 | ||||||
| |> BiSeqDict.insert bobId workspace1 | ||||||
| |> BiSeqDict.insert charlieId workspace2 | ||||||
|
|
||||||
| -- Forward lookup: What workspace does alice belong to? | ||||||
| BiSeqDict.get aliceId userWorkspaces | ||||||
| --> Just workspace1 | ||||||
|
|
||||||
| -- Reverse lookup: Who are all members of workspace1? | ||||||
| BiSeqDict.getReverse workspace1 userWorkspaces | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. Though still reads a bit awkward with the collection name - "get the keys for doc1 chat documents"... open to other ideas |
||||||
| --> SeqSet.fromList [aliceId, bobId] | ||||||
| ``` | ||||||
|
|
||||||
| **Note:** This works with opaque ID types that aren't `comparable` - you couldn't do this with regular `Dict`! | ||||||
|
|
||||||
| **Performance:** O(log n) for both forward and reverse lookups. | ||||||
|
|
||||||
| ### MultiSeqDict (One-to-Many) | ||||||
|
|
||||||
| `MultiSeqDict` allows one key to map to multiple values. This is useful when: | ||||||
| - A single key naturally has multiple associated values | ||||||
| - You want to maintain a collection of values per key | ||||||
| - You need set semantics (no duplicate values per key) | ||||||
|
|
||||||
| **Example with opaque types:** | ||||||
| ```elm | ||||||
| import MultiSeqDict exposing (MultiSeqDict) | ||||||
|
|
||||||
| type PropertyId = PropertyId Never | ||||||
| type UnitId = UnitId Never | ||||||
|
|
||||||
| -- A property can have multiple units | ||||||
| propertyUnits : MultiSeqDict (Id PropertyId) (Id UnitId) | ||||||
| propertyUnits = | ||||||
| MultiSeqDict.empty | ||||||
| |> MultiSeqDict.insert property1 unit101 | ||||||
| |> MultiSeqDict.insert property1 unit102 | ||||||
| |> MultiSeqDict.insert property2 unit201 | ||||||
|
|
||||||
| -- Get all units for property1 | ||||||
| MultiSeqDict.get property1 propertyUnits | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
A little torn on this one. But
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed 👍 |
||||||
| --> SeqSet.fromList [unit101, unit102] | ||||||
|
|
||||||
| -- Remove a specific unit | ||||||
| MultiSeqDict.remove property1 unit102 propertyUnits | ||||||
| ``` | ||||||
|
|
||||||
| **Performance:** O(log n) for lookups and insertions. | ||||||
|
|
||||||
| ### MultiBiSeqDict (Many-to-Many) | ||||||
|
|
||||||
| `MultiBiSeqDict` combines both features: multiple values per key AND efficient reverse lookups. This is useful when: | ||||||
| - You have a many-to-many relationship | ||||||
| - You need lookups in both directions | ||||||
| - Each key can have multiple values and each value can be associated with multiple keys | ||||||
|
|
||||||
| **Real-world example: Documents can belong to multiple chats** | ||||||
| ```elm | ||||||
| import MultiBiSeqDict exposing (MultiBiSeqDict) | ||||||
|
|
||||||
| type ChatId = ChatId Never | ||||||
| type DocumentId = DocumentId Never | ||||||
|
|
||||||
| -- Documents can be shared across multiple chats | ||||||
| -- Chats can have multiple documents | ||||||
| -- Documents can be transferred between chats | ||||||
| chatDocuments : MultiBiSeqDict (Id ChatId) (Id DocumentId) | ||||||
| chatDocuments = | ||||||
| MultiBiSeqDict.empty | ||||||
| |> MultiBiSeqDict.insert chat1 doc1 | ||||||
| |> MultiBiSeqDict.insert chat1 doc2 | ||||||
| |> MultiBiSeqDict.insert chat2 doc1 -- doc1 is shared! | ||||||
|
|
||||||
| -- What documents are in chat1? | ||||||
| MultiBiSeqDict.get chat1 chatDocuments | ||||||
| --> SeqSet.fromList [doc1, doc2] | ||||||
|
|
||||||
| -- Which chats contain doc1? | ||||||
| MultiBiSeqDict.getReverse doc1 chatDocuments | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, consideration for change to both But when reading through the example code, the insertion/removal reads beautifully, but the retrieval reads clunkily, my brain is juggling which direction things are going. Reads to me as "get all chat1 documents" -> not bad. Reads to me as "get the reverse document chat documents?" Feel like there's an opportunity for something much clearer here. Will think a bit more but ideas welcome.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same - open to ideas if something reads better |
||||||
| --> SeqSet.fromList [chat1, chat2] | ||||||
|
|
||||||
| -- Transfer doc2 from chat1 to chat3 | ||||||
| chatDocuments | ||||||
| |> MultiBiSeqDict.remove chat1 doc2 | ||||||
| |> MultiBiSeqDict.insert chat3 doc2 | ||||||
| ``` | ||||||
|
|
||||||
| **Why this is better than regular Dict:** | ||||||
| - ✅ Works with opaque ID types (not `comparable`) | ||||||
| - ✅ O(log n) queries in both directions | ||||||
| - ✅ Automatic consistency when transferring documents | ||||||
| - ❌ Regular `Dict` would require manual index maintenance and comparable keys | ||||||
|
|
||||||
| **Performance:** O(log n) for lookups in both directions. | ||||||
|
|
||||||
| ### Key Features | ||||||
|
|
||||||
| All three types: | ||||||
| - ✅ Work with any equatable types (no `comparable` constraint) | ||||||
| - ✅ Preserve insertion order | ||||||
| - ✅ Provide O(log n) performance for core operations | ||||||
| - ✅ Automatically maintain consistency (removing a key updates all related mappings) | ||||||
|
|
||||||
| **Wire3 Support:** Full Lamdera Wire3 codec support is included for all three types (`encodeBiSeqDict`, `encodeMultiSeqDict`, `encodeMultiBiSeqDict`), allowing them to be used directly in your Lamdera `BackendModel`, `FrontendModel`, and messages. | ||||||
|
|
||||||
|
|
||||||
| ## Comparison to other Elm packages | ||||||
|
|
||||||
| See miniBill's [comparison of Elm Dict implementations](https://docs.google.com/spreadsheets/d/1j2rHUx5Nf5auvg5ikzYxbW4e1M9g0-hgU8nMogLD4EY) for a meta-analysis of implementation and performance characteristics. | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One general thing I felt reading through as a whole was even after fully understanding what's going on, reading
BiSeqDictandMultiBiSeqDictandMultiSeqDictI kept thinking "wait hold on which one is that?", scrolling back to the titles below to see theBiSeqDict (Many-to-One)and going "Ah right yes, this is the many-to-one one.That just kept happening over and over... 😅
So what if instead, we names them the thing they are?
I'm not 100% a fan of this naming in general, but after redoing the examples with this naming at least to me reads a lot nicer and helps keep clarity in my head of what's going on, so I think it is a step better naming than the SeqDict versions:
ManyToOne (formerly
BiSeqDict)OneToMany (formerly
MultiSeqDict)ManyToMany (formerly
MultiBiSeqDict)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this naming because it naturally fits in with OneToOne.elm module I'd like to add
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this. Will wait for team consensus on exact names