Рубрики
Без рубрики

Функциональные интерфейсы: ненависть к себе в Kotlin

Этот пост является копией предыдущих постов на Medium (начальный, последующий) Но так как я планирую дел… Помеченный как kotlin, функциональный, java.

Этот пост является копией предыдущих постов на Medium ( начальный , последующие ) Но так как я планирую удалить свою учетную запись Medium, я переместил их сюда.

Котлин – замечательный язык программирования. Примерно после 12 лет программирования на Java, работая с Kotlin, мне захотелось надеть очки после долгих лет прищуривания: здесь так много всего, что можно любить.

Но, как и в любых отношениях, некоторые причуды вы обнаруживаете только позже в вашей совместной жизни. После переноса все большего и большего количества кода Java в код Kotlin я заметил кое-что довольно странное и, откровенно говоря, немного раздражающее.

Именно так Котлин обрабатывает функциональные интерфейсы .

Давайте вернемся в прошлое, в мир без лямбд. Это было ужасно многословно!

interface JavaInterface {
    String doSomething(Item item);
}

String delegateWork(JavaInterface f) {
    return f.doSomething(item);
}

void doWork() {
    delegateWork(new JavaInterface() {
        @Override
        public String doSomething(Item item) {
            return "Item = " + item;
        }
    });
}

Наконец-то Java 8 дала нам лямбды, и мы смогли избавиться от большого количества кода и сосредоточиться на том, что важно. Кроме того, нас не заставляли писать собственный функциональный интерфейс для каждой простой функции, мы могли бы просто использовать некоторые из предоставленных oracle, такие как: java.util.function. Функция R> R>

@FunctionalInterface
interface JavaInterface {
    String doSomething(Item item);
}

String delegateWork(JavaInterface f) {
    return f.doSomething(item);
}

String delegateOtherWork(Function f) {
    return f.apply(item);
}

void doWork() {
    delegateWork(item -> "Item = " + item);
    delegateOtherWork(item -> "Item = " + item);
}

Все было хорошо, пока вы не поняли, что, хотя у вас теперь есть типы функций, они все еще не являются первоклассными гражданами языка. Хотите доказательств? Угадайте, сколько “Типов функций” пришлось ввести в Java? Один? Три? Пять?

Не верите мне, посмотрите сами: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

И если вам этого недостаточно, добавьте joomla в микс, и у вас будет доступ еще к 35: https://github.com/jOOQ/jOOL/tree/master/jOOL/src/main/java/org/jooq/lambda/function Потому что кому бы не понравилось встретить сигнатуру метода, которая выглядит так:

Function5> higherOrder(Function12, String, Integer, Long, String, Double, Optional>>)

😜 примечание: jOOL на самом деле довольно аккуратная библиотека, и ее стоит проверить.

Теперь давайте добавим Kotlin в смесь. В Котлине функции выполняют первоклассные граждане. Так что нет необходимости запоминать десятки слегка отличающихся типов функций. Вам просто нужно запомнить синтаксис типа функции Kotlin:

(Parameter1Type, Parameter2Type, ParameterNType) -> ReturnType

Вот и все, вот и все, что нужно сделать.

Итак, хорошо, почему мы здесь, в чем проблема?

Как упоминалось ранее, по мере того, как я переносил все больше и больше кода с Java на Kotlin. Я столкнулся с некоторыми проблемами при работе с пользовательскими функциональными интерфейсами. Потому что иногда вам нужна эта дополнительная описательность.

Давайте вернемся к нашему примеру Java 8

@FunctionalInterface
interface JavaInterface {

    String doSomething(Item item);
}

class JavaComponent {

    private Item item = new Item();

    String delegateWork(JavaInterface f) {
        return f.doSomething(item);
    }

    String delegateOtherWork(Function f) {
        return f.apply(item);
    }
}

Теперь давайте используем его из кода Kotlin

delegateWork { "Print $it" }
delegateOtherWork { "Print $it" }

Хорошо, это здорово, именно то, что мы ожидали! Хорошо, теперь давайте перенесем это Компонент Java класс для Kotlin. Обратите внимание, что мы изменили java.util.функцию. Функция<Элемент, строка> к типу функции Kotlin (Предмет) -> Строка

class KotlinComponent(private val item: Item = Item()) {

    fun delegateWork(f: JavaInterface): String {
        return f.doSomething(item)
    }

    fun delegateOtherWork(f: (Item) -> String): String {
        return f.invoke(item)
    }
}

Давайте посмотрим, что происходит, когда мы используем эти функции более высокого порядка из кода Java.

delegateWork(item -> "Print: " + item);
delegateOtherWork(item -> "Print: " + item);

Ничего необычного, как и ожидалось, мы можем использовать одну и ту же лямбду для обоих методов. Давайте посмотрим, что произойдет, когда мы сделаем то, что мы ожидаем в Котлине:

delegateWork { "Print $it" }

Error: Kotlin: Type mismatch: inferred type is () -> String but JavaInterface was expected

Что случилось? Похоже, компилятор не может понять, что сигнатура лямбды совпадает с методом функционального интерфейса. Похоже, компилятор не может понять, что сигнатура лямбды совпадает с методом функционального интерфейса.

Поэтому мы должны четко сказать, чего мы ожидаем:

delegateWork(JavaInterface { "Print $it" })

Я думаю, что это довольно разочаровывает но это не так уж плохо. Теперь давайте посмотрим, что произойдет, когда мы также перенесем интерфейс на Kotlin:

interface KotlinInterface {
    fun doSomething(item: Item): String
}

class KotlinComponent(private val item: Item = Item()) {

    fun delegateWork(f: KotlinInterface): String {
        return f.doSomething(item)
    }

    fun delegateOtherWork(f: (Item) -> String): String {
        return f.invoke(item)
    }
}

Когда мы используем компонент Kotlin класс из Java, как и ожидалось, ничего не меняется, лямбды остаются точно такими же. Что, если мы используем его из кода Котлина:

delegateWork { "Print $it" }

Error: Kotlin: Type mismatch: inferred type is () -> String but KotlinInterface was expected

Похоже, то ЖЕ самое преобразование снова завершается неудачей. Теперь, что, если мы просто прямо упомянем интерфейс, как мы делали раньше?

delegateWork(KotlinInterface { "Print $it" })

Error: Kotlin: Interface KotlinInterface does not have constructors

Это тоже не помогло. Нам нужно создать анонимный объект, чтобы он работал:

delegateWork(object : KotlinInterface {
    override fun doSomething(item: Item): String {
        return "Print $item"
    }
})

Черт возьми! Это похоже на повторную работу с Java 7. К сожалению, это связано с тем, что Kotlin еще не поддерживает преобразование SAM для интерфейсов Kotlin, поэтому нам приходится создавать этот анонимный объект. Смотрите также: https://youtrack.jetbrains.com/issue/KT-7770 https://stackoverflow.com/a/43737962/611032

Итак, как мы можем избежать этих подробных анонимных объектов и по-прежнему использовать пользовательское имя для функции? Мы используем псевдоним типа:

/**
 * Very helpful comment.
 */
typealias KotlinFunctionAlias = (Item) -> String

fun delegateAliasWork(f: KotlinFunctionAlias): String {
  return f.invoke(item)
}

Итак, теперь мы можем передать лямбду так, как мы ожидали, и у нас все еще есть преимущество пользовательского имени для функции.

delegateAliasWork { "Print $it" }

Значит, все хорошо, дело закрыто, пора идти домой. К сожалению, не совсем.

Потерялся в переводе

Одна незначительная проблема с псевдонимами типов заключается в том, что, хотя вы можете назвать тип функции, вы не можете назвать имя метода:

val iface: JavaInterface = JavaInterface { "Print $it" }
iface.doSomething(item)

val alias: KotlinFunctionalAlias = { item -> "Print $item" }
alias.invoke(item)
alias(item)

Выбор подходящих имен для псевдонима типа и переменной может смягчить проблему. К счастью, мы, разработчики, отлично разбираемся в именовании вещей 😜

Безопасность типа

Более серьезная проблема заключается в том, что, хотя псевдоним типа дает нам другое имя, на самом деле они не являются разными типами, поэтому на самом деле мы не безопасны для типов.

Давайте рассмотрим пример Java с двумя функциональными интерфейсами, которые имеют одинаковую сигнатуру метода.

JavaInterface1 f1 = item -> "Print " + item;
JavaInterface2 f2 = item -> "Print " + item;
f1 = f2;

Error: java: incompatible types: JavaInterface2 cannot be converted to JavaInterface1

Это то, чего мы ожидали бы, мы не хотим смешивать здесь яблоки и апельсины.

Что произойдет, если мы сделаем то же самое с нашими псевдонимами типа Kotlin? (Я думаю, вы знаете, к чему я клоню)

var f1: KotlinFunctionAlias1 = { item -> "Print $item" }
var f2: KotlinFunctionAlias2 = { item -> "Print $item" }
var f3: (Item) -> String = { item -> "Print $item" }
f1 = f2
f2 = f3
f1 = f3

Это прекрасно работает, компилятор не жалуется, потому что, как я уже упоминал, на самом деле они не являются разными типами. Все они просто: (Элемент) -> Строка

Итак, давайте быстро рассмотрим различные способы, которыми мы можем справиться с отсутствующим преобразованием SAM в Kotlin для интерфейсов Kotlin, а также их плюсы и минусы

Оставьте функциональные интерфейсы в качестве интерфейсов Java

; + Хорошая совместимость с Java + Поддержка пользовательского имени метода + Безопасность типов

– Необходимо добавить префикс Kotlin lambda к имени интерфейса – Необходимы дополнительные круглые скобки – Необходимо поддерживать код Java

Используйте псевдоним типа для типов функций Kotlin

+ Хорошая совместимость с Java + Простота использовать

– Небезопасно для ввода – Нет имени пользовательского метода

Используйте онлайн-классы

Еще один вариант, который мы еще не обсуждали, – это использование экспериментальных встроенных классов Kotlin. Вы могли бы “обернуть” функцию Kotlin встроенным классом.

inline class KotlinInlineInterface(val doSomething: (Item) -> String)

fun delegateInlineWork(f: KotlinInlineInterface): String {
    return f.doSomething.invoke(item)
}

delegateInlineWork(KotlinInlineInterface { "Print $it" })

Несмотря на то, что это работает, я не думаю, что это подходящий способ использования онлайн-классов. Также совместимость Java в настоящее время не поддерживается: Также совместимость Java в настоящее время не поддерживается:

Всегда используйте типы функций Kotlin

Да, вы могли бы просто использовать ((Параметр) -> Возвращает типы везде. Часто этого бывает достаточно, но по мере роста вашего приложения его может становиться все труднее читать и поддерживать, а также он может быть более подвержен ошибкам.

Жить с анонимными объектами

Конечно, если вы не возражаете, вы можете просто жить с анонимными объектами, надеясь, что когда-нибудь Kotlin будет поддерживать полное преобразование SAM и использовать замечательную интеграцию IDE для переноса ваших анонимных объектов в лямбды

\ (ツ) /

На Reddit состоялась короткая дискуссия: На Reddit состоялась короткая дискуссия:

С тех пор я получил ответ от Романа Елизарова на эту тему

Я попробовал упомянутый вариант компилятора Kotlin:

// Gradle Kotlin DSL
tasks.withType {
    kotlinOptions.freeCompilerArgs += "-XXLanguage:+NewInference"
}
// Gradle Groovy DSL
compileKotlin {
    kotlinOptions {
        freeCompilerArgs += "-XXLanguage:+NewInference"
    }
}

Если вы больше интересуетесь другими системами сборки, обратитесь к документации Kotlin ( Maven / Ant ), чтобы узнать, как передавать аргументы компилятора Kotlin.

Проблема решена?

Сначала давайте посмотрим, что происходит, когда мы используем функциональный интерфейс Kotlin в коде Kotlin:

fun delegateWork(f: KotlinInterface): String {
    return f.doSomething(item)
}

delegateWork { item -> "Print: $item" }

Error: Type mismatch: inferred type is (Nothing) -> TypeVariable(_L) but KotlinInterface was expected

Как насчет явного указания интерфейса?

delegateWork(KotlinInterface { item -> "Print $item" }

Error: Interface KotlinInterface does not have constructors

Облом! Нам все еще нужен анонимный объект.

Как насчет использования функционального интерфейса Java в Kotlin код?

fun javaInterface(f: JavaInterface) {
    val res = f.doSomething(item)
    output(res)
}

javaInterface { item -> "Print: $item" }

Наконец: именно то, что мы ожидали . Все хорошо, пиво заслужено!

Терпение юный джедай

Если вы наблюдательны, вы увидите это во время сборки:

w: ATTENTION!
This build uses unsafe internal compiler arguments:
-XXLanguage:+NewInference

This mode is not recommended for production use,
as no stability/compatibility guarantees are given on
compiler or generated code. Use it at your own risk!

Так что же это значит? Это означает то, что здесь сказано: это еще не совсем безопасно в использовании. Но, зная, что JetBrains работает в этом направлении, я бы предложил, чтобы мы сейчас действовали следующим образом (от наиболее благоприятного к наименее благоприятному)

  1. Сохраняйте функциональные интерфейсы в виде кода Java
  2. Используйте псевдонимы типов для типов функций Kotlin (если вы можете смириться с возможным смешиванием яблок и апельсинов)
  3. Жить с анонимными объектами

Спасибо за чтение. Как всегда, я открыт для критики и отзывов.

Оригинал: “https://dev.to/ranilch/functional-interfaces-self-loathing-in-kotlin-4bce”