My journey with Kotlin

My journey with Kotlin

B1ed299a884f153fd23b9a1b81b798ac?s=128

Alexis Seigneurin

December 08, 2017
Tweet

Transcript

  1. (My journey with) Kotlin Alexis Seigneurin

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

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

    Java project • … and it hurts! → I want to write useful code (not generate code)
  4. What hurts (me) in Java

  5. 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
  6. 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…
  7. 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)
  8. Streams API is verbose List<AclBinding> userAclBindings = aclBindings.stream() .filter(aclBinding ->

    aclBinding.entry().principal().equals(filterUser)) .collect(Collectors.toList()); Boilerplate Boilerplate
  9. No operator overloading List<AclBinding> userAclBindings = aclBindings.stream() .filter(aclBinding -> aclBinding.entry().principal().equals(filterUser))

    .collect(Collectors.toList()); how about? == for object equality === for reference equality
  10. No string interpolation MessageFormat.format("{0}://{1}{2}:{3}", securityProtocol, instanceName, domain, port).toString() new RuntimeException("Tag

    [" + tagKey + "] not found") (No multiline strings either)
  11. 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; …
  12. Define a “function” @FunctionalInterface public interface KafkaAdminFunction<T> { T apply(AdminClient

    client) throws InterruptedException, ExecutionException; } Function = interface with one method!
  13. What are our options?

  14. 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
  15. Java 10? • Value types • Reified generics • …

  16. 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
  17. Non-JVM language? • Language change + ecosystem change → Too

    disruptive
  18. Other JVM language? • Clojure → Too functional • JRuby,

    JPython… → Clunky • Groovy → Too dynamic • … • Kotlin → YES!
  19. Kotlin

  20. 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)
  21. 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)
  22. 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)
  23. Java code Kotlin code

  24. 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
  25. 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
  26. 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)
  27. 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
  28. 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} ... """)
  29. 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
  30. 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
  31. 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}") }
  32. 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()
  33. 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)
  34. 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 }
  35. 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
  36. Null safety • Elvis operator val hostname = aclParameters.hostname ?:

    “*” • Safe calls val length: Int? = s?.length Return value is a nullable type
  37. 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)
  38. Type alias • Can alias any type typealias Clusters =

    List<Cluster> • Example typealias KakfaAdminFunc<R> = (AdminClient) -> R protected fun <R> withAdminClient(func: KakfaAdminFunc<R>): R { …
  39. 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
  40. Wrapping up

  41. Conclusion • Easy to use & intuitive • Greater expressivity

    than Java • Simpler than Scala • Very positive experience so far
  42. Try it out! @aseigneurin