Skip to content

Latest commit

 

History

History
609 lines (424 loc) · 8.95 KB

talk-immutability-pure-functions.md

File metadata and controls

609 lines (424 loc) · 8.95 KB

author: Malte Neuss title: Immutability & Pure Functions

revealjs

theme: sky transition: none slideNumber: true

Reasoning about code

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]

Content

::: incremental

  • Immutability
  • Pure Functions
  • Apply to OO

:::

. . .

Easier reasoning, testing, debugging..

Immutability

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]

Benefit: Reasoning

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]

Pure Functions

$$ \begin{align*} f\colon \mathbb{R} &\to \mathbb{R} \qquad &\text{Type} \\ f(x) &= x + \pi \qquad &\text{Body} \end{align*} $$

. . .

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

:::

Side-Effect

::: incremental

  • File read/write
  • Network access
  • I/O
  • Throwing exception
  • Argument mutation
  • ...

:::

. . .

Any state change outside of return value

def f(x: float) -> float

Benefit: Reasoning

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!                    

Benefit: Testing

def test()                                    
   values   = [5,2,7]                         # data        
   expected = 2

   out = minimum(values)                       # pure

   assert out == expected 

. . .

cost(data) < cost(classes)

Benefit: Refactoring

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

Impure Functions

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

Benefit: Abstraction

def f(_: Char) -> Int

. . .

def toAscii(c: Char) -> Int

. . .

def f(_: [Any]) -> Int

. . .

def length(list: [Any]) -> Int

Types often enough for understanding.

Impure Functions

def f() -> None

. . .

def performTask() -> None
  inputs = loadInputs()
  computeResult(inputs)

. . .

def launchMissiles() -> None
  coord = loadCoordinates()
  ...

Need to look at body to be sure.

Classical OO

class MyClass
  state: LotsOfState                          # private

. . .

  def do() -> None                            # public
    state.do_a()   
    state.do_b()  
    this._do()   

. . .

  def _do()                                   # private
    state.do_c()
    ...

Classical OO

:::::::::::::: {.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()
...

FP-ish OO

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

Global Mutation

state: LotsOfState                            # Global variable

. . .

def someOperation()                                
   state.do_a()                               # Global mutation

. . .

def main()                                
   ...
   someOperation()                             
   ...

Class Mutation

class MyClass
  state: LotsOfState                          # Class variable

. . .

  def someOperation()                                
    state.do_a()                              # Class mutation

. . .

def main()                                
   ...
   myClass.someOperation()                             
   ...

Output arguments

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

How to Side-Effects

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

Further Topics

:::::::::::::: {.columns} ::: {.column width="50%"}

Try

  • Scala
  • Rust
  • Haskell

::: ::: {.column width="50%"}

Category Theory

{ width=40% }

  • Algebraic Data Types
  • Functor (map)
  • Monad (flatMap)
  • Lens ...

::: ::::::::::::::

::: notes Image is public domain :::

Questions?

Thanks

Type Checker Support

Make illegal state (more) unrepresentable

Immutable Built-in Classes

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 Custom Classes

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)

Immutable Custom Classes

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;
}

Immutable Variables

Mutable:

var value = 1                                 // Typescript
value = 2                                     // ok

. . .

Immutable:

const value = 1                               // Typescript
value = 2                                     // type error!