Skip to content
131 changes: 130 additions & 1 deletion README.md
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`.

Expand All @@ -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)
Copy link
Member

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 BiSeqDict and MultiBiSeqDict and MultiSeqDict I kept thinking "wait hold on which one is that?", scrolling back to the titles below to see the BiSeqDict (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?

  • ManyToOne
  • OneToMany
  • ManyToMany

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)

import ManyToOne exposing (ManyToOne)

type UserId = UserId Never
type WorkspaceId = WorkspaceId Never

userWorkspaces : ManyToOne (Id UserId) (Id WorkspaceId)
userWorkspaces =
    ManyToOne.empty
        |> ManyToOne.insert aliceId workspace1
        |> ManyToOne.insert bobId workspace1
        |> ManyToOne.insert charlieId workspace2

-- Forward lookup
ManyToOne.get aliceId userWorkspaces
--> Just workspace1

-- Reverse lookup (renamed API)
ManyToOne.getKeys workspace1 userWorkspaces
--> SeqSet.fromList [ aliceId, bobId ]

OneToMany (formerly MultiSeqDict)

import OneToMany exposing (OneToMany)

type PropertyId = PropertyId Never
type UnitId = UnitId Never

propertyUnits : OneToMany (Id PropertyId) (Id UnitId)
propertyUnits =
    OneToMany.empty
        |> OneToMany.insert property1 unit101
        |> OneToMany.insert property1 unit102
        |> OneToMany.insert property2 unit201

-- Forward lookup
OneToMany.getAll property1 propertyUnits
--> SeqSet.fromList [ unit101, unit102 ]

-- Reverse lookup (renamed API)
OneToMany.getKeys unit101 propertyUnits
--> SeqSet.fromList [ property1 ]

-- Remove a specific association
OneToMany.remove property1 unit102 propertyUnits

ManyToMany (formerly MultiBiSeqDict)

import ManyToMany exposing (ManyToMany)

type ChatId = ChatId Never
type DocumentId = DocumentId Never

chatDocuments : ManyToMany (Id ChatId) (Id DocumentId)
chatDocuments =
    ManyToMany.empty
        |> ManyToMany.insert chat1 doc1
        |> ManyToMany.insert chat1 doc2
        |> ManyToMany.insert chat2 doc1

-- Forward lookup
ManyToMany.getAll chat1 chatDocuments
--> SeqSet.fromList [ doc1, doc2 ]

-- Reverse lookup (renamed API)
ManyToMany.getKeys doc1 chatDocuments
--> SeqSet.fromList [ chat1, chat2 ]

-- Transfer
chatDocuments
    |> ManyToMany.remove chat1 doc2
    |> ManyToMany.insert chat3 doc2

Copy link
Member

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

Copy link
Author

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


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.getKeys workspace1 userWorkspaces
--> 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.getAll property1 propertyUnits
--> 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.getAll chat1 chatDocuments
--> SeqSet.fromList [doc1, doc2]

-- Which chats contain doc1?
MultiBiSeqDict.getKeys doc1 chatDocuments
--> 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.
Expand Down
5 changes: 4 additions & 1 deletion elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"version": "1.0.0",
"exposed-modules": [
"SeqDict",
"SeqSet"
"SeqSet",
"BiSeqDict",
"MultiSeqDict",
"MultiBiSeqDict"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
Expand Down
Loading