В мире 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 соответствующих страниц. Вот что у меня получилось:
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. Как и ScalaKotlin позволяет вызывать методы при помощи инфиксной записиа не через точку. Объяснение тогочто делает функция run