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

Writing a library in Kotlin by Vincent Carrier

Writing a library in Kotlin by Vincent Carrier

1b77dd441f657f5aefb3e21283b252e6?s=128

GDG Montreal

May 08, 2018
Tweet

Transcript

  1. Writing a library in Kotlin by Vincent Carrier

  2. A year ago today

  3. Last talk

  4. The problem with grids val chessboard = arrayOf( arrayOf(ChessPiece(BLACK, ROOK),

    ChessPiece(BLACK, KNIGHT), …), … ) Verbose, hardcoded implementation, hard to visualize
  5. So I made a library val chessboard = """ |rnbqkbnr

    |pppppppp |________ |________ |________ |________ |PPPPPPPP |RNBQKBNR """.toMutableGrid { char -> char.toChessPiece() }
  6. So I made a library val e2 = Cell(4, 6)

    val e4 = Cell(4, 4) chessboard.swap(e2, e4) assertEquals( ChessPiece(WHITE, PAWN), chessboard[e4] ) // True
  7. Structure of this talk 1. Maintainability 2. Readability 3. Performance

    4. Benchmarking with JMH 5. Publishing on jCenter
  8. Maintainability

  9. Class hierarchy interface Grid<out E> interface MutableGrid<E> abstract class BaseGrid<out

    E> open class ListGrid<out E> open class MutableListGrid<E> open class ArrayGrid<out E> open class MutableArrayGrid<E>
  10. Consider specifying types explicitly • Type inference is great, but

    it forces you to use concrete types and can lead to confusing bugs when refactoring. Use with care! • Be sure to enable type hints within Android Studio : Preferences -> Editor -> General -> Appearance -> Show parameter type hints -> Configure -> Language -> Kotlin
  11. Be aware of the Kotlin’s modifiers • Kotlin classes and

    members are public by default • Everything is implicitly final except protected / interface members / override • internal stands for module-private visibility • private is either class-private or file-private for top-level members
  12. Consider extension functions instead of default interface method implementation or

    static class utilities interface Grid<out E> { … fun cellsAdjacentTo(x: Int, y: Int): List<Cell> { … } } BAD: Can be overridden
  13. Consider extension functions instead of default interface method implementation or

    static class utilities object Grids { fun <E> adjacent(x: Int, y: Int, grid: Grid<E>): List<Cell> { … } } BAD: Ugly and hidden in an utility class
  14. Consider extension functions instead of default interface method implementation or

    static class utilities interface Grid<out E> { … } fun <E> Grid<E>.cellsAdjacentTo(x: Int, y: Int): List<Cell> { … }
  15. 1. Superclass constructor 2. Superclass final properties initialization 3. Superclass

    init {…} 4. Subclass constructor 5. Subclass properties initialization 6. Subclass init {…} Avoid using open members in the constructors, property initializers, and init blocks in a base class
  16. Avoid using open members in the constructors, property initializers, and

    init blocks in a base class abstract class BaseGrid<out E> : Grid<E> { final override val rows = 0 until height final override val columns = 0 until width … } BAD: Accessing non-final properties in constructor
  17. abstract class BaseGrid<out E> : Grid<E> { final override val

    rows by lazy { 0 until height } final override val columns by lazy { 0 until width } … } Avoid using open members in the constructors, property initializers, and init blocks in a base class
  18. Use the inline and reified keywords when tackling type erasure

    Reified Erased Array<E> List<E> Runtime type safety Compile-time type safety Since Java 1 Since Java 5 reify | ˈriːɪfʌɪ, ˈreɪɪfʌɪ | verb (reifies, reifying, reified) [with object] formal make (something abstract) more concrete or real: these instincts are, in man, reified as verbal constructs.
  19. Use the inline and reified keywords when tackling type erasure

    fun <E> String.toMutableArrayGrid(toValue: (Char) -> E) : MutableArrayGrid<E> { val s = trimMargin().filter { it != '\n' } val array = Array(s.length) { i -> toValue(s[i]) } val width = trimMargin().lines().first().length return MutableArrayGrid(array, width) } Cannot use ‘E’ as reified type parameter. Use a class instead.
  20. Use the inline and reified keywords when tackling type erasure

    inline fun <reified E> String.toMutableArrayGrid(toValue: (Char) -> E) : MutableArrayGrid<E> { val s = trimMargin().filter { it != '\n' } val array = Array(s.length) { i -> toValue(s[i]) } val width = trimMargin().lines().first().length return MutableArrayGrid(array, width) }
  21. When using type parameters, think of T? as Optional<T> open

    class ArrayGrid<E>( protected val array: Array<E?>, final override val width: Int) : BaseGrid<E?>() open class ArrayGrid<E>( protected val array: Array<E>, final override val width: Int) : BaseGrid<E>() BAD: Only supports nullable types
  22. Use those scratch files

  23. Readability

  24. Always code as if the guy who ends up maintaining

    your code will be a violent psychopath who knows where you live. - John Woods
  25. Consider using the language features to replace Java design patterns

    when possible • by keyword for Decorator pattern (i.e. class delegation) • object for Singleton pattern • Higher-order functions for Strategy pattern • Delegated properties for simple Observer pattern • sealed class for State pattern • Type-safe builder / default arguments for Builder pattern https://github.com/dbacinski/Design-Patterns-In-Kotlin
  26. Use check() and require()instead of if (…) throw {…} when

    appropriate • require(…) throws IllegalArgumentException • check(…) throws IllegalStateException • Use assert(…) for internal or very expensive sanity checks. Won’t be compiled unless the -ea (enable assertions) flag is passed.
  27. open class ArrayGrid<E>(protected val array: Array<E>, final override val width:

    Int) : BaseGrid<E>() { init { require(array.size % width == 0) { "Grid must be a rectangle but array of size ${array.size} " + "wasn't divisible by width $width” // Lazy-evaluated } } … } Use check() and require()instead of if (…) throw {…} when appropriate
  28. Consider implementing operator functions operator fun <E> Grid<E>.get(c: Cell): E

    { get(c.x, c.y) } Usage: val e4 = Cell(4, 4) chessboard[e4]
  29. Consider implementing operator functions operator fun <E> Grid<E>.component1() = width

    operator fun <E> Grid<E>.component2() = height Usage: val (w, h) = chessboard // val w = chessboard.width; val h = chessboard.height
  30. Sort the contents of a class logically https://kotlinlang.org/docs/reference/coding-conventions.html “Do not

    sort the method declarations alphabetically or by visibility, and do not separate regular methods from extension methods. Instead, put related stuff together, so that someone reading the class from top to bottom would be able to follow the logic of what's happening. Choose an order (either higher-level stuff first, or vice versa) and stick to it.”
  31. Keep it simple, stupid • Consider grouping related elements (like

    a type and its extensions) into a single file, as long as the file stays short enough. • If you can’t find a good name for a variable, consider inlining it. Use let {…}, apply {…}, also {…}, run {…} or with(…) if need be.
  32. Performance

  33. Don’t optimize yet (but plan for it) • Premature optimization

    is the root of all evil • Once your core structure is in place and your library is public, it’s too late to change it without introducing breaking changes for its users.
  34. Consider performance limitations • Lambdas / Higher-order functions involve creation

    of a Function object, along with primitive boxing (unlike Java 8). Use inline keyword when appropriate. Be wary of local functions as they can’t be inlined. • companion object generate a lot of extra methods • Auto-generated null checks (can be disabled with ProGuard) • lazy properties are synchronized by default • Prefer for-loops over forEach {…} https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
  35. Benchmarking with JMH Why Are Java Microbenchmarks Hard? Writing benchmarks

    that correctly measure the performance of a small part of a larger application is hard. There are many optimizations that the JVM or underlying hardware may apply to your component when the benchmark executes that component in isolation. These optimizations may not be possible to apply when the component is running as part of a larger application. Badly implemented microbenchmarks may thus make you believe that your component's performance is better than it will be in reality. http://tutorials.jenkov.com/java-performance/jmh.html
  36. Benchmarking with JMH

  37. Benchmarking with JMH

  38. Benchmarking with JMH

  39. Benchmarking with JMH open class MutableArrayGridBenchmark { @State(Thread) open class

    MyState { val grid = """ |abc |def |ghi """.toMutableArrayGrid { it } } @Benchmark fun swap(state: MyState) { state.grid.swap(0, 0, 1, 1) } }
  40. Benchmarking with JMH org.openjdk.jmh.infra.Blackhole 
 
 public class Blackhole extends

    Object 
 Black hole "consumes" the values, conceiving no information to JIT whether the value is actually used afterwards. This can save from the dead-code elimination of the computations resulting in the given values.

  41. Benchmarking with JMH # Run complete. Total time: 00:22:30 Benchmark

    Mode Cnt Score Error Units CharGridBenchmark.swap thrpt 200 362148372.636 ± 3391525.749 ops/s MutableArrayGridBenchmark.swap thrpt 200 228289634.510 ± 941099.976 ops/s MutableListGridBenchmark.swap thrpt 200 132345179.610 ± 1009663.525 ops/s kgrid-benchmark $ java -jar target/benchmarks.jar CharArray > Array<Char> > MutableList<MutableList<Char>>
  42. Publishing to jCenter

  43. None
  44. None
  45. None
  46. A word of warning I’m not a Gradle/Maven genius, I

    just copy-paste things from the Internet until they work (i.e. StackOverflow- Driven Development) THIS IS GRADLE. IT’S A BUILD SYSTEM FOR THE JVM THAT WORKS THROUGH A BEAUTIFUL DIRECTED ACYCLIC GRAPH MODEL.
  47. Publishing to jCenter apply plugin: 'maven' apply plugin: ‘maven-publish’ apply

    plugin: 'com.jfrog.bintray' group = 'com.vincentcarrier' version = '1.0.1'
  48. Publishing to jCenter publishing { publications { DefaultPublication(MavenPublication) { from

    components.java groupId group artifactId name version this.version } } }
  49. Publishing to jCenter bintray { user = 'vincent-carrier' key =

    System.getenv('BINTRAY_KEY') configurations = ['archives'] publish = true pkg { repo = 'maven' name = 'kgrid' version { name = this.version released = new Date() } } }
  50. None
  51. None
  52. Thank you for listening!