bimap

A bidirectional map (bijection) with reverse lookup, insertion order, and index access - MoonBit port of Rust's bimap

bimap
bidirectional
bijection
data-structure
ordered
moon add aurasuisui/bimap@0.1.1
Download zip
Version
0.1.1
License
Apache-2.0
Last updated
3 hours ago
Downloads
6
README

#moonbit-bimap

License CI

A bidirectional map (bijection) for MoonBit — a port of Rust's bimap crate / Guava BiMap, extended with insertion-order preservation and index-based access (which neither Rust nor Guava provides).

A BiMap[L, R] keeps keys and values in one-to-one correspondence: you can look up left→right and right→left, and every insertion maintains the bijection invariant.

let m = @aurasuisui/bimap.new()
m.insert("alice", "admin") |> ignore
m.insert("bob", "user") |> ignore

// Forward and reverse lookup:
println(m.get_by_left("alice")) // Some("admin")
println(m.get_by_right("user")) // Some("bob")

// Index access (insertion order preserved):
println(m.get_index(0)) // Some(("alice", "admin"))

#Why a BiMap? (vs the built-in Map and vs indexmap)

Featurebuilt-in MapBiMapindexmap
key → value
value → key (reverse)
index access get_index(i)
keys unique
values also unique (bijection)
preserves insertion orderimpl-defined
Eq/Hash semanticsorder-independentorder-independentorder-sensitive

BiMap and indexmap solve orthogonal problems — Bi = bidirectional (one-to-one, reverse lookup); Index = positional access. They share only the underlying hash table (as any two maps share arrays). This package is a fresh, dependency-free library, not a fork or rename of indexmap.

#Features

  • Bidirectional lookupget_by_left / get_by_right, contains_left / contains_right
  • Bijection-enforcing insertioninsert returns an Overwritten enum describing what was displaced (including the classic C4 collapse, see below)
  • Non-overwriting insertioninsert_no_overwrite returns Result[Unit, (L, R)]
  • Insertion-order iterationiter() yields pairs in the order left keys were inserted
  • Index-based accessget_index(i), get_index_of_left, get_index_of_right, first(), last()
  • Inverse copyto_inverse() -> BiMap[R, L] (a copy, not a live view)
  • Standard traitsDebug, Default, Show, Eq/Hash (order-independent), ToJson, plus QuickCheck Arbitrary

#Installation

Add the dependency to your project's moon.mod:

import { "aurasuisui/bimap@0.1.1", }

Then import it in the relevant moon.pkg:

import { "aurasuisui/bimap", }

#The five insertion cases (C0–C4)

Inserting (l, r) into a bijection has five sub-cases — the crux of a correct BiMap:

CaseConditioninsert returnslen change
C0neither l nor r presentNeither+1
C1the exact pair (l, r) already presentPair(l, r)0
C2l was bound to r'≠r; r freeLeft(l, r')0
C3r was bound to l'≠l; l freeRight(l', r)0
C4l→r' and l'→r both existBoth((l,r'), (l',r))−1

C4 collapses two pairs into oneinsert can reduce the map's size! This mirrors Rust bimap's Overwritten::Both exactly.

let m = @aurasuisui/bimap.new()
m.insert("a", 1) |> ignore // Neither {a↔1}
m.insert("b", 2) |> ignore // Neither {a↔1, b↔2}
m.insert("a", 4) |> ignore // Left(a, 1) {a↔4, b↔2}
m.insert("c", 2) |> ignore // Right(b, 2) {a↔4, c↔2}
let r = m.insert("a", 2) // Both((a,4),(c,2)) {a↔2} — len 2→1!

#Gotchas

  1. insert can shrink the map (C4 collapse). Check the returned Overwritten if you need to know what was displaced.
  2. Eq and Hash are order-independent. A BiMap is a set of pairs; two maps with the same pairs in different insertion order are equal and hash the same. This is the opposite of the author's indexmap, whose Eq/Hash are order-sensitive. Because Hash combines pair hashes commutatively, it is weaker against collision attacks — fine for a collection, but be mindful if using a BiMap as a key in another hash container.
  3. to_inverse() returns a copy, not a live view. Mutating the inverse does not affect the original (MoonBit's ownership model favors copies over shared live views; this matches Rust bimap's method-based access rather than Guava's live inverse()).
  4. ToJson keys use l.to_string() (L : Show), so String keys serialize verbatim.
  5. Don't mutate the map while an iterator is active — iterators are fail-fast (they snapshot a mutation counter and abort if the map changes mid-iteration).
  6. from_array resolves duplicate pairs by "last wins" (via insert), matching Rust's FromIterator.
  7. A rebind (C2) keeps the left key's insertion position — rebinding l to a new right value does not move l to the end of the order. This is an intentional, order-preserving extension over Rust's remove-then-reinsert behavior (see CHANGELOG).
  8. BiMap is not thread-safe. It is mutable and its iterators are fail-fast; concurrent reads/writes from multiple threads are undefined behavior. Use one BiMap per thread, or guard shared access with external synchronization.

#API Overview

CategoryMethods
Constructnew(), with_capacity(n), from_array(pairs), default(), copy()
Querylen(), is_empty(), capacity()
Insertinsert(l, r) -> Overwritten, insert_no_overwrite(l, r) -> Result[Unit,(L,R)]
Forwardget_by_left(l), contains_left(l), remove_by_left(l) -> R?
Reverseget_by_right(r), contains_right(r), remove_by_right(r) -> L?
Indexget_index(i), get_index_of_left(l), get_index_of_right(r), first(), last()
Iterateiter(), lefts(), rights(), into_array()
Convertto_inverse() -> BiMap[R, L]
TraitsDebug, Default, Show, Hash, Eq, ToJson, Arbitrary

#Design

  • Two inverse Robin Hood hash tables (forward: L→R, backward: R→L) keep the bijection.
  • One shared order array + positions map tracks left-key insertion order, enabling index access without a second order structure on the backward table.
  • All mutations funnel through private put_pair / remove_by_left / remove_by_right helpers that maintain the invariants: ∀(l,r)∈forward ⟺ backward[r]==l, and five consistent counters.
  • The Robin Hood engine is adapted from the author's aurasuisui/indexmap (see below).

#Examples

Runnable example packages live in cmd/:

  • cmd/username_email — username ↔ email bidirectional lookup, iteration, and a rebind
  • cmd/country_code — country name ↔ ISO code ("China" ↔ "CN"), reverse lookup, index access, and non-overwriting insert

Note: the cmd/* example packages are standalone modules excluded from the root workspace (they import the published aurasuisui/bimap). To run one, make the package resolvable (e.g. after moon publish) and run moon run cmd/<name>.

#Development

moon check # type check moon test # run all 229 tests moon fmt # format moon build # build

The five-step CI pipeline runs: moon fmt --checkmoon check moon info && git diff --exit-codemoon testmoon build.

See CONTRIBUTING.md for the architecture deep-dive and test conventions.

#Known Issues

  • Fail-fast abort is not in-process testable. Mutating a map mid-iteration triggers abort, which the MoonBit test framework cannot catch as a passing assertion (a panicking test is reported as failed, not as "expected panic"). The version-snapshot + abort logic in src/bimap_iter.mbt is verified by inspection and by a manual reproduction (documented there); all other iterator behavior is fully tested.

#Acknowledgements & Licensing

  • The Robin Hood hash-table engine is adapted from the author's aurasuisui/indexmap (Apache-2.0).
  • The BiMap semantics (insert/insert_no_overwrite, Overwritten, bidirectional lookup) are ported from the Rust bimap crate (MIT / Apache-2.0), with conceptual reference to Guava BiMap (Apache-2.0). Order preservation and index access are original additions.

#License

Apache 2.0 — see LICENSE.

Built for the 2026 MoonBit Open Source Ecosystem Hackathon (August).

#
BiMap

type BiMap[L, R]

A bidirectional map maintaining a one-to-one correspondence (a bijection) between left keys of type L and right values of type R.

Both sides are unique: inserting a pair whose left OR right already exists displaces the conflicting pair(s). Lookups work in both directions (get_by_left / get_by_right), and the insertion order of left keys is preserved and indexable (get_index, get_index_of_left, first, last).
impl Default for BiMap[L, R]
impl Eq for BiMap[L, R]
impl Hash for BiMap[L, R]
impl Show for BiMap[L, R]
impl ToJson for BiMap[L, R]
impl Debug for BiMap[L, R]
impl Arbitrary for BiMap[L, R]

#
BiMap::capacity

fn[L, R] BiMap::capacity(self : BiMap[L, R]) -> Int

Return the current bucket capacity of the underlying forward table.

#
BiMap::contains_left

fn[L : Hash + Eq, R] BiMap::contains_left(self : BiMap[L, R], l : L) -> Bool

Return true if left key l is present.

#
BiMap::contains_right

fn[L, R : Hash + Eq] BiMap::contains_right(self : BiMap[L, R], r : R) -> Bool

Return true if right value r is present.

#
BiMap::copy

fn[L : Hash + Eq, R : Hash + Eq] BiMap::copy(self : BiMap[L, R]) -> BiMap[L, R]

Return an independent deep copy. Mutating the copy does not affect the original (and vice versa). Insertion order is preserved.

#
BiMap::first

fn[L : Hash + Eq, R] BiMap::first(self : BiMap[L, R]) -> (L, R)?

Return the earliest-inserted pair, or None if the map is empty.

#
BiMap::from_array

fn[L : Hash + Eq, R : Hash + Eq] BiMap::from_array(pairs : Array[(L, R)]) -> BiMap[L, R]

Build a BiMap from an array of pairs. Pairs are inserted in order through insert, so on a conflict the LATER pair wins (aligning with Rust FromIterator). For example from_array([("a",1), ("a",2)]) yields { "a" <-> 2 }.

#
BiMap::get_by_left

fn[L : Hash + Eq, R] BiMap::get_by_left(self : BiMap[L, R], l : L) -> R?

Return the right value bound to left key l (forward lookup).

#
BiMap::get_by_right

fn[L, R : Hash + Eq] BiMap::get_by_right(self : BiMap[L, R], r : R) -> L?

Return the left key bound to right value r (reverse lookup).

#
BiMap::get_index

fn[L : Hash + Eq, R] BiMap::get_index(self : BiMap[L, R], i : Int) -> (L, R)?

Return the pair at insertion-order index i, or None if i is out of bounds.

#
BiMap::get_index_of_left

fn[L : Hash + Eq, R] BiMap::get_index_of_left(self : BiMap[L, R], l : L) -> Int?

Return the insertion-order index of left key l, or None if absent.

#
BiMap::get_index_of_right

fn[L : Hash + Eq, R : Hash + Eq] BiMap::get_index_of_right(self : BiMap[L, R], r : R) -> Int?

Return the insertion-order index of the pair whose right value is r, or None if absent. This is a convenience for get_index_of_left(get_by_right(r)): the order array only stores left keys, so a right value's index is the index of the left key it is paired with.

#
BiMap::insert

fn[L : Hash + Eq, R : Hash + Eq] BiMap::insert(self : BiMap[L, R], l : L, r : R) -> Overwritten[L, R]

Insert the pair (l, r), displacing any conflicting pair(s), and report what was overwritten via the returned Overwritten value.

Cases:
  • C0 neither side present → Neither, length +1.
  • C1 the exact pair already present → Pair(l, r), no change (idempotent).
  • C2 l already bound to another right → Left(l, old_r), length unchanged.
  • C3 r already bound to another left → Right(old_l, r), length unchanged.
  • C4 both sides conflict → Both((l, old_r), (old_l, r)), length −1 (the two old pairs collapse into one).

#
BiMap::insert_no_overwrite

fn[L : Hash + Eq, R : Hash + Eq] BiMap::insert_no_overwrite(self : BiMap[L, R], l : L, r : R) -> Result[Unit, (L, R)]

Insert (l, r) only if NEITHER side is already present. If the left key or the right value already exists, the map is left unchanged and the attempted pair is returned in Err((l, r)); otherwise the pair is inserted and Ok(()) is returned. Aligns with Rust bimap::insert_no_overwrite.

#
BiMap::into_array

fn[L : Hash + Eq, R] BiMap::into_array(self : BiMap[L, R]) -> Array[(L, R)]

Return all pairs as an array in left-key insertion order.

#
BiMap::is_empty

fn[L, R] BiMap::is_empty(self : BiMap[L, R]) -> Bool

Return true if the map contains no pairs.

#
BiMap::iter

fn[L : Hash + Eq, R] BiMap::iter(self : BiMap[L, R]) -> Iter[(L, R)]

Return a lazy iterator over (left, right) pairs in left-key insertion order. Supports for (l, r) in m { ... }. Aborts if the map is mutated during iteration.

#
BiMap::last

fn[L : Hash + Eq, R] BiMap::last(self : BiMap[L, R]) -> (L, R)?

Return the most-recently-inserted pair, or None if the map is empty.

#
BiMap::lefts

fn[L : Hash + Eq, R] BiMap::lefts(self : BiMap[L, R]) -> Iter[L]

Return a lazy iterator over the left keys in insertion order. Aborts if the map is mutated during iteration.

#
BiMap::len

fn[L, R] BiMap::len(self : BiMap[L, R]) -> Int

Return the number of pairs in the map.

#
BiMap::new

fn[L : Hash + Eq, R] BiMap::new() -> BiMap[L, R]

Create a new, empty BiMap with default capacity.

#
BiMap::remove_by_left

fn[L : Hash + Eq, R : Hash + Eq] BiMap::remove_by_left(self : BiMap[L, R], l : L) -> R?

Remove the pair keyed by left key l, returning its right value if present. Cleans up both tables and the order/position bookkeeping.

#
BiMap::remove_by_right

fn[L : Hash + Eq, R : Hash + Eq] BiMap::remove_by_right(self : BiMap[L, R], r : R) -> L?

Remove the pair keyed by right value r, returning its left key if present. Cleans up both tables and the order/position bookkeeping.

#
BiMap::rights

fn[L : Hash + Eq, R] BiMap::rights(self : BiMap[L, R]) -> Iter[R]

Return a lazy iterator over the right values in insertion order. Aborts if the map is mutated during iteration.

#
BiMap::to_inverse

fn[L : Hash + Eq, R : Hash + Eq] BiMap::to_inverse(self : BiMap[L, R]) -> BiMap[R, L]

Return an independent copy with the two sides swapped: a BiMap[R, L] whose left keys are this map's right values (in the same pair order). This is a COPY, not a live view — mutating it does not affect the original. (This differs from Guava's live inverse() and matches Rust bimap's method-based reverse access.)

#
BiMap::with_capacity

fn[L : Hash + Eq, R] BiMap::with_capacity(cap : Int) -> BiMap[L, R]

Create a new, empty BiMap with capacity for at least cap pairs.

#
Overwritten

pub enum Overwritten[L, R] {
Neither
Left(L, R)
Right(L, R)
Both((L, R), (L, R))
Pair(L, R)
} derive(Eq,
Debug
)

The result of insert, describing which existing pair(s) were displaced. Mirrors Rust bimap::Overwritten.

  • Neither — a brand-new pair; nothing was displaced (case C0).
  • Left(l, old_r) — the left key l was already bound to old_r, which is displaced (case C2).
  • Right(old_l, r) — the right value r was already bound to old_l, which is displaced (case C3).
  • Both((l, old_r), (old_l, r)) — both sides conflicted; the two old pairs collapse into the single new pair and the length decreases by one (case C4).
  • Pair(l, r) — the exact pair already existed; the insert is idempotent (case C1).

#
VERSION

let VERSION : String

Library version string.

#
new

fn[L : Hash + Eq, R] new() -> BiMap[L, R]

Create a new, empty BiMap.

#
with_capacity

fn[L : Hash + Eq, R] with_capacity(cap : Int) -> BiMap[L, R]

Create a new, empty BiMap with capacity for at least cap pairs.