author: Malte Neuss title: Immutability & Pure Functions
def main()
prices = [5,2,7]
# assert prices[0] == 5
. . .
minPrice = minimum(prices)
# assert minPrice == 2
. . .
# assert prices[0] == 5?
. . .
def minimum(values)
values.sort() # surprise
return values[0]
::: incremental
- Immutability
- Pure Functions
- Apply to OO
:::
. . .
Easier reasoning, testing, debugging..
No reassign of variables:
value = 1 # Assign
# value = 2 # Reassign
other = 2
. . .
No mutation on objects:
def minimum(values)
# values.sort() # mutation
other = sorted(values) # no mutation
return other[0]
def main()
prices = immutable([5,2,7]) # if supported
# assert prices[0] == 5
minPrice = minimum(prices)
# assert prices[0] == 5!
. . .
def minimum(values)
values.sort() # error
return other[0]
. . .
def f(x: float) -> float: # Type
return x + math.pi # Body
::: incremental
- Same input, same output
- No side-effects
- External immutable values or constants ok
:::
::: incremental
- File read/write
- Network access
- I/O
- Throwing exception
- Argument mutation
- ...
:::
. . .
Any state change outside of return value
def f(x: float) -> float
def minimum(values) # pure
# values.sort()
other = sorted(values) # no mutation
return other[0]
. . .
def main()
prices = [5,2,7]
# assert prices[0] == 5
minPrice = minimum(prices) # pure
# assert prices[0] == 5!
def test()
values = [5,2,7] # data
expected = 2
out = minimum(values) # pure
assert out == expected
. . .
cost(data) < cost(classes)
Referential transparency:
def main():
y = compute(5,2) + 7
z = compute(5,2) + 1
. . .
=
def main():
x = compute(5,2) # Factored out
y = x + 7
z = x + 1
. . .
Fearless refactoring, also by compiler
No referential transparency:
def main(): # 2 side-effects
y = randomNumber() + 7
z = randomNumber() + 1
. . .
!=
def main(): # 1 side-effect
x = randomNumber() # Factored out
y = x + 7
z = x + 1
. . .
Careful refactoring, often manual
def f(_: Char) -> Int
. . .
def toAscii(c: Char) -> Int
. . .
def f(_: [Any]) -> Int
. . .
def length(list: [Any]) -> Int
Types often enough for understanding.
def f() -> None
. . .
def performTask() -> None
inputs = loadInputs()
computeResult(inputs)
. . .
def launchMissiles() -> None
coord = loadCoordinates()
...
Need to look at body to be sure.
class MyClass
state: LotsOfState # private
. . .
def do() -> None # public
state.do_a()
state.do_b()
this._do()
. . .
def _do() # private
state.do_c()
...
:::::::::::::: {.columns} ::: {.column width="50%"}
class MyClass
state: LotsOfState
def do() -> None
state.do_a()
state.do_b()
this._do()
def _do()
state.do_c()
...
::: ::: {.column width="50%"}
@startuml
class MyClass
MyClass : state: A
MyClass : state: B
MyClass : do()
together {
class A
A : state: C
A : do_a()
class B
B : state
B : do_b()
}
class C
C : state
C : do_c()
MyClass --r-> A
MyClass ---r--> B
A -r-> C
A -[hidden]d-> B
@enduml
::: ::::::::::::::
. . .
...
myClass.do()
...
class MyClass
constants
. . .
def do(start) # Pure
x = do_a(start) # No mutation
y = do_b(x, constants) # No mutation
z = this._do(y) # No mutation
return z
. . .
def _do(y) # Pure
return do_c(y, constants) # No mutation
state: LotsOfState # Global variable
. . .
def someOperation()
state.do_a() # Global mutation
. . .
def main()
...
someOperation()
...
class MyClass
state: LotsOfState # Class variable
. . .
def someOperation()
state.do_a() # Class mutation
. . .
def main()
...
myClass.someOperation()
...
Not ok:
def do(state):
# ... modify argument 'state'
. . .
x = do(state) # 'state' mutated
. . .
So why should this?
x = state.do() # 'state' mutated
. . .
class State:
def do(self): # 'self' is 'state'
# ... modify argument self
def performTask()
inputs = fetchInputs() # IO
result = 2*inputs
publish(result) # IO
. . .
# Infrastructure/Application layer # Impure
def performTask()
inputs = fetchInputs()
result = domainLogic(inputs)
publish(result)
# Domain layer
def domainLogic(inputs) # Pure
return 2*inputs
Separate IO and logic
:::::::::::::: {.columns} ::: {.column width="50%"}
Try
- Scala
- Rust
- Haskell
::: ::: {.column width="50%"}
Category Theory- Algebraic Data Types
- Functor (map)
- Monad (flatMap)
- Lens ...
::: ::::::::::::::
::: notes Image is public domain :::
Thanks
Make illegal state (more) unrepresentable
Immutable interface:
mySet = frozenset([1, 2, 3]) # Python
mySet.add(4) # type error
. . .
Copy on change:
val myList = immutable.List(1, 2, 3) // Scala
val other = myList.appended(4) // other list
Immutable interface around mutable data:
class MyClass:
_value: int # hide mutables
def getValue() # no setters
return _value
def calcSth() # _value read only
return _value*2
def incremented() # Copy on change
return MyClass(_value+1)
With extra language support:
@dataclass(frozen=True) # Python
class MyClass:
value: int
object = MyClass(1)
object.value = 2 # compile error!
. . .
interface MyInterface { // Typescript
readonly value: int;
}
Mutable:
var value = 1 // Typescript
value = 2 // ok
. . .
Immutable:
const value = 1 // Typescript
value = 2 // type error!