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

可読性から見たKotlinの言語機能 -「使いたい」の、その先へ。 / Kotlin language features and readability

可読性から見たKotlinの言語機能 -「使いたい」の、その先へ。 / Kotlin language features and readability

Kotlin language features and readability
可読性から見たKotlinの言語機能 -「使いたい」の、その先へ。

Munetoshi Ishikawa, LINE Corporation

※ この資料は以下のイベントの発表内容です
https://fortee.jp/kotlin-fest-2022/proposal/e23789e7-fc9d-4496-8646-ca5aa12399f9

LINE Developers

December 10, 2022
Tweet

More Decks by LINE Developers

Other Decks in Technology

Transcript

  1. Kotlin's language features Examples: null safety, data classes, extensions...
 Helps

    writing code - Removes boiler plates - Keeps code type-safe - Makes code concise and robust
  2. Drawbacks of language features "Feature overusing" may cause unreadable and

    non-robust code - Need to make the objective clear - Shouldn't mistake the means and the ends This talk demonstrates anti-patterns and the solutions
  3. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  4. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  5. Null safety Nullable/Non-null type is statically checked - cf. https://kotlinlang.org/docs/null-safety.html

    var nonNullInt: Int = 1 nonNullInt = null // compile error Introduce 1 anti-pattern example var nullableInt: Int? = 1 nullableInt + 1 // compile error
  6. Null safety: Anti-pattern example Function to return indices matching a

    condition fun indicesIncludingText(list: List<String>?, searchText: String?): Set<Int>? { What’s wrong with this code? if (searchText == null) { return null } return list.orEmpty().asSequence() .withIndex() .filter { (_, value) -> value.contains(searchText) } .map { (index, _) -> index } .toSet() }
  7. Null safety: Problem Inappropriate nullable parameters 
 
 Unpredictable behavior

    for null argument - Null list: Returns empty set - Null searchText: Returns a null set fun indicesIncludingText(list: List<String>?, searchText: String?): Set<Int>? {
  8. Null safety: Solution Options: 1. Use "standard/default/initial value" instead of

    null 2. Write documentation for null argument 3. Create a null object explicitly
  9. Null safety: Solution Options: 1. Use "standard/default/initial value" instead of

    null 2. Write documentation for null argument 3. Create a null object explicitly
  10. Null safety: Solution, option 1 Use "standard/default/initial value" instead of

    null - empty(): Collection - "": String - 0: adder, size, count - 1: multiplier, exponent - MIN_VALUE: timestamp, max value selection, lower bound - INVALID, INITIAL: model, state
  11. Null safety: Solution, option 1 (cont.) Select a standard/default/initial value

    carefully Some values might not be appropriate - 0 for invalid UInt/ULong value (ID, age, ...) - -1 or size for an invalid index - "" for invalid UUID string 
 Keep null if there is a special meaning
  12. Null safety: Solution Options: 1. Use "standard/default/initial value" instead of

    null 2. Write documentation for null argument 3. Create a null object explicitly
  13. Null safety: Solution, option 2 Write documentation for null argument

    class TextUiElement { fun setText(text: String?) { ... } } /** * Shows [text] in this UI element. * * This UI element is hidden with a null argument * while an empty string `""` is shown explicitly. */
  14. Null safety: Solution Options: 1. Use "standard/default/initial value" instead of

    null 2. Write documentation for null argument 3. Create a null object explicitly
  15. Null safety: Solution, option 3 Create a null object explicitly

    sealed class TextUiElementState { class Visible(text: String) : TextUiElementState() object Hidden : TextUiElementState() } Note: Avoid over-engineering class TextUiElement { fun setState(state: TextUiElementState) { ... } }
  16. Null safety: Summary Don't overuse null value - Replace with

    "default value" - Write documentation or create a null object
  17. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  18. Scope function Executes a given function within the context of

    an object - cf. https://kotlinlang.org/docs/scope-functions.html - 5 functions: let, run, with, apply, and also Typical purposes: - Nullable value handling - Function call grouping/chaining Introduce 1 anti-pattern example
  19. Scope function: Anti-pattern example Cache layer for query/response class ResponseValueCache<Q,

    V>(...) { private val cachedValues: MutableMap<Q, V> = mutableMapOf() } What’s wrong with this code? fun getCacheOrQuery(queryParams: Q): V? = cachedValues[query] ?: queryClient.query(queryParams) .let { when (it) { is Response.Success -> it.value is Response.Error -> null } }?.also { cachedValues[query] = it }
  20. Scope function: Problem Scope functions just for chaining .let {

    when (it) { is Response.Success -> it.value is Response.Error -> null } }?.also { cachedValues[query] = it }
  21. Scope function: Problem Scope functions just for chaining - Not

    easy to find the receiver .let { when (it) { is Response.Success -> it.value is Response.Error -> null } }?.also { cachedValues[query] = it }
  22. Scope function: Problem Scope functions just for chaining - Not

    easy to find the receiver - The important side-effect is hidden .let { when (it) { is Response.Success -> it.value is Response.Error -> null } }?.also { cachedValues[query] = it }
  23. Scope function: Problem Scope functions just for chaining - Not

    easy to find the receiver - The important side-effect is hidden .let { when (it) { is Response.Success -> it.value is Response.Error -> null } }?.also { cachedValues[query] = it }
  24. Scope function: Solution Options: 1. Simplify the chain: Extract as

    private functions or extensions 2. Break the chain .let { when (it) { is Response.Success -> it.value is Response.Error -> null } } queryClient.query(queryParams)
  25. Scope function: Solution Options: 1. Simplify the chain: Extract as

    private functions or extensions 2. Break the chain queryClient.query(queryParams).valueOrNull private val Response<R>.valueOrNull: R? get() = when(this) { ... }
  26. Scope function: Solution Options: 1. Simplify the chain: Extract as

    private functions or extensions 2. Break the chain: Define local values private val Response<R>.valueOrNull: R? get() = when(this) { ... } queryClient.query(queryParams).valueOrNull
  27. Scope function: Solution Options: 1. Simplify the chain: Extract as

    private functions or extensions 2. Break the chain: Define local values val responseValue = queryClient.query(queryParams).valueOrNull if (responseValue != null) { cachedValues[query] = responseValue } return responseValue private val Response<R>.valueOrNull: R? get() = when(this) { ... }
  28. Scope function: Solution Options: 1. Simplify the chain: Extract as

    private functions or extensions 2. Break the chain: Define local values val responseValue = queryClient.query(queryParams).valueOrNull if (responseValue != null) { cachedValues[query] = responseValue } return responseValue private val Response<R>.valueOrNull: R? get() = when(this) { ... }
  29. Scope function: Other anti-patterns Nested scope functions
 
 
 


    ?.let for not important value receiver.apply { property = MutableValue().apply { ... } anotherProperty = MutableValue().apply { ... } } option?.let { repository.store(data, it) } if (option != null) { repository.store(data, option) }
  30. Scope function: Summary Don't use scope functions just to make

    a chain - Make the objective of the chain clear - Simplify or break the chain if possible
  31. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  32. Data class A class to represent a plain product data

    type - cf. https://kotlinlang.org/docs/data-classes.html - Some functions (equals, toString, ...) are automatically derived Similar to case class, record, and tuple at other languages Introduce 2 anti-pattern examples
  33. Data class 1/2: Anti-pattern example A data class with -

    A readonly property of a mutable object - A non read-only property of an immutable object data class FooModel( val mutableElement: MutableElement, var immutableElement: ImmutableElement ) What’s wrong with this code?
  34. Data class 1/2: Problem Multiple ways to update the properties

    - Create a new instance - Modify a property directly 
 
 
 Data class can be shared and have a long lifecycle - The affected scope of modification is ambiguous fooModel.immutableElement = ImmutableElement(0) fooModel.copy(immutableElement = ImmutableElement(0))
  35. Data class 1/2: Solution Make data classes immutable - Use

    read-only variables - Use immutable property type instances data class FooModel(val immutableElement: ImmutableElement)
  36. Data class 2/2: Anti-pattern example A data class of UI

    layout attributes data class FooLayoutParameters( val titleText: String, val backgroundColorInt: Int, val buttonText: String, val onButtonClickListener: () -> Unit ) What’s wrong with this code?
  37. Data class 2/2: Problem Ambiguous equivalency data class FooLayoutParameters( val

    titleText: String, val backgroundColorInt: Int, val buttonText: String, val onButtonClickListener: () -> Unit )
  38. Data class 2/2: Problem Ambiguous equivalency // false FooLayoutParameters("", 0,

    "") { privateFunction() } == FooLayoutParameters("", 0, "") { privateFunction() } // true FooLayoutParameters("", 0, "", ::privateFunction) == FooLayoutParameters("", 0, "", ::privateFunction) // Java code // false new FooLayoutAttributes("", 0, "", JavaClass::staticMethod).equals( new FooLayoutAttributes("", 0, "", JavaClass::staticMethod) )
  39. Data class 2/2: Solution Confirm every property is data class

    friendly If not... - Use non-data class - Split properties into two classes class FooLayoutAttributes( val titleText: String, val backgroundColorInt: Int, val buttonText: String, val onButtonClickListener: () -> Unit )
  40. Data class: Other anti-patterns Non-product data model
 
 
 


    Special case: Should be replaced with a sealed class // An invalid instance is easily created by `copy`. data class CoinState(val coinCount: Int, val coinText: String) { constructor(coinCount: Int): this(coinCount, "Coins: $coinCount") } data class Response<R, E>(val result: R? = null, val error: E? = null)
  41. Data class: Summary Confirm whether a data class behaves as

    a plain product data type - Immutability - Equivalency
  42. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  43. Extension Defines a function of an existing receiver type -

    Adds a method without inheriting or decorator - cf. https://kotlinlang.org/docs/extensions.html Introduce 3 anti-pattern examples
  44. Extension 1/3: Anti-pattern example Parses the receiver and creates a

    UserModel instance - At the top-level of a Kotlin file fun String.toUserModel(): UserModel? { val userId = ... ?: return null val userName = ... ?: return null ... return UserModel(userId, userName, ...) } What’s wrong with this code?
  45. Extension 1/3: Solution Avoid "public" or "extension" for feature specific

    code - Use internal or private - Define as a normal function in an object 
 Confirm if it's natural to define the method in the receiver class class String: ... { fun toFeatureSpecificModel(): FeatureSpecificModel = ...
  46. Extension 2/3: Anti-pattern example An extension which modifies an instance

    member open class FooClass { private val registeredElements: MutableSet<Element> = mutableSetOf() protected fun List<Element>.register() { registeredElements += this } } What’s wrong with this code?
  47. Extension 2/3: Problem The implicit receiver hides modification
 register modifies

    the parent property class BarClass(...): FooClass() { fun someFunction() { ... someDataList.register() } }
  48. Extension 2/3: Root cause How do we guess a function

    behavior 1. Has an important return value: No modification 2. Has a receiver: Modify the receiver 3. Has a parameter: Modify the parameter 
 someDataList.register() looks modifying the receiver
  49. Extension 2/3: Solution Remove implicit receiver modification - An instance

    method is enough open class FooClass { protected fun register(list: Collection<Element>) { registeredElements += list } } ... val listToRegister = dataList.map...filter... register(listToRegister) - A receiver-less function implies that this can be the receiver
  50. Extension 3/3: Anti-pattern example Background: - Observable instance is observed

    while ObserverScope is alive interface Observable<T> { /** * Calls `action` when this instance is updated and `scope` is alive... */ fun observe(scope: ObserverScope, action: (T) -> Unit) }
  51. Extension 3/3: Anti-pattern example (cont.) An extension allows to pass

    ObserverScope as the receiver fun <T> ObserverScope.observe(observable: Observable<T>, action: (T) -> Unit) = observable.observe(this, action) What’s wrong with this code? class FooObserver(private val observerScope: ObserverScope) { init { ... observerScope.observe(observable) { /* do something */ } observerScope.observe(anotherObservable) { /* do another thing */ }
  52. Extension 3/3: Problem Non obvious prioritization of function calls -

    Example: A class inheriting both ObserverScope and Observable class BadClass: ObserverScope(), Observable<Int> { ... } fun function(badInstance1: BadClass, badInstance2: BadClass) { // As `Observable.observe` badInstance1.observe(badInstance2) { ... } // As `ObserverScope.observe` (badInstance1 as ObserverScope).observe(badInstance2) { ... }
  53. Extension 3/3: Root cause An extension is similar to an

    overloading method
 
 "Overloaded functions flipping the parameter order" is confusing fun <T> observe(scope: ObserverScope, observable: Observable<T>) { ... } fun <T> observe(observable: Observable<T>, scope: ObserverScope) { ... }
  54. Extension 3/3: Solution Options: 1. Remove extensions flipping the receiver

    and the parameter 2. Give different names for such extensions fun <T> ObserverScope.observe(observable: Observable<T>) { ... } fun <T> Observable<T>.observedDuring(scope: ObserverScope) { ... }
  55. Extension: Other anti-patterns With inheritance 
 (cf., https://kotlinlang.org/docs/extensions.html#extensions-are-resolved-statically)
 
 


    
 With down-casting for non-sealed class fun Base.getName() = "Base" fun Derived.getName() = "Derived" val base: Base = Derived() base.getName() // "Base" val RpcFrameworkException.errorCode: ErrorCode get() = when (this) { is FooException -> fooErrorCode is BarException -> barErrorCode else -> ErrorCode.UNKNOWN }
  56. Extension: Summary Extension behavior should look as natural as a

    normal function - For feature specific logic - For the receiver - For sub-typing
  57. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  58. Default argument Allows skipping arguments without boiler plates for overloading

    - cf. https://kotlinlang.org/docs/functions.html#default-arguments Introduce 2 anti-pattern examples
  59. Default argument 1/2: Anti-pattern example An extension filtering even/odd numbers

    What’s wrong with this code? fun List<Int>.filterParity(takesEven: Boolean = false): List<Int> { val expectedRemainder = if (takesEven) 0 else 1 return filter { it % 2 == expectedRemainder } } - true: Returns even elements - false: Returns odd elements
  60. Default argument 1/2: Problem The default value is not predictable

    on the caller side
 
 
 Some booleans might be predictable - View.isEnabled: true? - CheckBox.isChecked: false? - isEven: ??? listOf(1, 2, 3, 4).filterParity() // even or odd?
  61. Default argument 1/2: Solution Use well-known or predictable default values

    - 0, 1, MIN_VALUE, empty(), null, null object - "Initial value" can be a hint 
 Simply, remove default argument
  62. Default argument 2/2: Anti-pattern example Changing the logic depending on

    which parameter is given fun setBackground(colorInt: Int? = null, themeType: ThemeType? = null) { when { colorInt != null -> ... themeType != null -> ... } } What’s wrong with this code? view.setBackground(colorInt = Color.WHITE) view.setBackground(themeType = ThemeType.PRIMARY)
  63. Default argument 2/2: Problem The number of arguments might not

    be one - The behavior is not predictable // This may apply to both arguments. // Or, one of the arguments takes the priority. view.setColor(colorInt = Color.WHITE, themeType = ThemeType.PRIMARY) // This may do nothing, or may throw an exception. view.setColor()
  64. Default argument 2/2: Solution Options: 1. Split functions 2. Implement

    a function for type conversion 3. Define a sealed class of the parameter
  65. Default argument 2/2: Solution Options: 1. Split functions 2. Implement

    a function for type conversion 3. Define a sealed class of the parameter fun setBackgroundColor(colorInt: Int) { ... } fun setBackgroundTheme(themeType: ThemeType) { ... }
  66. Default argument 2/2: Solution Options: 1. Split functions 2. Implement

    a function for type conversion 3. Define a sealed class of the parameter fun setBackgroundColor(colorInt: Int) { ... } enum class ThemeType { ... fun toColorInt(): Int { ... } }
  67. Default argument 2/2: Solution Options: 1. Split functions 2. Implement

    a function for type conversion 3. Define a sealed class of the parameter fun setBackground(model: BackgroundModel) { ... } sealed class BackgroundModel { class Color(val colorInt: Int) : BackgroundModel() class Theme(val themeType: ThemeType) : BackgroundModel() }
  68. Default argument: Other anti-patterns With vararg
 
 
 
 


    
 Pass the child class constructor parameter fun getGeneralizedMean(vararg values: Double, exponent: Double = 1.0): Double = ... // Executed as getGeneralizedMean(2.0, 4.0, 1.0, exponent = 1.0) // instead of getGeneralizedMean(2.0, 4.0, exponent = 1.0) getGeneralizedMean(*doubleArrayOf(2.0, 4.0), 1.0) abstract class Layout(themeType: ThemeType = ThemeType.LIGHT) class FooLayout(themeType: ThemeType): Layout(themeType)
  69. Default argument: Summary Use default argument just for "default" -

    Default value must be predictable - Don't use for any other purpose - Consider to remove the default argument
  70. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class
  71. Sealed class A class cannot be overridden externally - cf.

    https://kotlinlang.org/docs/functions.html#default-arguments - Used to represent a sum data type, basically Introduce 2 anti-pattern examples
  72. Sealed class 1/2: Anti-pattern example A sealed class Color with

    child data classes: Rgb, Cmy, and Hsl sealed class Color { data class Rgb(val red: UByte, val green: UByte, val blue: UByte) : Color() data class Cmy(val cyan: UByte, val magenta: UByte, val yellow: UByte) : Color() data class Hsl(val hue: AngleInt, val saturation: PercentInt, ...) : Color() } What’s wrong with this code?
  73. Sealed class 1/2: Problem Ambiguity on equivalency val RED_1: Color

    = Color.Rgb(red = 255u, green = 0u, blue = 0u) val RED_2: Color = Color.Cmy(cyan = 0u, magenta = 255u, yellow = 255u) RED_1 == RED_2 // Can be false!
  74. Sealed class 1/2: Solution 1 Choose a "main" unit -

    Create constructors/factories to convert to the "main" unit data class Color(val red: UByte, val green: UByte, val blue: UByte) { companion object { fun fromCmy(cyan: UByte, magenta: UByte, yellow: UByte) : Color = Color((UByte.MAX_VALUE - cyan).toUByte(), ...) fun fromHsl(hue: AngleInt, ...) : Color = Color(...)
  75. Sealed class 1/2: Note of solution 1 Resolution and value

    domain may differ - Rounding error - HSL color has "hue" even for black or white val redHue = AngleInt.of(0) val blackColor = Color.fromHsl( hue = redHue, saturation = PercentInt.MIN, lightness = PercentInt.MIN ) blackColor.hue != redHue // May be true
  76. Sealed class 1/2: Solution 2 Implement equals function for every

    child class combination
 
 Can be too complex - Number of combinations: (n2) / 2 - hashCode must be implemented too
  77. Sealed class 1/2: Solution 3 Define as "representation" instead of

    "value" - Consider to use non-data class sealed class ColorRepresentation { class Rgb(val red: UByte, ...) : ColorRepresentation() class Cmy(val cyan: UByte, ...) : ColorRepresentation() class Hsl(val hue: AngleInt, ...) : ColorRepresentation() }
  78. Sealed class 2/2: Anti-pattern example Data model of "comment" for

    a specific location sealed class GeoLocationComment { } What's wrong with this code? class AtLatLon( val commentText: String, val latitude: Long, val longitude: Long ) : GeoLocationComment() class AtPlace( val commentText: String, val placeId: Long ) : GeoLocationComment()
  79. Sealed class 2/2: Problem Unnecessary downcast - Required to take

    a common property val commentText = when(locationComment) { is GeoLocationComment.AtLatLon -> locationComment.commentText is GeoLocationComment.AtPlace -> locationComment.commentText }
  80. Sealed class 2/2: Solution? Extract the common property to the

    parent sealed class GeoLocationComment { class AtLatLon( val commentText: String, val latitude: Long, val longitude: Long ) : GeoLocationComment() class AtPlace( val commentText: String, val placeId: Long ) : GeoLocationComment() }
  81. Sealed class 2/2: Solution? Extract the common property to the

    parent sealed class GeoLocationComment { class AtLatLon( commentText: String, val latitude: Long, val longitude: Long ) : GeoLocationComment() class AtPlace( commentText: String, val placeId: Long ) : GeoLocationComment() }
  82. Sealed class 2/2: Solution? Extract the common property to the

    parent sealed class GeoLocationComment(val commentText: String) { class AtLatLon( commentText: String, val latitude: Long, val longitude: Long ) : GeoLocationComment(commentText) class AtPlace( commentText: String, val placeId: Long ) : GeoLocationComment(commentText) }
  83. Sealed class 2/2: Solution? Extract the common property to the

    parent sealed class GeoLocationComment(val commentText: String) { class AtLatLon( commentText: String, val latitude: Long, val longitude: Long ) : GeoLocationComment(commentText) class AtPlace( commentText: String, val placeId: Long ) : GeoLocationComment(commentText) }
  84. Sealed class 2/2: Other problems 1. Need to update all

    the children to add a new property 2. open val is required to make a data class
  85. Sealed class 2/2: Other problems 1. Need to update all

    the children to add a new property 2. open val is required to make a data class sealed class GeoLocationComment( val commentText: String, val commentAuthorId: Long ) { class AtLatLon( commentText: String, commentAuthorId: Long, ... ) : GeoLocationComment(commentText, commentAuthorId) ...
  86. Sealed class 2/2: Other problems 1. Need to update all

    the children to add a new property 2. open val is required to make a data class sealed class GeoLocationComment( open val commentText: String, open val commentAuthorId: Long ) { data lass AtLatLon( override val commentText: String, override val commentAuthorId: Long, ... ) : GeoLocationComment(commentText, commentAuthorId) ...
  87. Sealed class 2/2: Solution Minimize the sealed class responsibility -

    Split the sealed class of location type from GeoLocationComment sealed class GeoLocation { class LatLon(val latitude: Long, val longitude: Long) : GeoLocation() class Place(val placeId: Long) : GeoLocation() } class GeoLocationComment( val commentText: String, val location: GeoLocation )
  88. Sealed class: Summary Keep sealed class responsibility minimized - Make

    unrelated properties accessible without downcast - Try to split if there is a "common" property
  89. Contents of this talk - Null safety - Scope function

    - Data class - Extension - Default argument - Sealed class Sealed class
  90. Aside 1. How to share the knowledge
 
 
 


    
 
 
 
 2. Non-Kotlin specific topic 3. Something is missing Watch a presentation at DroidKaigi 2021
 
 ௕͘ੜ͖Δίʔυϕʔεͷʮ඼࣭ʯ໰୊ʹ޲͖߹͏
 https://droidkaigi.jp/2021/timetable/276957/?day=2
  91. Aside 1. How to share the knowledge 2. Non-Kotlin specific

    topic
 
 
 
 
 
 
 
 3. Something is missing Presentation "code readability"ɻ
 https://speakerdeck.com/munetoshi/code-readability Book "ಡΈ΍͍͢ίʔυͷΨΠυϥΠϯ
 -࣋ଓՄೳͳιϑτ΢ΣΞ։ൃͷͨΊʹ"
 https://gihyo.jp/book/2022/978-4-297-13036-7
  92. Aside 1. How to share the knowledge 2. Non-Kotlin specific

    topic 3. Something is missing
 
 Coroutines? Listen to the next presentation!

  93. Summary Kotlin's language feature is convenient, but needs attentions -

    Make the objective clear - Don't mistake the means and the ends
 Introduced anti-patterns and the solutions - For null safety, scope functions, data classes, extensions...