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. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. Case 1: With a mutable queue fun search(valueToFind: Int, root:

    Node): Node? { val queue = ArrayDeque<Node>() 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
  8. Case 2: With recursive call fun search(valueToFind: Int, root: Node):

    Node? = innerSearch(valueToFind, listOf(root)) tailrec fun innerSearch(valueToFind: Int, queue: List<Node>): 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
  9. 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
  10. 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
  11. Topics Important points for readability - Relationship between variables -

    State transition State > Relationship between variables
  12. "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
  13. "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
  14. "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
  15. 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
  16. How to remove non-orthogonal relationships - Replace with a function

    or a getter property - Use sum type State > Relationship between variables
  17. 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
  18. 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
  19. How to remove non-orthogonal relationships - Replace with a function

    or a getter property - Use sum type State > Relationship between variables > Use sum type
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  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. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. Note on immutability 2/3 Be aware of the difference between

    immutable and read-only val mutableList: MutableList<ItemModel> = mutableListOf() val list: List<ItemModel> = mutableList println(list.size) // => "0" mutableList += itemModel println(list.size) // => "1" State > State transition > Immutable
  35. Note on immutability 3/3 Avoid making both a reference and

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

    the referenced object mutable (if possible) var itemModelList: MutableList<ItemModel> val list = itemModelList fun clearItemModelList() { // itemModelList.clear() or itemModelList = emptyMutableList() } Hard to understand the affected scope of the state change State > State transition > Immutable
  37. 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
  38. 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
  39. State graph of idempotent object Two states with a transition

    and self-loop Open Closed self-loop initial state State > State transition > Idempotent
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. Example of "cyclic" 1/2 Reusable by startMeasurement Measuring Stopped finishMeasurement

    finishMeasurement startMeasurement startMeasurement initial state State > State transition > Cyclic/acyclic
  48. 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
  49. 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
  50. "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
  51. 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
  52. 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
  53. State transition: Summary - Use immutability and idempotence effectively -

    Remove/minimize the cycle State > State transition > Summary
  54. 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