Зачем использовать 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>
, не было бы проблем.