Зачем использовать Arrow's Options вместо Kotlin nullable

Я смотрел на библиотеку Эрроу, найденную здесь. Зачем когда-либо хотеть использовать Option типа вместо котлина встроены обнуляемые?

1 ответ

Я использовал OptionТип данных предоставлялся Arrow более года, и вначале мы задали себе тот же вопрос. Ответ следует.

Вариант против Nullable

Если сравнить только option тип данных с nullablesв Котлине они почти равны. Та же семантика (есть какое-то значение или нет), почти такой же синтаксис (с Option, который вы используетеmap, с значениями NULL вы используете безопасный оператор вызова).

Но при использовании Options вы даете возможность воспользоваться преимуществами экосистемы стрелки!

Экосистема Arrow (функциональная экосистема)

Когда используешь Options вы используете Monad Pattern. При использовании шаблона монады с такими библиотеками, как arrow, scala cats, scalaz, вы можете воспользоваться преимуществами нескольких функциональных концепций. Всего 3 примера преимуществ (их намного больше):

1. Доступ к другим монадам

Optionне единственный! Например,Eitherочень полезно выражать и избегать создания исключений.Try, Validated а также IO являются примерами других распространенных монад, которые помогают нам делать (лучше) то, что мы делаем в типичных проектах.

2. Преобразование между монадами + абстракции

Вы можете легко преобразовать одну монаду в другую. У тебяTry но хочу вернуть (и выразить) Either? Просто конвертируйся в это. У вас естьEitherно не заботится об ошибке? Просто конвертируйте вOption.

val foo = Try { 2 / 0 }
val bar = foo.toEither()
val baz = bar.toOption()

Эта абстракция также помогает вам создавать функции, которые не заботятся о самом контейнере (монаде), а только о содержимом. Например, вы можете создать метод расширенияSum(anyContainerWithBigDecimalInside, anotherContainerWithBigDecimal)который работает с ЛЮБОЙ МОНАДОЙ (точнее: "к любому экземпляру аппликативного") следующим образом:

fun <F> Applicative<F>.sum(vararg kinds: Kind<F, BigDecimal>): Kind<F, BigDecimal> {
    return kinds.reduce { kindA, kindB ->
        map(kindA, kindB) { (a, b) -> a.add(b) }
    }
}

Немного сложный для понимания, но очень полезный и простой в использовании.

3. Понимания монад

Переход от значений NULL к монадам - ​​это не просто изменение операторов безопасного вызова на mapзвонки. Взгляните на функцию "привязки", которую предоставляет стрелка как реализация шаблона "Монадные понимания":

fun calculateRocketBoost(rocketStatus: RocketStatus): Option<Double> {
    return binding {
        val (gravity) = rocketStatus.gravity
        val (currentSpeed) = rocketStatus.currentSpeed
        val (fuel) = rocketStatus.fuel
        val (science) = calculateRocketScienceStuff(rocketStatus)
        val fuelConsumptionRate = Math.pow(gravity, fuel)
        val universeStuff = Math.log(fuelConsumptionRate * science)

        universeStuff * currentSpeed
    }
}

Все используемые функции, а также свойства из rocketStatus параметр в приведенном выше примере Options. Внутриbinding заблокировать flatMapзвонок для нас абстрагируется. Код намного проще читать (и писать), и вам не нужно проверять, присутствуют ли значения, если некоторые из них нет, вычисление остановится, и результатом будет вариант сNone!

Теперь попробуйте представить этот код с нулевыми проверками. Не простоsafe call operators но также вероятно if null then returnпути кода. Намного сложнее, не правда ли?

Кроме того, в приведенном выше примере используется Optionно истинная сила понимания монад как абстракции заключается в том, что вы используете его с монадами, такими как IO, в которых вы можете абстрагироваться от выполнения асинхронного кода точно таким же "чистым, последовательным и императивным" способом, как указано выше:

Вывод

Я настоятельно рекомендую вам начать использовать монады вроде Option, Eitherи т. д., как только вы увидите, что концепция соответствует нужной вам семантике, даже если вы не уверены, извлечете ли вы другие большие выгоды из функциональной экосистемы или еще не очень хорошо их знаете. Скоро вы будете использовать его, даже не заметив кривой обучения. Что В моей компании мы используем его почти во всех проектах Kotlin, даже в объектно-ориентированных (а их большинство).

Отказ от ответственности: Если вы действительно хотите подробно рассказать о том, почему Arrow полезен, перейдите по https://soundcloud.com/user-38099918/arrow-functional-library и послушайте одного из людей, которые над ним работают, (5:35мин)

Люди, которые просто создают и используют эту библиотеку, хотят использовать Kotlin иначе, чем люди, которые ее создали, и используют "тип данных Option, аналогичный тому, как Scala, Haskell и другие языки FP обрабатывают необязательные значения".

Это просто еще один способ определения возвращаемых типов значений, для которых вы не знаете, что выводить.

Позвольте мне показать вам три версии:

обнуляемость в Котлине

val someString: String? = if (condition) "String" else null

объект с другим значением

val someString: String = if (condition) "String" else ""

версия Arrow

val someString: Option<String> = if (condition) "String" else None

Основная часть логики Kotlin может заключаться в том, чтобы никогда не использовать обнуляемые типы, такие как String?, но вам нужно будет использовать его при взаимодействии с Java. При этом вам нужно использовать безопасные звонки, такие как string?.split("a") или ненулевое утверждение string!!.split("a"),

Я думаю, что совершенно правильно использовать безопасные вызовы при использовании библиотек Java, но парни из Arrow, кажется, думают иначе и хотят постоянно использовать свою логику.

Преимущество использования логики " Стрелка" заключается в том, что "пользователи могут определять приложения и библиотеки на чистом FP, построенные на основе абстракций высшего порядка. Используйте приведенный ниже список, чтобы узнать больше об основных функциях Λrrow".

Одна вещь, о которой не упоминали другие ответы: вы можете иметь Option<Option<SomeType>> где ты не можешь SomeType??. ИлиOption<SomeType?>, в этом отношении. Это очень полезно для композиционности. Например, рассмотрим КотлинаMap.get:

abstract operator fun get(key: K): V?

Возвращает значение, соответствующее данному ключу, или null если такой ключ отсутствует на карте.

А вдруг Vэто тип, допускающий значение NULL? Тогда, когдаget возвращается nullэто может быть из-за того, что карта сохранила нулевое значение для данного ключа или из-за отсутствия значения; ты не можешь сказать! Если он вернулсяOption<V>, не было бы проблем.

Другие вопросы по тегам