0
0
Rubyprogramming~15 mins

Immutable data with freeze in Ruby - Deep Dive

Choose your learning style9 modes available
Overview - Immutable data with freeze
What is it?
Immutable data means data that cannot be changed after it is created. In Ruby, the freeze method is used to make an object immutable by preventing any modifications to it. Once an object is frozen, attempts to change it will cause errors. This helps keep data safe and predictable in programs.
Why it matters
Without immutable data, programs can accidentally change values in unexpected ways, causing bugs that are hard to find. Freezing data ensures that once set, important values stay the same, making programs more reliable and easier to understand. It also helps when multiple parts of a program share data, preventing one part from breaking another.
Where it fits
Before learning about freezing, you should understand Ruby objects and how variables reference them. After this, you can explore deeper concepts like thread safety, functional programming, and how immutability helps in concurrent code.
Mental Model
Core Idea
Freezing an object locks it so no one can change it anymore, making it permanently fixed.
Think of it like...
Imagine writing a message on a whiteboard and then covering it with a clear plastic sheet. Now, no one can erase or change the message underneath, keeping it exactly as it was.
Object (modifiable) ──freeze──▶ Object (immutable)
  │                            │
  ▼                            ▼
Can change                 Cannot change
properties                properties or values
Build-Up - 7 Steps
1
FoundationUnderstanding Ruby objects and mutability
🤔
Concept: Objects in Ruby can usually be changed after creation, which is called mutability.
In Ruby, most objects like strings and arrays can be modified. For example, you can add elements to an array or change characters in a string. This is normal behavior and useful for many tasks.
Result
You can change the contents of objects freely, like adding items to an array or changing string letters.
Knowing that Ruby objects are mutable by default helps you understand why freezing is needed to prevent changes.
2
FoundationWhat does freeze do in Ruby?
🤔
Concept: The freeze method makes an object immutable, stopping any future changes.
Calling freeze on an object locks it. For example, calling freeze on a string means you cannot change its characters anymore. If you try, Ruby raises an error.
Result
Frozen objects cannot be changed; attempts to modify them cause errors.
Understanding freeze is key to controlling when and how data can be changed in your program.
3
IntermediateFreezing different object types
🤔Before reading on: do you think freezing an array also freezes the objects inside it? Commit to your answer.
Concept: Freezing an object only freezes that object, not the objects it contains inside.
If you freeze an array, you cannot add or remove elements, but the elements themselves can still be changed if they are mutable. For example, freezing an array of strings stops adding/removing strings, but you can still change the strings unless they are frozen too.
Result
Only the container is frozen; contained objects remain mutable unless frozen separately.
Knowing that freeze is shallow helps avoid bugs where inner data changes unexpectedly.
4
IntermediateChecking if an object is frozen
🤔
Concept: Ruby provides a method to check if an object is frozen or not.
You can call frozen? on any object to see if it is frozen. It returns true if the object is frozen, false otherwise. This helps you write safer code by checking before modifying.
Result
You can detect frozen objects and avoid errors by checking frozen? before changes.
Using frozen? helps prevent runtime errors and makes your code more robust.
5
IntermediateFreezing nested objects deeply
🤔Before reading on: do you think Ruby has a built-in way to freeze an object and all objects inside it deeply? Commit to your answer.
Concept: Ruby's freeze is shallow, but you can write code to freeze objects deeply, including nested ones.
To freeze an object and all objects inside it, you need to write a method that recursively calls freeze on the object and its contents. This ensures complete immutability for complex data structures.
Result
You can create fully immutable nested data by freezing all parts deeply.
Understanding shallow vs deep freeze is crucial for managing complex data safely.
6
AdvancedWhy frozen objects improve thread safety
🤔Before reading on: do you think freezing objects alone guarantees thread safety? Commit to your answer.
Concept: Frozen objects cannot be changed, so they can be safely shared between threads without locks.
In multi-threaded Ruby programs, mutable shared data can cause conflicts. Frozen objects prevent changes, so threads can read them safely without synchronization. However, freezing alone does not guarantee full thread safety if other mutable data is involved.
Result
Frozen objects reduce risks of data races and make concurrent code safer.
Knowing how immutability relates to thread safety helps write better concurrent programs.
7
ExpertSurprising behavior with frozen objects and dup
🤔Before reading on: do you think dup on a frozen object returns a frozen or unfrozen copy? Commit to your answer.
Concept: Calling dup on a frozen object returns an unfrozen copy, allowing modifications on the duplicate.
When you call dup on a frozen object, Ruby creates a shallow copy that is not frozen. This means you can modify the duplicate without affecting the original frozen object. This behavior is useful but can be surprising if you expect the copy to remain frozen.
Result
dup breaks immutability by creating a modifiable copy of a frozen object.
Understanding dup's behavior prevents bugs where frozen data is unintentionally changed through copies.
Under the Hood
Ruby objects have an internal flag that marks them as frozen. When freeze is called, this flag is set. Any method that tries to modify the object checks this flag and raises a RuntimeError if the object is frozen. This check happens at runtime for every mutating method call.
Why designed this way?
Freeze was designed to be simple and efficient by using a flag rather than copying or locking data. This allows quick checks without heavy overhead. The shallow freeze approach keeps performance reasonable, while giving programmers control to freeze nested objects if needed.
┌───────────────┐
│ Ruby Object   │
│ ┌───────────┐ │
│ │ frozen?   │─┼─▶ true/false flag
│ └───────────┘ │
│               │
│ Methods check │
│ frozen? flag  │
│ before change │
└──────┬────────┘
       │
       ▼
  If frozen? == true
  ────────────────▶ Raise error on modification
Myth Busters - 4 Common Misconceptions
Quick: Does freezing an array also freeze the objects inside it? Commit to yes or no before reading on.
Common Belief:Freezing an array makes everything inside it immutable too.
Tap to reveal reality
Reality:Freezing an array only prevents changing the array itself, not the objects inside it.
Why it matters:Assuming deep freeze causes bugs when inner objects are changed unexpectedly, breaking immutability assumptions.
Quick: Does dup on a frozen object return a frozen copy? Commit to yes or no before reading on.
Common Belief:Duplicating a frozen object keeps it frozen.
Tap to reveal reality
Reality:dup returns an unfrozen copy, allowing changes to the duplicate.
Why it matters:This can lead to accidental mutations of data thought to be immutable, causing subtle bugs.
Quick: Does freezing an object guarantee thread safety? Commit to yes or no before reading on.
Common Belief:Frozen objects are always safe to use in any thread without issues.
Tap to reveal reality
Reality:While frozen objects cannot be changed, thread safety depends on all shared data being immutable and no other mutable state.
Why it matters:Relying solely on freeze for thread safety can cause race conditions if other mutable data is accessed concurrently.
Quick: Can you unfreeze an object once frozen? Commit to yes or no before reading on.
Common Belief:You can unfreeze an object by calling some method or duplicating it.
Tap to reveal reality
Reality:Objects cannot be unfrozen; freeze is permanent for that object instance.
Why it matters:Trying to unfreeze leads to confusion and misuse of freeze, causing design mistakes.
Expert Zone
1
Freezing is shallow by design to keep performance high and give programmers control over nested data immutability.
2
Frozen objects still allow instance variables to be reassigned if they hold mutable objects, so deep immutability requires careful design.
3
Using freeze in combination with frozen string literals (introduced in Ruby 2.3) can optimize memory and performance by reusing immutable strings.
When NOT to use
Do not use freeze when you need to modify objects later or when working with large nested structures where deep immutability is required; instead, consider using immutable data structures from gems like 'immutable' or functional programming techniques.
Production Patterns
In production, freeze is often used to lock configuration data, constants, and shared resources to prevent accidental changes. It is also used in libraries to protect internal state and improve thread safety without heavy locking.
Connections
Functional programming
Builds-on
Understanding freeze helps grasp immutability, a core principle in functional programming that avoids side effects and makes code easier to reason about.
Concurrency and thread safety
Builds-on
Knowing how freeze prevents data changes aids in designing thread-safe programs by sharing immutable data without locks.
Legal contracts
Analogy in different field
Just like freezing an object locks it from changes, a signed legal contract locks agreements so parties cannot change terms later, ensuring trust and predictability.
Common Pitfalls
#1Assuming freezing an array freezes its elements too.
Wrong approach:arr = ["hello", "world"] arr.freeze arr[0].upcase! # This changes the string inside the frozen array
Correct approach:arr = ["hello", "world"] arr.each(&:freeze) arr.freeze arr[0].upcase! # Raises error because string is also frozen
Root cause:Misunderstanding that freeze is shallow and does not affect nested objects.
#2Expecting dup to keep the frozen state of an object.
Wrong approach:str = "hello".freeze copy = str.dup copy << "!" # This works and modifies copy
Correct approach:str = "hello".freeze copy = str.dup.freeze copy << "!" # Raises error because copy is frozen explicitly
Root cause:Not realizing dup returns an unfrozen copy by default.
#3Trying to unfreeze an object after freezing it.
Wrong approach:obj = "data".freeze obj.unfreeze # No such method, error
Correct approach:obj = "data".freeze obj = obj.dup # Create a new unfrozen copy instead
Root cause:Believing freeze can be reversed on the same object instance.
Key Takeaways
Freezing in Ruby makes objects immutable by setting an internal flag that prevents changes.
Freeze is shallow: it locks only the object itself, not nested objects inside it.
Frozen objects help prevent bugs by ensuring data does not change unexpectedly, especially in shared or concurrent contexts.
dup on a frozen object returns an unfrozen copy, which can be modified unless frozen again.
Understanding freeze deeply improves code safety, thread safety, and helps write more predictable Ruby programs.