66

# Code readability: Session 4 (ver. 2, En)

May 11, 2023

## Transcript

1. const val CODE_READABILITY_SESSION = 4
var State

- Comment with whatever readers require
- Documentation:
Short summary (mandatory) and details (optional)
- Informal comment:
Explanations for large, complex, unintuitive code

3. Contents of this lecture
- Introduction and Principles
- Inner type structure: State, Function
- Inter type structure: Dependency I, Dependency II
- Follow-up: Review
State > Introduction

4. 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

It is hard to read overly stated code
Answer: No, sometimes it can be easier to read code with a side-
effect or mutable state.
State > Introduction

Let us look at an example of a width-ﬁrst search of a binary tree
5
6 8
3 1
9
5 node
value
State > Introduction

Let us look at an example of a width-ﬁrst 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

8. 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 = queue.poll()
}
return node
}
State > Introduction

9. 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

10. 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

11. 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 ﬁne
- Fold/recursive call may adversely affect actual state
State > Introduction

12. Topics
- Relationship between variables
- State transition
State > Introduction

13. Topics
- Relationship between variables
- State transition
State > Relationship between variables

14. "Orthogonal" relationship: Deﬁnition
State > Relationship between variables

15. "Orthogonal" relationship: Deﬁnition
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

16. "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
use
use
8
10
11
/
/
/
5
5
5
State > Relationship between variables

17. "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

18. 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

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

20. 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 speciﬁed
State > Relationship between variables > Replace with a function or a getter property

21. 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

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

23. Use sum type 1/3
Result or error layout of coin status
User ABC's coins
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

24. 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

25. 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

26. 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

27. 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

28. 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

29. 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

30. 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

31. 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

32. 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

33. Topics
- Relationship between variables
- State transition
State > State transition

34. 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

35. 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

36. 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

37. 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

38. 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

39. 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

40. 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

41. 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

42. 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

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

44. Beneﬁt 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

45. 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

46. 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

47. 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

48. 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

49. 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

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

51. 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

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

53. 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

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

55. "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

56. 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

57. 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

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

59. Summary
Simplify state for readability and robustness
- Do not make it an objective
Avoid non-orthogonal relationships
- Replace with function or use sum-type