Lesson 3.3

Value Semantics

Everything copies. Assignment copies. Function args copy. Why Monk chose this and what it means.

monk 15 min read

Your data is yours.

In Monk, when you assign a value to a variable, you get your own copy. Nobody else can change it. No function you call can modify your data behind your back. No closure can silently mutate what you thought was frozen.

This is called value semantics. It means variables hold values, not references to values. When you copy a variable, you get a completely independent clone.

let a = [1, 2, 3]
let b = a           // b is a COPY of a
append(b, 4)
show(a)              // [1, 2, 3] -- unchanged
show(b)              // [1, 2, 3, 4]

The array b is a completely separate array. Appending to b does not touch a. This is how numbers already work in every language -- let x = 5; let y = x; y = 10 doesn't change x. Monk extends this intuition to every type.

The alternative: reference semantics.

In JavaScript or Python, variables for objects and arrays are labels pointing to data in memory. Multiple labels can point to the same data. Change the data through one label, and every other label sees the change.

// JavaScript -- reference semantics
let a = [1, 2, 3];
let b = a;           // b points to the SAME array
b.push(4);
console.log(a);      // [1, 2, 3, 4] -- surprise!

This is convenient when you want shared state. But it's a source of subtle bugs when you don't. You pass an array to a function, and the function modifies it. You store a value in a closure, and the outer scope changes it. Debugging becomes archaeology -- tracing who changed what and when.

Monk's philosophy: explicit over implicit. If sharing is needed, it should be opt-in, not the default. For now, everything copies.

Three rules, no exceptions.

1

Assignment = deep copy.

let b = a creates a fully independent clone of a. If a is an array of records, every record is copied too. All the way down.

2

Function arguments = deep copy.

When you call process(data), the function receives its own copy. The original data cannot be modified by process.

3

Closures = deep copy of captured variables.

When a closure is created, it snapshots every variable it references. Later changes to the original variables don't affect the closure, and vice versa.

How deep copy works under the hood.

The runtime provides a monk_deep_copy() function. When called on a MonkValue, it checks the tag and decides what to do:

Int, float, bool, none: return a copy of the struct. These values live inline in the union, so copying the struct is enough. Cheap.

String: allocate new memory, copy the bytes. The new MonkValue points to a fresh heap buffer.

Array: allocate a new array, then deep-copy every element. If the array contains records, those records are copied too. Recursive.

Record: allocate a new record, deep-copy every field value. Same recursive approach.

Similarly, monk_free() walks the same structure in reverse -- freeing nested heap allocations before freeing the parent. No value is leaked.

Deep const: freezing all the way down.

Monk has two variable keywords: let (mutable) and const (immutable). But Monk's const goes deeper than most languages.

const config = { host: "localhost", port: 8080 }
config.port = 9090   // ERROR: config is const

In JavaScript, const prevents reassigning the variable but not mutating the object it points to. In Monk, const freezes both the variable and its contents. The record's fields cannot be changed either. This is sometimes called "deep const" or "transitive immutability."

let means fully mutable -- the variable can be reassigned, and its contents can be modified.

The tradeoff: correctness vs. performance.

Deep copying everything is not free. Copying an array of 1,000 elements means allocating memory for 1,000 new elements and copying each one. If those elements are records, every record is cloned too.

Why accept this cost?

1

Deterministic behavior. No spooky action at a distance. If a function doesn't touch a variable, the variable doesn't change. Period.

2

No garbage collector. Values are freed when they go out of scope. The C stack handles the lifetime. No GC pauses, no reference counting overhead.

3

Simplicity. The ownership model is trivial: whoever holds the value owns it. No shared ownership, no cycles, no weak references.

Future optimizations like escape analysis (skip the copy if the original is never used again) and copy-on-write (share until someone mutates) can be added later without changing the language semantics. The program behaves the same -- it just gets faster.

No garbage collector. No reference counting. Values are copied, used, and freed when they go out of scope. The C stack handles the rest.

Key takeaways

1

Value semantics means assignment, function calls, and closures all produce independent copies.

2

Deep copy is recursive: arrays of records copy every record, all the way down.

3

const freezes the variable AND its contents. let is fully mutable.

4

The tradeoff is performance (more copying) for correctness (no aliasing bugs) and simplicity (no GC).