Uncontrolled mutation can make it challenging to understand the flow of data and the interactions between different parts of a program. This complexity arises because changes made to the state in one part of your program can have unexpected consequences in other parts.
Functional programming addresses this challenge by defaulting to immutability. Functional languages also introduce (encode? formalize?) abstractions such as monads to manage side effects while still ensuring referential transparency, “purity”, etc. Languages like Haskell even have constructs such as mutable reference cells (IORef
) which provides the convenience of mutable objects through a controlled mechanism for managing mutability while still preserving the advantages of immutability and purity.
The functional approach is effective (and oftentimes aboslutely fascinating). But it is just one approach. Rust’s ownership model is another and I think it presents unique advantages in terms of managing side effects and mutation compared to functional programming. One simply or obvious advantage is that Rust’s ownership model — which makes it possible to reason about local, in-place mutation within specific, constrained boundaries — is (at least arguably) more intuitive than monads and the like.
An example of those constraints: Rust enables passing a mutable reference (&mut
) to a database connection object into a function, indicating that the function may execute SQL queries, modify the database, and so on. Or you can pass a mutable reference to a terminal buffer (tui::buffer::Buffer
) into a function, allowing the function to modify the contents of the buffer. But mutable variables and mutable references must be explicitly annotated using the mut
keyword. Moreover, you cannot simply mutate all the time, from anywhere, whenever you want. Borrowing allows multiple references to the same data, but enforces strict rules to prevent data races and other problems. For example, you can have either one mutable reference (&mut
) or multiple immutable references (&
) to a piece of data at any given time, but not both simultaneously.
Beyond being (arguably) more intuitive than the FP constructs, there’s an argument to be made that Rust’s approach offers more flexibility in terms of enabling a much wider range of concurrency paradigms and patterns. I need to flesh this out some more, but I’m thinking of things like:
- Shared mutable state between threads: producer-consumer, with multiple threads producing data and feeding it into a shared queue for consumption by other threads.
- Locking and synchronization: with low-level primitives like mutexes, semaphores, etc.
- Concurrent data structures: like concurrent queues, stacks, hash maps, etc.
- Parallelism with SIMD
I’m not saying these patterns aren’t possible in FP, but I do think they are more straightforward in a language like Rust.
Overall, I’m not really saying anything novel or groundbreaking here, or at least I don’t think I am. I just wanted to share my reflections on the contrasting methods of managing unrestrained mutation, which have recently become clearer to me for some reason or another.