Skip to content

Maps

Maps are powerful collections that store key-value pairs, like a real-world dictionary where you look up definitions (values) using words (keys). They’re perfect when you need to associate related data together!

A map is a collection where each key maps to exactly one value. Think of it like:

  • A phone book: names (keys) → phone numbers (values)
  • A scoreboard: player names (keys) → scores (values)
  • A product catalog: product IDs (keys) → prices (values)

Maps use the syntax map[KeyType]ValueType:

// Map from strings to integers
let ages: map[str]i32;
// Map from integers to strings
let id_to_name: map[i32]str;
// Map from strings to floats
let prices: map[str]f64;

The key type comes first in brackets [KeyType], followed by the value type.

Create maps using curly braces with the => arrow operator:

let scores := {
"alice" => 95,
"bob" => 87,
"charlie" => 92
}; // types are automatically inferred as map[str]i32
let prices := {
"apple" => 1.99,
"banana" => 0.99,
"orange" => 1.49
} as map[str]f64; // explicit type annotation

The => operator clearly shows “this key maps to this value.”

Create an empty map by casting an empty literal:

let empty_scores := {} as map[str]i32;
let user_data := {} as map[i32]str;

Ferret provides two ways to access map values, each with different safety guarantees:

Direct Indexing (Returns T, Panics if Missing)

Section titled “Direct Indexing (Returns T, Panics if Missing)”

Direct map indexing map[key] returns the value type T directly. If the key doesn’t exist, the program will panic:

let scores := {"alice" => 95, "bob" => 87} as map[str]i32;
// Returns i32 directly (not i32?)
let alice_score: i32 = scores["alice"]; // ✅ 95
let bob_score: i32 = scores["bob"]; // ✅ 87
let charlie_score: i32 = scores["charlie"]; // ❌ Panic: key not found!

Use direct indexing only when you’re certain the key exists.

The get() builtin function safely retrieves a value, returning an optional type (T?) if the key doesn’t exist:

let ages := {"alice" => 25, "bob" => 30} as map[str]i32;
// Returns i32? (optional), not i32!
let alice_age: i32? = get(&ages, "alice"); // Returns i32? with value 25
let unknown_age: i32? = get(&ages, "nobody"); // Returns i32? with value none

This design prevents common bugs! You can’t forget to check if a key exists because the type system reminds you.

The most common way to work with map values is using the coalescing operator ?? with get() to provide a default:

let scores := {
"alice" => 95,
"bob" => 87
} as map[str]i32;
// Get value or use default
let alice_score := get(&scores, "alice") ?? 0; // 95
let carol_score := get(&scores, "carol") ?? 0; // 0 (key doesn't exist)

Alternatively, use the get_or() builtin function for a more concise syntax:

let alice_score := get_or(&scores, "alice", 0); // 95
let carol_score := get_or(&scores, "carol", 0); // 0 (fallback)

This pattern is so useful because:

  • It’s concise - just one line
  • It’s safe - no crashes
  • It’s clear - the default value is right there

You can use the has() builtin to check if a key exists before accessing it:

let user_emails := {
"alice" => "alice@example.com",
"bob" => "bob@example.com"
} as map[str]str;
// Check if key exists
if has(&user_emails, "alice") {
// Safe to use direct indexing
let email: str = user_emails["alice"];
send_email(email);
} else {
io::Println("No email found");
}

Or use get() and check the optional:

let email: str? = get(&user_emails, "alice");
if email != none {
// Inside this block, email is str (not str?)
send_email(email);
} else {
io::Println("No email found");
}

When you check != none, Ferret knows the value exists and automatically narrows the type!

String keys are super common for configuration, user data, etc:

let config := {
"api_url" => "https://api.example.com",
"timeout" => "30s",
"retry_count" => "3"
};
let api_url := config["api_url"] ?? "https://default.com";

Perfect for lookup tables and ID mappings:

let id_to_name := {
1 => "Alice",
2 => "Bob",
3 => "Charlie"
};
let name := id_to_name[1] ?? "Unknown";

Values can be different types than keys:

let user_scores := {
"alice" => 95,
"bob" => 87
} as map[str]i32;
let product_prices := {
"laptop" => 999.99,
"mouse" => 24.99
} as map[str]f64;
fn main() {
// Store user ages
let user_ages : map[str]i32 = {
"alice" => 25,
"bob" => 30,
"charlie" => 35
};
// Safe access with defaults
let alice_age := user_ages["alice"] ?? 0;
let unknown_age := user_ages["nobody"] ?? 18; // Default to 18
io::Println("Alice is " + alice_age + " years old");
}
fn main() {
let inventory : map[str]i32 = {
"apples" => 50,
"bananas" => 30,
"oranges" => 40
};
// Check stock levels
let apple_count := inventory["apples"] ?? 0;
let grape_count := inventory["grapes"] ?? 0; // Not in stock
if apple_count > 0 {
io::Println("We have apples!");
}
}
fn main() {
let settings := {
"theme" => "dark",
"font_size" => "14",
"auto_save" => "true"
};
let theme := settings["theme"] ?? "light";
let font := settings["font_size"] ?? "12";
io::Println("Theme: " + theme);
}

Ferret validates your map operations at compile time:

let scores := {"alice" => 95};
// ✅ Correct: returns i32?
let score: i32? = scores["alice"];
// ✅ Correct: unwrap with coalescing
let value: i32 = scores["alice"] ?? 0;
// ❌ Error: can't assign i32? to i32
let bad: i32 = scores["alice"];

Unlike many languages, Ferret maps never crash when accessing missing keys:

let data := {"key1" => 100};
// In other languages, this might crash!
// In Ferret, you get 'none' safely
let missing := data["key_does_not_exist"] ?? -1; // Returns -1

This design eliminates an entire class of runtime errors!

When using coalescing operator, always think about what default makes sense:

let scores := {"alice" => 95};
// ✅ Good: meaningful default
let score := scores["bob"] ?? 0;
// ✅ Good: indicates "not found"
let score := scores["bob"] ?? -1;

Make your map types self-documenting:

// ✅ Clear what this map represents
let user_scores: map[str]i32;
// ✅ Clear ID mapping
let id_to_email: map[i32]str;

Always consider what happens when a key doesn’t exist:

let cache := {"page1" => "content"};
// Option 1: Provide default
let content := cache["page1"] ?? "Loading...";
// Option 2: Check explicitly
let maybe_content: str? = cache["page2"];
if maybe_content != none {
display(maybe_content);
} else {
load_from_server("page2");
}

Understanding how Ferret maps differ from other languages:

LanguageMissing Key Behavior
FerretReturns V? (optional) ✅
RustHashMap::get() returns Option<&V>
SwiftDictionary[key] returns V?
GoDual return value, ok := map[key]
PythonRaises KeyError exception ❌
JavaScriptReturns undefined

Ferret’s approach is similar to Rust and Swift - it makes missing values impossible to ignore through the type system.

Most common - use coalescing for instant defaults:

let value := mymap[key] ?? default_value;

Try multiple keys until one works:

let config := {"env" => "prod"};
let env := config["environment"] ??
config["env"] ??
"development"; // Ultimate fallback

Store the optional for later checking:

let user_data := {"id" => 123};
let maybe_id: i32? = user_data["id"];
// Check later
if maybe_id != none {
process(maybe_id);
}

Use the set() builtin function to add or update map entries. It requires a mutable reference (&mut T):

let scores := {"alice" => 95} as map[str]i32;
// Add new key (requires mutable reference)
set(&mut scores, "bob", 87);
// Update existing key
set(&mut scores, "alice", 96);

Use the remove() builtin function to remove a key from a map:

let scores := {"alice" => 95, "bob" => 87} as map[str]i32;
// Remove a key (requires mutable reference)
let removed := remove(&mut scores, "bob"); // true if found
// Check if key still exists
let has_bob := has(&scores, "bob"); // false

Learn more: See the Built-in Functions documentation for complete details on all container operations.

Maps in Ferret are:

  • Safe: Builtin functions prevent crashes
  • Explicit: You must handle missing keys
  • Type-safe: Keys and values have specific types
  • Ergonomic: Builtin functions make operations easy

Key takeaways:

  • Map syntax: map[KeyType]ValueType
  • Map literals: { key => value } as map[K]V
  • Direct indexing: map[key]V (panics if key missing)
  • Safe access: get(&map, key)V? (returns none if missing)
  • Use get_or() for defaults: get_or(&map, key, default)V
  • Use has() to check existence: has(&map, key)bool
  • Use set() to modify: set(&mut map, key, value)bool
  • Use remove() to delete: remove(&mut map, key)bool

Now that you understand maps, explore:

Maps, along with arrays, structs, and enums, form the foundation of Ferret’s data modeling capabilities!