Мои первые впечатления от языка Kotlin

25 февраля 2015

В мире JVM уже давно предпринимаются попытки заместить Java чем-то более пристойным. Наиболее успешной такой попыткой, по всей видимости, на сегодняшний день является Scala. Тут вам и сообщество программистов, и куча фреймворков, и вакансии — все что угодно. Но и Scala далека от идеала. Среди наиболее существенных недостатков языка можно отметить его относительную сложность (что признает даже Одерски) и, что намного важнее, медленную скорость компиляции, а также требовательность к ресурсам во время этой компиляции. Поэтому такие языки под JVM, как Kotlin, Gosu и Ceylon все еще представляют собой интерес.

Когда мне захотелось ознакомиться с одним из этих языков, Ceylon отпугнул меня своим синтаксисом. Gosu выглядел куда более приятно. Но в примечаниях к последнему релизу языка среди прочего говорилось об устаревании оператора неравенства <> и что дескать вместо него теперь нужно использовать !=. С такой нестабильностью я мириться не готов. А вот Kotlin и порадовал синтаксисом и произвел впечатление стабильного языка. К тому же, его разрабатывают наши соотечественники из JetBrains, и, понятное дело, полноценная поддержка со стороны IntelliJ IDEA также уже имеется. На нем я и решил остановиться.

Прочитав 130 страниц полного описания языка я сделал следующие выводы:

  • Язык очень похож на Scala, только проще и быстро компилируется. В частности, скорость компиляции не медленнее, чем в Java, разработчики языка ставят одной из своих главных целей. Из наиболее существенных отличий от Scala следует отметить полное отсутствие каких-либо имплиситов. Однако прикручивать методы к уже существующим final классам можно, как и в Scala.
  • Язык компилируется не только под JVM, но и в JavaScript. Это хорошо, можно писать UI и бэкенд на одном языке.
  • Все основные элементы ФП присутствуют — замыкания, хвостовая рекурсия, REPL, функции высших порядков, map / reduce / filter, автоматический вывод типов, паттерн матчинг, АТД (кстати, здесь case классы называются data классами) и так далее. Каррирования, как в Scala, похоже нет, но если функция принимает последним аргументом другую функцию, можно использовать синтаксис в стиле lock(lock) { body }, что решает ту же самую проблему.
  • Коллекции бывают неизменяемые (List, Set, Map) и изменяемые (MutableList, MutableMap, …), то же самое с переменными (val, var).
  • В языке есть немало интересных решений. Например, меня удивили делегаты, через которые, помимо прочего, сделаны ленивые вычисления. Еще есть такая фишка под названием smart casts, благодаря которой код if(x is String) { print(x.length) } компилируется и тайпчекается без дополнительного приведения переменной к типу String. Немного напоминает Rust.
  • Вместо Option/Maybe в языке используются nullable типы. С одной стороны, они дают такие же статические гарантии отсутствия NPE, с другой — более эффективны, чем Option, так как работают поверх все тех же null’ов, то есть, не приводят к использованию дополнительных классов. Что опять-таки напоминает Rust.
  • В Kotlin также есть ковариация и контравариация, но благодаря использованию ключевых слов in и out работа с ними становится намного проще и понятнее. Если тип помечен как in, методы класса могут принимать его на вход (консьюмер), если же out — методы возвращают тип (продьюсер). И не нужно помнить о том, что in — это контравариация, а out — ковариация, все просто и понятно.
  • Вообще, язык выглядит очень продуманно. Поля класса по умолчанию не являются public, при выводе data-классов перед значениями полей выводятся их имена, функции можно объявлять прямо в пакете без всяких дополнительных package object’ов и так далее. Всякие такие мелочи внушают доверие.

Чтобы попрактиковаться в программировании на Kotlin, я решил написать на нем одну программку, которую в свое время я уже писал на Scala, а также на Rust. Программа эта принимает текстовый файл, выдирает из него все URL, и выдает на выходе HTML-код со списком этих ссылок, используя в качестве тестов ссылок title соответствующих страниц. Вот что у меня получилось:

package me.eax.examples.shownotegen

import org.apache.http.client.methods.*
import org.apache.http.impl.client.*
import java.util.regex.*
import java.io.*

fun defaultUserAgent() =
  """Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, """ +
  """like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60"""

fun getTitle(url: String): String {
  val req = HttpGet(url)
  req.setHeader("User-Agent", defaultUserAgent())
  try {
    HttpClients.createDefault() use {
      it.execute(req) use { resp ->
        val entity = resp.getEntity()
        val charset = run {
          val contentType = entity.getContentType().getValue() ?: ""
          val pattern = Pattern.compile("charset=(.*)")
          val matcher = pattern.matcher(contentType)
          if (matcher.find()) matcher.group(1) else "UTF-8"
        }
        val title = run {
          val body = entity.getContent().readBytes().toString(charset)
          val pttrn = Pattern.compile("""(?is)<title>(.*?)</title>""")
          val matcher = pttrn.matcher(body)
          matcher.find()
          matcher.group(1)
        }
        return title.replaceAll("""\s+""", " ").trim()
      }

    }
  } catch(e: Exception) {
    return "[NO TITLE: ${e.getMessage()}]"
  }
}

fun processFile(fileName: String) {
  val data = File(fileName).readText(Charsets.UTF_8)
  val matcher = Pattern.compile("""https?://\S+""").matcher(data)
  println("<ul>")
  while(matcher.find()) {
    val url = matcher.group()
    println("<li><a href=\"$url\">${getTitle(url)}</a></li>")
  }
  println("</ul>")
}

fun printUsage() {
  val executableName = System.getProperty("sun.java.command")
  println("Usage: $executableName input.txt")
  System.exit(1)
}

fun main(args : Array<String>) {
  when(args.size()) {
    0 -> printUsage()
    else -> processFile(args[0])
  }
}

Обратите внимание на использование метода use. В Kotlin он используется вместо try with resoruces в Java. Как и Scala, Kotlin позволяет вызывать методы при помощи инфиксной записи, а не через точку. Объяснение того, что делает функция run, можно найти здесь. Также в ходе своих экспериментов с Kotlin я выяснил, что простой hello world, упакованный в fat jar, весит 937 Кб, что не так уж много. Приведенная же выше программа в виде fat jar, как оказалось, весит чуть менее 2 Мб. Против 5.5 Мб в случае с аналогичной программой на Scala. Размер fat jar’ов, возможно, не является супер важным фактором при выборе языка, но на приятную мелочь вполне потянет.

Общие впечатления от языка у меня исключительно положительные. Как Java, только намного более лаконичный, и с удобняшками из мира ФП. Порог вхождения при этом намного ниже, чем у Scala. А скорость компиляции заметно выше. Эдакий очень правильный OCaml под JVM. Каких-то особо громких историй успеха и кучи своих фреймворков у Kotlin, правда, вроде бы нет. Но есть сильные подозрения, что это просто потому что при программировании на Kotlin используют все те же фреймворки, что при программировании на Java. И используют Kotlin те же Java-программисты, уставшие от многословности Java. В том числе был замечен очень сильный интерес к языку Kotlin со стороны разработчиков под Android. Да, что же касательно поддержки языка со стороны IntelliJ IDEA, то тут тоже вроде все ОК.

Ссылки по теме:

А что вы думаете о Kotlin и не пишите ли на нем случайно?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.