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

My journey with Kotlin

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

My journey with Kotlin

Avatar for Alexis Seigneurin

Alexis Seigneurin

December 08, 2017
Tweet

More Decks by Alexis Seigneurin

Other Decks in Technology

Transcript

  1. My background • VB → Perl → C → C++

    → Java → C# → Java → Scala • Statically typed languages (except Perl) • Almost exclusively using Scala for 3 years
  2. Why I looked at Kotlin • Now contributing to a

    Java project • … and it hurts! → I want to write useful code (not generate code)
  3. POJOs • Getters/setters • Can intercept assignation/retrieval • No one

    does that in practice → Boilerplate code • Convention for naming getters/setters • Not enforced / may not be respected • Getting/setting values = method calls :-( • Not using ‘=‘ • Step-by-step debugging is unnecessarily complex • Have to write equals() / hashCode() • Not immutable
  4. POJOs on my project $ wc -l src/main/java/com/…/**/*.java | tail

    -n 1 2059 total $ wc -l src/main/java/com/…/dto/*.java | tail -n 1 485 total $ wc -l src/main/java/com/…/model/*.java | tail -n 1 105 total • → 590 / 2059 lines = 29% boilerplate code • Code reviews…
  5. No type inference (or so few) NewTopic newTopic = new

    NewTopic(topicName, topicParameters.getNumPartitions(), topicParameters.getReplicationFactor()); Duplicated type Method calls… just to get values Parsers no longer need semicolons (this is 2017)
  6. Streams API is verbose List<AclBinding> userAclBindings = aclBindings.stream() .filter(aclBinding ->

    aclBinding.entry().principal().equals(filterUser)) .collect(Collectors.toList()); Boilerplate Boilerplate
  7. One class per file • ClusterDto.java public class ClusterDto {

    private String clusterId; private NodeDto controller; private List<NodeDto> nodes; … • NodeDto.java public class NodeDto { private int id; private String host; private int port; private String rack; …
  8. Define a “function” @FunctionalInterface public interface KafkaAdminFunction<T> { T apply(AdminClient

    client) throws InterruptedException, ExecutionException; } Function = interface with one method!
  9. Java 9? • Jigsaw, new GC, HTTP 2… • Syntax

    improvements (lol) • try-with-resource with a pre-defined variable • diamond operator on anonymous inner classes • interface with private methods
  10. Scala • Extremely powerful • Can be overwhelming • Not

    acceptable in my team • Scala - Java interop isn’t the best • Requires a stop-the-world project migration
  11. Other JVM language? • Clojure → Too functional • JRuby,

    JPython… → Clunky • Groovy → Too dynamic • … • Kotlin → YES!
  12. Kotlin • JVM language - Initiated by JetBrains • Intent:

    • make the code concise + readable • make the code safer than pure Java • Made for 100% interoperability with Java • Migrate one class at a time • Statically typed • Compiles faster than Scala (troll)
  13. Maturity? • Tooling support • Build tools: Maven, Gradle •

    IDE: mature support in IntelliJ IDEA (who cares about Eclipse) • Official programming language on Android • Extended support for Kotlin in Spring 5 • (To write more idiomatic code)
  14. Interoperability • Migrate one class at a time • IntelliJ

    can convert a class / a package at once • Perfect interoperability • Call Java from Kotlin (seamless) • Call Kotlin from Java (seamless)
  15. Basic syntax • Defining a class + methods class KafkaDescribeClient(val

    adminClientFactory: KafkaAdminClientFactory) { fun describeCluster(region: String, cluster: String): ClusterDto { … } } Function name: Type Constructor with 1 parameter
  16. Basic syntax • Defining an immutable variable val region =

    "us-east-1" val cluster: String = "engine" val adminClient = KafkaDescribeClient(adminClientFactory) • Defining a variable (you don’t really need that) var something = 12 something = 42 No `new` keyword
  17. Basic syntax • No `static` methods • Define a function

    in a file (in a package, no class) fun nodeToDto(node: Node): NodeDto { return NodeDto(node.id(), node.host(), node.port(), node.rack()) } • Import the functions of the class import com.….dto.* • Call the function nodeToDto(controller)
  18. Basic syntax • Reference equality === !== • Structural equality

    == → equals() Note that the `==` operator in Kotlin code is translated into a call to [equals] when objects on both sides of the operator are not null. • … meaning it’s safe, and `o == null` still works
  19. Strings • String comparison: use ‘==’ if(port == "9092") {

    • String interpolation private fun buildUri(securityProtocol: String, instanceName: String, port: String): String { return "${securityProtocol}://${instanceName}${domain}:${port}" } • Multi-line strings System.err.println(""" ========== Data checking ========== Purpose: checks data quality of messages in a topic. - Kafka brokers = ${config.kafkaBrokers} - Kafka group ID = ${config.applicationId} ... """)
  20. Data classes • Data class = (immutable) POJO data class

    AclParameters( val operation: AclOperation, val user: String, val hostname: String?, val groupId: String?) • Create an instance (no `new`) val aclParams = AclParameters(AclOperation.READ, "u1", "h1", “g1") • equals() / hashCode() / toString() → generated • From Java = POJO with a constructor + getters • For Jackson: there’s a module
  21. Data classes • Immutable → copy if you need to

    update val aclParamsCopy = aclParams.copy(groupId = groupName) • Destructuring val (operation, user, hostname, groupId) = aclParams • Partially destructuring val (operation, _, hostname, _) = aclParams
  22. Expressions • Evaluating an expression and assigning a variable val

    res = if (operation == CREATE) { createAcl(aclParameters) } else if (operation == ALTER) { alterAcl(aclParameters) } else if (operation == DELETE) { deleteAcl(aclParameters) } else { throw RuntimeException("Operation not supported: ${operation}") }
  23. Streams • Remember this piece of Java code? List<AclBinding> userAclBindings

    = aclBindings.stream() .filter(aclBinding -> aclBinding.entry().principal().equals(filterUser)) .collect(Collectors.toList()); • The same in Kotlin val userAclBindings = aclBindings.filter { it.entry().principal() == filterUser } Type inference No stream(), no Collector Implicit argument Replaces equals()
  24. Streams • Java private List<String> buildKafkaUris(List<Instance> instances, String securityProtocol, String

    port) { return instances.stream() .map(i -> getTagOrFail(i, TAG_KEY_NAME)) .map(name -> buildUri(securityProtocol, name, port)) .collect(Collectors.toList()); } • Kotlin private fun buildKafkaUris(instances: List<Instance>, securityProtocol: String, port: String): List<String> { return instances .map { getTag(it, TAG_KEY_NAME) } .map { buildUri(securityProtocol, it, port) } } Null safety through types (see further)
  25. Null safety • Kotlin tries to eliminate NullPointerExceptions • Suffix

    the type with ‘?’ to declare a nullable var/param var s: String? = null • Need to test for nulls before being able to use the value return s.length Error:(37, 17) Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? if (s !=null) { return s.length }
  26. Null safety • Can’t return a potentially null value if

    not declared as nullable private fun getTag(instance: Instance, tagKey: String): String { val tag = instance.tags .filter { it.key.equals(tagKey, ignoreCase = true) } .map { it.value } .firstOrNull() return tag } Error:(40, 16) Kotlin: Type mismatch: inferred type is String? but String was expected • Safe: private fun getTag(instance: Instance, tagKey: String): String { val tag = instance.tags .filter { it.key.equals(tagKey, ignoreCase = true) } .map { it.value } .firstOrNull() if (tag == null) { throw RuntimeException("Tag [${tagKey}] not found") } return tag } • (Or just call first() in this case) tag is of type String? tag is of type String
  27. Null safety • Elvis operator val hostname = aclParameters.hostname ?:

    “*” • Safe calls val length: Int? = s?.length Return value is a nullable type
  28. Functions • First class support for functions protected fun <R>

    withAdminClient(func: (AdminClient) -> R): R { val adminClient = adminClientFactory.createAdminClient() return adminClient.use { func(adminClient) } } A function with one param of type AdminClient Call it… as a function (Bye bye functional interfaces)
  29. Type alias • Can alias any type typealias Clusters =

    List<Cluster> • Example typealias KakfaAdminFunc<R> = (AdminClient) -> R protected fun <R> withAdminClient(func: KakfaAdminFunc<R>): R { …
  30. Extension functions • Extend existing classes with new methods •

    Resolved statically / locally • Example fun AmazonEC2.findInstances(request: DescribeInstancesRequest): ArrayList<Instance> { val allInstances = ArrayList<Instance>() var token: String? do { val result = this.describeInstances(request) val instances = result.reservations.flatMap { it.instances } allInstances.addAll(instances) token = result.nextToken request.nextToken = token } while (token != null) return allInstances } ec2.findInstances(request) Class to extend Method
  31. Conclusion • Easy to use & intuitive • Greater expressivity

    than Java • Simpler than Scala • Very positive experience so far