Graduate Program KB

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