Clean Architecture
Chapter 6 - Functional Programming
- Lambda-calculus provided a foundation for function abstraction and application, influencing the design of functional programming languages
Squares of Integers
-
A Java snippet for printing the first 25 integers squared
public class Squint { public static void main(String args[]) { for (int i = 0; i < 25; i ++) System.out.println(i * i); } }
-
Clojure is a derivative of Lisp
-
A Clojure snippet for printing the first 25 integers squared
- Lazy evaluation causes elements within a lazy sequence to not be evaluated until it's accessed
(println (take 25 (map (fn [x] (* x x)) (range)))) ; (range) returns a lazy sequence of infinite numbers ; (take 25 list) returns the first 25 elements of the list
-
Takeaways
- Java uses a mutable variable (variable i changes state during exeuction of the program)
- No mutable variable was used in the Clojure snippet, variable x in the lambda function is never modified after initialisation
- Variables in functional languages don't vary
- Make code easier to reason about and less prone to errors (predictable state)
Immutability and Architecture
-
Concurrency issues
- All race conditions, deadlock conditions, and concurrent update problems stem from mutable variables
- Without mutable variables, race conditions and concurrent update issues are eliminated
- Deadlocks cannot occur without mutable locks
-
We want to ensure the robustness of systems using multiple threads and processors
-
Theoretical solution is to practice immutability given infinite storage and processor speed
- Infinite resources is not realistic, but immutability can still be practiced given compromises
Segregation of Mutability
-
Separate the application into mutable and immutable components
-
Immutable components
- Purely functional
- Don't use mutable variables
- Communicate with other components handling state mutations
-
Mutable components
- Allow for state of variables to be mutated
- Exposed to problems of concurrency
-
Transactional memory
- Protect mutable variables from the issues of concurrency, ensuring safe access and modification similar to how databases handle transactions
- In a database:
- A transaction is a sequence of operations (queries, updates, deletions) executed as a single unit
- Ensures all operations within the transaction either succeed or fail together (if any operation fails, the entire transaction is rolled back)
- Transforms the database from one valid state to another and ensures concurrent transactions don't interfere with each other
- In transactional memory:
- Sequence of operations on shared variables are treated as a single transaction, preventing partial updates
- Isolate transactions, preventing race conditions
- If a transaction fails due to conflict (another transaction modified the same variable), it can rollback and retry
-
Ex. Safely incrementing a counter
- An atom variable can be updated under conditions enforced by swap!
- swap! reads the current value, applies the callback function then calls compare-and-set!
(compare-and-set! atom oldval newval) ; Sets value of atom to newval IF value of atom = oldval, returns true ; Else, returns false
(def counter (atom 0)) (swap! counter inc) ; Ex. of swap! process ; 1. Read value of counter, oldval is 0 ; 2. Apply inc, newval is 1 ; 3. (compare-and-set! counter 0 1) ; 4. If value of counter is still 0, set value of counter to 1 and return true ; Else, return false. Re-attempt the entire swap! process until it succeeds
- Mutable component is the counter
- Immutable component is the pure function, inc
- Separating components that don't mutate variables and components that mutate variables
- inc does not directly update counter, the update is safely handled by swap!
Event Sourcing
- A strategy for storing the transactions, rather than the state. State is the result of applying all transactions from the beginning of time
- Ex. Banking application
- Currently store account balances (a mutable variable), updated with each deposit and withdrawal transactions
- Propose to store all transactions, then add them up whenever state is required, removing all mutable variables
- Requires infinite storage and processing power
- We can minimise the required storage and processing power to a realistic scenario
- Compute and save state once a day
- With improved technology, already have plenty of storage to store transactions
- No concurrency issues with just a create and read model (no update / delete)
- Given enough storage and processing power, applications can become entirely immutable, therefore entirely functional
- The necessity for mutable state diminishes as processors and storage capabilities become more powerful
Conclusion
-
The three paradigms has essentially taught us what not to do
- Structured programming restricts the use of goto statements or code that's disruptive to control flow
- Object-oriented programming restricts creating tightly coupled objects that are difficult to modify or extend
- Functional programming restricts the use of mutable variables and non-pure functions that cause unpredictable behaviour
-
Core principals of software have remained constant, despite better tools and more powerful hardware