Idiomatic Kotlin

646 views

Published on

"Idiomatic Kotlin" talk from Kotlin Night Berlin, July 6th, 2017

Published in: Software
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
646
On SlideShare
0
From Embeds
0
Number of Embeds
104
Actions
Shares
0
Downloads
5
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Idiomatic Kotlin

  1. 1. Idiomatic Kotlin Dmitry Jemerov <yole@jetbrains.com>
  2. 2. Expressions
  3. 3. Use ‘when’ as expression body fun parseNum(number: String): Int? { when (number) { "one" -> return 1 "two" -> return 2 else -> return null } }
  4. 4. Use ‘when’ as expression body fun parseNum(number: String): Int? { when (number) { "one" -> return 1 "two" -> return 2 else -> return null } } fun parseNum(number: String) = when (number) { "one" -> 1 "two" -> 2 else -> null }
  5. 5. Use ‘try’ as expression body fun tryParse(number: String): Int? { try { return Integer.parseInt(number) } catch (e: NumberFormatException) { return null } }
  6. 6. Use ‘try’ as expression body fun tryParse(number: String): Int? { try { return Integer.parseInt(number) } catch (e: NumberFormatException) { return null } } fun tryParse(number: String) = try { Integer.parseInt(number) } catch (e: NumberFormatException) { null }
  7. 7. Use ‘try’ as expression fun tryParse(number: String): Int? { try { return Integer.parseInt(number) } catch (e: NumberFormatException) { return null } } fun tryParse(number: String): Int? { val n = try { Integer.parseInt(number) } catch (e: NumberFormatException) { null } println(n) return n }
  8. 8. Use elvis with ‘return’ and ‘throw’ fun processPerson(person: Person) { val name = person.name if (name == null) throw IllegalArgumentException(
 "Named required") val age = person.age if (age == null) return println("$name: $age") } class Person(val name: String?, val age: Int?)
  9. 9. Use elvis with ‘return’ and ‘throw’ fun processPerson(person: Person) { val name = person.name if (name == null) throw IllegalArgumentException(
 "Named required") val age = person.age if (age == null) return println("$name: $age") } fun processPerson(person: Person) { val name = person.name ?: throw IllegalArgumentException( "Name required") val age = person.age ?: return println("$name: $age") } class Person(val name: String?, val age: Int?)
  10. 10. Use range checks instead of comparison pairs fun isLatinUppercase(c: Char) = c >= 'A' && c <= 'Z'
  11. 11. Use range checks instead of comparison pairs fun isLatinUppercase(c: Char) = c >= 'A' && c <= 'Z' fun isLatinUppercase(c: Char) = c in 'A'..'Z'
  12. 12. Classes and Functions
  13. 13. Don’t create classes just to put functions in class StringUtils { companion object { fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } } }
  14. 14. Don’t create classes just to put functions in class StringUtils { companion object { fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } } } object StringUtils { fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } }
  15. 15. Don’t create classes just to put functions in class StringUtils { companion object { fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } } } fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } object StringUtils { fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } }
  16. 16. Use extension functions copiously fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() }
  17. 17. Use extension functions copiously fun isPhoneNumber(s: String) = s.length == 7 && s.all { it.isDigit() } fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }
  18. 18. Avoid using member extension functions 
 (unless required for DSLs) class PhoneBook { fun String.isPhoneNumber() = length == 7 && all { it.isDigit() } }
  19. 19. Avoid using member extension functions 
 (unless required for DSLs) class PhoneBook { fun String.isPhoneNumber() = length == 7 && all { it.isDigit() } } class PhoneBook { } private fun String.isPhoneNumber() = length == 7 && all { it.isDigit() }
  20. 20. Don't use member extensions with 
 containing class as the receiver class PhoneBook { fun PhoneBook.find(name: String)= "1234567" }
  21. 21. Don't use member extensions with 
 containing class as the receiver class PhoneBook { fun PhoneBook.find(name: String)= "1234567" } class PhoneBook { fun find(name: String) = "1234567" }
  22. 22. Consider extracting non-essential API
 of classes into extensions class Person(val firstName: String, val lastName: String) { val fullName: String get() = "$firstName $lastName" }
  23. 23. Consider extracting non-essential API
 of classes into extensions class Person(val firstName: String, val lastName: String) { val fullName: String get() = "$firstName $lastName" } class Person(val firstName: String, val lastName: String) val Person.fullName: String get() = "$firstName $lastName"
  24. 24. Use default parameter values instead of overloads wherever possible class Phonebook { fun print() { print(",") } fun print(separator: String) { } } fun main(args: Array<String>) { Phonebook().print("|") }
  25. 25. Use default parameter values instead of overloads wherever possible class Phonebook { fun print() { print(",") } fun print(separator: String) { } } fun main(args: Array<String>) { Phonebook().print("|") } class Phonebook { fun print(
 separator: String = ",") { } } fun main(args: Array<String>) { Phonebook().print( separator = "|") }
  26. 26. Use ‘lateinit’ for properties that can’t be initialised in a constructor class MyTest { class State(val data: String) var state: State? = null @Before fun setup() { state = State("abc") } @Test fun foo() { Assert.assertEquals(
 "abc", state!!.data) } }
  27. 27. Use ‘lateinit’ for properties that can’t be initialised in a constructor class MyTest { class State(val data: String) var state: State? = null @Before fun setup() { state = State("abc") } @Test fun foo() { Assert.assertEquals(
 "abc", state!!.data) } } class MyTest { class State(val data: String) lateinit var state: State @Before fun setup() { state = State("abc") } @Test fun foo() { Assert.assertEquals(
 "abc", state.data) } }
  28. 28. Use type aliases for functional types and collections class EventDispatcher { fun addClickHandler(
 handler: (Event) -> Unit) { } fun removeClickHandler( handler: (Event) -> Unit) { } }
  29. 29. Use type aliases for functional types and collections class EventDispatcher { fun addClickHandler(
 handler: (Event) -> Unit) { } fun removeClickHandler( handler: (Event) -> Unit) { } } typealias ClickHandler = (Event) -> Unit class EventDispatcher { fun addClickHandler( handler: ClickHandler) { } fun removeClickHandler( handler: ClickHandler) { } }
  30. 30. Use type aliases for functional types and collections class EventDispatcher { fun addClickHandler(
 handler: (Event) -> Unit) { } fun removeClickHandler( handler: (Event) -> Unit) { } } typealias ClickHandler = (Event) -> Unit typealias HandlerMap = Map<EventType, List<Event>>
  31. 31. Use data classes to return multiple values fun namedNum(): Pair<Int, String> = 1 to "one" fun main(args: Array<String>) { val pair = namedNum() val number = pair.first val name = pair.second }
  32. 32. Use data classes to return multiple values fun namedNum(): Pair<Int, String> = 1 to "one" fun main(args: Array<String>) { val pair = namedNum() val number = pair.first val name = pair.second } data class NamedNumber( val number: Int, val name: String) fun namedNum() = NamedNumber(1, "one") fun main(args: Array<String>) { val (number, name) =
 namedNum() }
  33. 33. Use destructuring in loops fun printMap(m: Map<String, String>) { for (e in m.entries) { println("${e.key} -> ${e.value}") } }
  34. 34. Use destructuring in loops fun printMap(m: Map<String, String>) { for (e in m.entries) { println("${e.key} -> ${e.value}") } } fun printMap(m: Map<String, String>) { for ((key, value) in m) { println("$key -> $value") } }
  35. 35. Use destructuring with lists fun splitNameExt(fn: String): NameExt { if ('.' in fn) { val parts = fn.split('.', limit = 2) return NameExt(parts[0], parts[1]) } return NameExt(fn, null) } data class NameExt(val name: String, val ext: String?)
  36. 36. Use destructuring with lists fun splitNameExt(fn: String): NameExt { if ('.' in fn) { val parts = fn.split('.', limit = 2) return NameExt(parts[0], parts[1]) } return NameExt(fn, null) } fun splitNameExt(fn: String): NameExt { if ('.' in fn) { val (name, ext) = fn.split('.', limit = 2) return NameExt(name, ext) } return NameExt(fn, null) } data class NameExt(val name: String, val ext: String?)
  37. 37. Use ‘copy’ method for data classes class Person(val name: String, var age: Int) fun happyBirthday(person: Person) { person.age++ }
  38. 38. Use ‘copy’ method for data classes class Person(val name: String, var age: Int) fun happyBirthday(person: Person) { person.age++ } data class Person(val name: String, val age: Int) fun happyBirthday(person: Person) = person.copy( age = person.age + 1)
  39. 39. The Standard Library
  40. 40. Use ‘coerceIn’ to ensure a number is in range fun updateProgress(value: Int) { val newValue = when { value < 0 -> 0 value > 100 -> 100 else -> value } }
  41. 41. Use ‘coerceIn’ to ensure a number is in range fun updateProgress(value: Int) { val newValue = when { value < 0 -> 0 value > 100 -> 100 else -> value } } fun updateProgress(value: Int) { val newValue = 
 value.coerceIn(0, 100) }
  42. 42. Use ‘apply’ for object initialisation fun createLabel(): JLabel { val label = JLabel("Foo") label.foreground = Color.RED label.background = Color.BLUE return label }
  43. 43. Use ‘apply’ for object initialisation fun createLabel(): JLabel { val label = JLabel("Foo") label.foreground = Color.RED label.background = Color.BLUE return label } fun createLabel() = JLabel("Foo").apply { foreground = Color.RED background = Color.BLUE }
  44. 44. Use ‘filterIsInstance’ to filter a list by object type fun findStrings(objs: List<Any>) = objs.filter { it is String }
  45. 45. Use ‘filterIsInstance’ to filter a list by object type fun findStrings(objs: List<Any>) = objs.filter { it is String } fun findStrings(objs: List<Any>) = obs.filterIsInstance<String>()
  46. 46. Use ‘mapNotNull’ to apply a function and select items for which it returns a non-null value fun listErrors(
 results: List<Result>) = results .map { it.error } .filterNotNull() data class Result(val data: Any?, val error: String?)
  47. 47. Use ‘mapNotNull’ to apply a function and select items for which it returns a non-null value fun listErrors(
 results: List<Result>) = results .map { it.error } .filterNotNull() fun listErrors( results: List<Result>) = results.mapNotNull { 
 it.error } data class Result(val data: Any?, val error: String?)
  48. 48. Use ‘compareBy’ for multi-step comparisons fun sortPersons(persons: List<Person>) = persons.sortedWith( Comparator<Person> { p1, p2 -> val rc = p1.name.compareTo(p2.name) if (rc != 0) rc else p1.age - p2.age }) class Person(val name: String, val age: Int)
  49. 49. Use ‘compareBy’ for multi-step comparisons fun sortPersons(persons: List<Person>) = persons.sortedWith( Comparator<Person> { p1, p2 -> val rc = p1.name.compareTo(p2.name) if (rc != 0) rc else p1.age - p2.age }) fun sortPersons(persons: List<Person>) = persons.sortedWith( compareBy(Person::name, Person::age)) class Person(val name: String, val age: Int)
  50. 50. Use ‘groupBy’ to group items in a collection fun analyzeLog(log: List<Request>) { val map = mutableMapOf<String, MutableList<Request>>() for (request in log) { map.getOrPut(request.url) { mutableListOf() } .add(request) } } class Request(val url: String, val remoteIP: String, val timestamp: Long)
  51. 51. Use ‘groupBy’ to group items in a collection fun analyzeLog(log: List<Request>) { val map = mutableMapOf<String, MutableList<Request>>() for (request in log) { map.getOrPut(request.url) { mutableListOf() } .add(request) } } fun analyzeLog(log: List<Request>) { val map = log.groupBy(Request::url) } class Request(val url: String, val remoteIP: String, val timestamp: Long)
  52. 52. Use String methods for string parsing val pattern = Regex("(.+)/([^/]*)") fun splitPath(path: String): PathParts { val match = pattern.matchEntire(path) ?: return PathParts("", path) return PathParts(match.groupValues[1], match.groupValues[2]) } data class PathParts(val dir: String, val name: String)
  53. 53. Use String methods for string parsing val pattern = Regex("(.+)/([^/]*)") fun splitPath(path: String): PathParts { val match = pattern.matchEntire(path) ?: return PathParts("", path) return PathParts(match.groupValues[1], match.groupValues[2]) } fun splitPath(path: String) = PathParts( path.substringBeforeLast('/', ""), path.substringAfterLast(‘/')) data class PathParts(val dir: String, val name: String)
  54. 54. Q&A yole@jetbrains.com @intelliyole

×