Slide 1

Slide 1 text

const val CODE_READABILITY_SESSION = 4 var State

Slide 2

Slide 2 text

Brushup: Comments - Comment with whatever readers require - Refactor before/after writing comments - Documentation: Short summary (mandatory) and details (optional) - Informal comment: Explanations for large, complex, unintuitive code

Slide 3

Slide 3 text

Contents of this lecture - Introduction and Principles - Natural language: Naming, Comments - Inner type structure: State, Function - Inter type structure: Dependency I, Dependency II - Follow-up: Review State > Introduction

Slide 4

Slide 4 text

State and code execution/evaluation State will change while code execution/evaluation var i = 1 println(i) // prints `"1"` i = i + 1 println(i) // prints `"2"` State > Introduction

Slide 5

Slide 5 text

State and readability 1/2 It is hard to read overly stated code Question: Reference transparent/immutable == readable? Answer: No, sometimes it can be easier to read code with a side- effect or mutable state. State > Introduction

Slide 6

Slide 6 text

State and readability 2/2 Let us look at an example of a width-first search of a binary tree 5 6 8 3 1 9 5 node value State > Introduction

Slide 7

Slide 7 text

State and readability 2/2 Let us look at an example of a width-first search of a binary tree class Node(val value: Int, val left: Node?, val right: Node?) Case 1: With a mutable queue Case 2: With recursive call State > Introduction

Slide 8

Slide 8 text

Case 1: With a mutable queue fun search(valueToFind: Int, root: Node): Node? { val queue = ArrayDeque() var node: Node? = root while (node != null && node.value != valueToFind) { node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } node = queue.poll() } return node } State > Introduction

Slide 9

Slide 9 text

Case 2: With recursive call fun search(valueToFind: Int, root: Node): Node? = innerSearch(valueToFind, listOf(root)) tailrec fun innerSearch(valueToFind: Int, queue: List): Node? { val node = queue.getOrNull(0) if (node == null || node.value == valueToFind) { return node } val nextQueue = queue.subList(1, queue.size) + (node.left?.let { listOf(it) } ?: emptyList()) + (node.right?.let { listOf(it) } ?: emptyList()) return innerSearch(valueToFind, nextQueue) } State > Introduction

Slide 10

Slide 10 text

Problems of the recursive call example More complicated - Requires a separate public interface from recursive function - Complex queue calculation for the next iteration No guarantee that valueToFind is constant - Recursive call arguments can be changed for each call - A nested function is required to make it constant State > Introduction

Slide 11

Slide 11 text

What we should care about Simplifying state is just a way to make code readable/robust - Stateless/immutable/reference-transparent code is not the goal - Apply fold/recursive call only when it makes code readable Focus on actual program state rather than apparent immutability - Mutability within a small scope may be fine - Fold/recursive call may adversely affect actual state State > Introduction

Slide 12

Slide 12 text

Topics Important points for readability - Relationship between variables - State transition State > Introduction

Slide 13

Slide 13 text

Topics Important points for readability - Relationship between variables - State transition State > Relationship between variables

Slide 14

Slide 14 text

"Orthogonal" relationship: Definition State > Relationship between variables

Slide 15

Slide 15 text

"Orthogonal" relationship: Definition For two variables, when a value of one variable is determined, it does not affect the value domain of the other variable. State > Relationship between variables

Slide 16

Slide 16 text

"Orthogonal" relationship: Example Screen of in-service currency "coin" User ABC's coins 123,456 coins Coin usage/charge history charge 100,000 comics 1,000 music 1,000 open/close button of coin history add use use 8 10 11 / / / 5 5 5 State > Relationship between variables

Slide 17

Slide 17 text

"Orthogonal" relationship: Example Screen of in-service currency "coin" Orthogonal: coinCount and coinHistoryVisibility coinCount value is not related to coinHistoryVisibility Non-orthogonal: coinCount and coinText Valid value combinations of coinCount and coinText are limited State > Relationship between variables

Slide 18

Slide 18 text

Basic policy on variable relationships Avoid non-orthogonal relationships Example of class with a non-orthogonal relationship class CoinState(val coinCount: Int, val coinText: String) May cause an illegal state CoinState(coinCount = 10, coinText = "0 coin") State > Relationship between variables

Slide 19

Slide 19 text

How to remove non-orthogonal relationships - Replace with a function or a getter property - Use sum type State > Relationship between variables

Slide 20

Slide 20 text

Replace with a function or getter property 1/2 coinText can be obtained from coinCount class CoinState( val coinCount: Int, val coinText: String ) Allow only a value of coinCount to be specified State > Relationship between variables > Replace with a function or a getter property

Slide 21

Slide 21 text

Replace with a function or getter property 2/2 Calculate coinText from coinCount class CoinState( val coinCount: Int ) { fun coinText(formatter: Formatter): String = formatter.getQuantityString(coinCount, "coin") } Illegal CoinState instances cannot exist State > Relationship between variables > Replace with a function or a getter property

Slide 22

Slide 22 text

How to remove non-orthogonal relationships - Replace with a function or a getter property - Use sum type State > Relationship between variables > Use sum type

Slide 23

Slide 23 text

Use sum type 1/3 Result or error layout of coin status User ABC's coins loading... 123,456 coins Under maintenance view layout of result view layout of error Either one of the layouts is shown State > Relationship between variables > Use sum type

Slide 24

Slide 24 text

Use sum type 2/3 Only one of resultText and errorCode is non-null class QueryResponse( val resultText: String?, val errorCode: Int? ) One value cannot be obtained from the other Represent exclusive values as direct sum types State > Relationship between variables > Use sum type

Slide 25

Slide 25 text

Sum type A type that holds only one value of several types Corresponding to a disjoint set of set theory Example of QueryResponse: QueryResponse = Result | Error Realized by: Tagged union, variant, sealed class, associated value ... State > Relationship between variables > Use sum type

Slide 26

Slide 26 text

Use sum type 3/3 sealed class QueryResponse { class Result(val resultText: String): QueryResponse() class Error(val errorCode: Int): QueryResponse() } QueryResponse has resultText or errorCode exclusively State > Relationship between variables > Use sum type

Slide 27

Slide 27 text

How to remove non-orthogonal relationships - Replace with a function or a getter property - Use sum type - Emulate sum type by a small class - Use an enum for a special case State > Relationship between variables > Use sum type

Slide 28

Slide 28 text

Emulate sum type 1/2 Some languages do not have algebraic data type (e.g., Java ~14) Emulate sum type with a small class State > Relationship between variables > Use sum type

Slide 29

Slide 29 text

Emulate sum type 2/2 class QueryResponse { @Nullable private final String resultText; @Nullable private final Integer errorCode; private QueryResponse(...) { ... } @NonNull static QueryResponse asResult(@NonNull String ...) { ... } @NonNull static QueryResponse asError(int errorCode) { ... } State > Relationship between variables > Use sum type

Slide 30

Slide 30 text

How to remove non-orthogonal relationships - Replace with a function or a getter property - Use sum type - Emulate sum type by a small class - Use an enum for a special case State > Relationship between variables > Use sum type

Slide 31

Slide 31 text

Use an enum for a special case An enum may be enough to remove a non-orthogonal relationship // isResultViewShown && isErrorViewShown can't happen var isResultViewShown: Boolean var isErrorViewShown: Boolean An enum can remove (true, true) case enum class VisibleViewType { RESULT_VIEW, ERROR_VIEW, NOTHING } State > Relationship between variables > Use sum type

Slide 32

Slide 32 text

Relationship between variables: Summary Relationships of variables: orthogonal and non-orthogonal - Represents whether a value affects domain of another variable - Important not to make illegal state How to avoid non-orthogonal relationship - Replace with a function or a getter property - Use sum type, enum, or a small class State > Relationship between variables > Summary

Slide 33

Slide 33 text

Topics Important points for readability - Relationship between variables - State transition State > State transition

Slide 34

Slide 34 text

Types of state transition - Immutable: e.g., constant value - Idempotent: e.g., closable, lazy - Acyclic (except for self-loop): e.g., resource stream - Cyclic: e.g., reusable object State > State transition

Slide 35

Slide 35 text

Types of state transition - Immutable: e.g., constant value - Idempotent: e.g., closable, lazy - Acyclic (except for self-loop): e.g., resource stream - Cyclic: e.g., reusable object State > State transition

Slide 36

Slide 36 text

Immutable object All properties will not be changed // Examples of "immutable" class Immutable(val value: Int) class AnotherImmutable(val immutable: Immutable) // Examples of "mutable" class Mutable(var value: Int) class AnotherMutable(var immutable: Immutable) class YetAnotherMutable(val mutable: Mutable) State > State transition > Immutable

Slide 37

Slide 37 text

Note on immutability 1/3 Try to make properties immutable, even if only partially class CheckBoxViewModel( val text: String, var isEnabled: Boolean ) Consider separating classes for each value lifecycle State > State transition > Immutable

Slide 38

Slide 38 text

Note on immutability 2/3 Be aware of the difference between immutable and read-only val mutableList: MutableList = mutableListOf() val list: List = mutableList println(list.size) // => "0" mutableList += itemModel println(list.size) // => "1" State > State transition > Immutable

Slide 39

Slide 39 text

Note on immutability 3/3 Avoid making both a reference and the referenced object mutable (if possible) var itemModelList: MutableList fun clearItemModelList() { // itemModelList.clear() or itemModelList = emptyMutableList() } State > State transition > Immutable

Slide 40

Slide 40 text

Note on immutability 3/3 Avoid making both a reference and the referenced object mutable (if possible) var itemModelList: MutableList val list = itemModelList fun clearItemModelList() { // itemModelList.clear() or itemModelList = emptyMutableList() } Hard to understand the affected scope of the state change State > State transition > Immutable

Slide 41

Slide 41 text

Types of state transition - Immutable: e.g., constant value - Idempotent: e.g., closable, lazy - Acyclic (except for self-loop): e.g., resource stream - Cyclic: e.g., reusable object State > State transition > Idempotent

Slide 42

Slide 42 text

Idempotence The result is the same for a single execution or multiple val closable = Closable() // "Open" state closable.close() // "Closed" state closable.close() // Valid. Keep "Closed" state Side-effects may be hidden (e.g, Lazy of Kotlin) State > State transition > Idempotent

Slide 43

Slide 43 text

State graph of idempotent object Two states with a transition and self-loop Open Closed self-loop initial state State > State transition > Idempotent

Slide 44

Slide 44 text

Benefit of Idempotence Elimination of illegal state transitions // We may forget to check `isClosed` if (!nonIdempotentClosable.isClosed()) { nonIdempotentClosable.close() } // We can simply call `close` for an idempotent instance idempotentClosable.close() State > State transition > Idempotent

Slide 45

Slide 45 text

Application of self-loop A self-loop is also effective for a non-idempotent object Put a self-loop at the last state of transition Zero Two runtime error initial state One State > State transition > Idempotent

Slide 46

Slide 46 text

Application of self-loop A self-loop is also effective for a non-idempotent object Put a self-loop at the last state of transition None Multiple self-loop initial state Single State > State transition > Idempotent

Slide 47

Slide 47 text

Types of state transition - Immutable: e.g., constant value - Idempotent: e.g., closable, lazy - Acyclic (except for self-loop): e.g., resource stream - Cyclic: e.g., reusable object State > State transition > Cyclic/acyclic

Slide 48

Slide 48 text

Acyclic and cyclic transitions 1/2 Acyclic: Has no trail to reach a previous state Cyclic: Has a trail to reach a previous state State1 State2 State3 State1 State2 State3 acyclic cyclic State > State transition > Cyclic/acyclic

Slide 49

Slide 49 text

Acyclic and cyclic transitions 2/2 "Acyclic" is preferred (except for self-loop) Make objects disposable - Create a new object to reset the state - May have disadvantage in performance State > State transition > Cyclic/acyclic

Slide 50

Slide 50 text

Example of "acyclic" 1/2 Class DurationLogger Measuring Finished finishMeasurement finishMeasurement initial state State > State transition > Cyclic/acyclic

Slide 51

Slide 51 text

Example of "acyclic" 2/2 class DurationLogger(...) { private var state: State = State.Measuring(...) fun finishMeasurement() { val measuringState = state as? State.Measuring ?: return ... state = State.Finished } private sealed class State { class Measuring(val startedTimeInNanos: Long) : State() object Finished : State() State > State transition > Cyclic/acyclic

Slide 52

Slide 52 text

Example of "cyclic" 1/2 Reusable by startMeasurement Measuring Stopped finishMeasurement finishMeasurement startMeasurement startMeasurement initial state State > State transition > Cyclic/acyclic

Slide 53

Slide 53 text

Example of "cyclic" 2/2 class DurationLogger(...) { private var state: State = State.Stopped fun startMeasurement() { if (state == State.Stopped) { state = State.Measuring(...) } } fun finishMeasurement() { val measuringState = state as? State.Measuring ?: return ... state = State.Stopped State > State transition > Cyclic/acyclic

Slide 54

Slide 54 text

Bad point of "cyclic" Easy to misuse private val logger = DurationLogger(...) fun runSomeHeavyTask() { logger.startMeasurement() ... runAnotherHeavyTask() // Bug: `logger` is touched internally logger.finishMeasurement() } private fun runAnotherHeavyTask() { /* Use `logger` here, too */ } State > State transition > Cyclic/acyclic

Slide 55

Slide 55 text

"Acyclic" VS reality A cycle is often required to make a model simple - May be overly complex to remove all cycles - Example of DurationLogger: Measuring <-> Paused Make the cycle as small as possible State > State transition > Cyclic/acyclic

Slide 56

Slide 56 text

Cycle encapsulation Put a cycle in an enclosing state = Do not create a large cycle Measuring Finished initial state Paused finish pause resume Measuring/Paused State > State transition > Cyclic/acyclic

Slide 57

Slide 57 text

Types of state transition - Immutable: e.g., constant value - Idempotent: e.g., closable, lazy - Acyclic (except for self-loop): e.g., resource stream - Cyclic: e.g., reusable object State > State transition > Summary

Slide 58

Slide 58 text

State transition: Summary - Use immutability and idempotence effectively - Remove/minimize the cycle State > State transition > Summary

Slide 59

Slide 59 text

Summary Simplify state for readability and robustness - Do not make it an objective Avoid non-orthogonal relationships - Replace with function or use sum-type Care about state transition - Immutability, idempotence, and cycle State > Summary