Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Munetoshi Ishikawa

May 11, 2023
Tweet

More Decks by Munetoshi Ishikawa

Other Decks in Programming

Transcript

  1. const val CODE_READABILITY_SESSION = 4
    var State

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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.left?.let { queue.add(it) }
    node.right?.let { queue.add(it) }
    node = queue.poll()
    }
    return node
    }
    State > Introduction

    View Slide

  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

    View Slide

  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

    View Slide

  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 fine
    - Fold/recursive call may adversely affect actual state
    State > Introduction

    View Slide

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

    View Slide

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

    View Slide

  14. "Orthogonal" relationship: Definition
    State > Relationship between variables

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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 specified
    State > Relationship between variables > Replace with a function or a getter property

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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
    Care about state transition
    - Immutability, idempotence, and cycle
    State > Summary

    View Slide