Введение в Стрелу в Котлине
1. обзор
Arrow - это библиотека, объединенная изKΛTEGORY иfunKTionale.
В этом руководстве мы рассмотрим основы Arrow и то, как он может помочь нам использовать возможности функционального программирования на Kotlin.
Мы обсудим типы данных в основном пакете и рассмотрим пример использования обработки ошибок.
2. Maven Dependency
Чтобы включить Arrow в наш проект, нам нужно добавитьthe arrow-core dependency:
io.arrow-kt
arrow-core
0.7.3
3. Функциональные типы данных
Начнем с изучения типов данных в основном модуле.
3.1. Введение в монады
Некоторые из обсуждаемых типов данных здесь - Монады. В основном монады обладают следующими свойствами:
-
Они представляют собой специальный тип данных, который по сути является оберткой вокруг одного или нескольких необработанных значений.
-
У них есть три публичных метода:
-
фабричный метод для переноса значений
-
map
-
flatMap
-
-
Эти методы действуютnicely, то есть не имеют побочных эффектов.
В мире Java массивы и потоки - это монады, ноOptional isn’t. Чтобы узнать больше о монадах, возможно,bag of peanuts can help.
Теперь давайте посмотрим на первый тип данных из модуляarrow-core.
3.2. Idс
Id - простейшая оболочка в Arrow.
Мы можем создать его с помощью конструктора или фабричного метода:
val id = Id("foo")
val justId = Id.just("foo");
И у него есть методextract для извлечения обернутого значения:
Assert.assertEquals("foo", id.extract())
Assert.assertEquals(justId, id)
КлассId выполняет требования шаблона Monad.
3.3. Optionс
Option - это тип данных для моделирования значения, которое может отсутствовать, аналогично необязательному в Java.
И хотя технически это не монада, она все же очень полезна.
Он может содержать два типа:Some wrapper вокруг значения илиNone, когда оно не имеет значения.
У нас есть несколько разных способов создатьOption:
val factory = Option.just(42)
val constructor = Option(42)
val emptyOptional = Option.empty()
val fromNullable = Option.fromNullable(null)
Assert.assertEquals(42, factory.getOrElse { -1 })
Assert.assertEquals(factory, constructor)
Assert.assertEquals(emptyOptional, fromNullable)
Now, there is a tricky bit here,, то есть фабричный метод и конструктор ведут себя по-разному дляnull:
val constructor : Option = Option(null)
val fromNullable : Option = Option.fromNullable(null)
Assert.assertNotEquals(constructor, fromNullable)
Мы предпочитаем второй вариант, поскольку у него нет рискаKotlinNullPointerException :
try {
constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
fromNullable.map { s -> s!!.length }
}
3.3. Eitherс
Как мы видели ранее,Option может не иметь значения (None) или иметь некоторое значение (Some).
Either идет дальше по этому пути и может принимать одно из двух значений. Either имеет два общих параметра для типа двух значений, которые обозначаются какright иleft:
val rightOnly : Either = Either.right(42)
val leftOnly : Either = Either.left("foo")
Этот класс разработан какright-biased. Таким образом, правая ветвь должна содержать бизнес-ценность, скажем, результат некоторых вычислений. Левая ветвь может содержать сообщение об ошибке или даже исключение.
Следовательно, метод извлечения значений (getOrElse) спроектирован с правой стороны:
Assert.assertTrue(rightOnly.isRight())
Assert.assertTrue(leftOnly.isLeft())
Assert.assertEquals(42, rightOnly.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.getOrElse { -1 })
Даже методыmap иflatMap предназначены для работы с правой стороной и пропускают левую:
Assert.assertEquals(0, rightOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertTrue(rightOnly.flatMap { Either.Right(it % 2) }.isRight())
Assert.assertTrue(leftOnly.flatMap { Either.Right(it % 2) }.isLeft())
Мы исследуем, как использоватьEither для обработки ошибок, в разделе 4.
3.4. Evalс
Eval - это монада, предназначенная для управления оценкой операций. It has a built-in support for memoization and eager and lazy evaluation.
С помощью фабричного методаnow мы можем создать экземплярEval из уже вычисленных значений:
val now = Eval.now(1)
Операцииmap иflatMap будут выполняться лениво:
var counter : Int = 0
val map = now.map { x -> counter++; x+1 }
Assert.assertEquals(0, counter)
val extract = map.value()
Assert.assertEquals(2, extract)
Assert.assertEquals(1, counter)
Как мы видим,counter изменяется только после вызова методаvalue .
Методlater factory создаст экземплярEval из функции. Оценка будет отложена до вызоваvalue, и результат будет запомнен:
var counter : Int = 0
val later = Eval.later { counter++; counter }
Assert.assertEquals(0, counter)
val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)
val secondValue = later.value()
Assert.assertEquals(1, secondValue)
Assert.assertEquals(1, counter)
Третья фабрика -always. Он создает экземплярEval, который будет пересчитывать данную функцию каждый раз, когда вызываетсяvalue:
var counter : Int = 0
val later = Eval.always { counter++; counter }
Assert.assertEquals(0, counter)
val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)
val secondValue = later.value()
Assert.assertEquals(2, secondValue)
Assert.assertEquals(2, counter)
4. Шаблоны обработки ошибок с функциональными типами данных
Обработка ошибок путем выдачи исключений имеет несколько недостатков.
Для методов, которые часто и предсказуемо дают сбой, таких как анализ пользовательского ввода в виде числа, создавать исключения дорого и не нужно. Наибольшая часть затрат приходится на методfillInStackTrace. Действительно, в современных средах трассировка стека может невероятно долго расти с удивительно небольшим количеством информации о бизнес-логике.
Более того, обработка проверенных исключений может легко усложнить код клиента. С другой стороны, с исключениями времени выполнения у вызывающей стороны нет информации о возможности исключения.
Затем мы реализуем решение, чтобы узнать, является ли наибольший делитель четного входного числа квадратным числом. Пользовательский ввод поступит в виде строки. Наряду с этим примером мы исследуем, как типы данных Arrow могут помочь в обработке ошибок.
4.1. Обработка ошибок с помощьюOption
Сначала мы анализируем входную строку как целое число.
К счастью, у Kotlin есть удобный, безопасный для исключения метод:
fun parseInput(s : String) : Option = Option.fromNullable(s.toIntOrNull())
Мы оборачиваем результат синтаксического анализа вOption. Затем мы преобразуем это начальное значение с помощью некоторой собственной логики:
fun isEven(x : Int) : Boolean // ...
fun biggestDivisor(x: Int) : Int // ...
fun isSquareNumber(x : Int) : Boolean // ...
Благодаря дизайнуOption наша бизнес-логика не будет загромождена обработкой исключений и ветвями if-else:
fun computeWithOption(input : String) : Option {
return parseInput(input)
.filter(::isEven)
.map(::biggestDivisor)
.map(::isSquareNumber)
}
Как мы видим, это чистый бизнес-код, без обременения техническими деталями.
Посмотрим, как клиент может работать с результатом:
fun computeWithOptionClient(input : String) : String {
val computeOption = computeWithOption(input)
return when(computeOption) {
is None -> "Not an even number!"
is Some -> "The greatest divisor is square number: ${computeOption.t}"
}
}
Это здорово, но у клиента нет подробной информации о том, что не так с вводом.
Теперь давайте посмотрим, как мы можем предоставить более подробное описание случая ошибки с помощьюEither.
4.2 Error Handling with Either
У нас есть несколько вариантов для возврата информации о случае ошибки сEither. С левой стороны мы могли бы включить сообщение String, код ошибки или даже исключение.
Сейчас мы создаем запечатанный класс для этой цели:
sealed class ComputeProblem {
object OddNumber : ComputeProblem()
object NotANumber : ComputeProblem()
}
Мы включаем этот класс в возвращаемыйEither. В методе синтаксического анализа мы будем использовать фабричную функциюcond:
Either.cond( /Condition/, /Right-side provider/, /Left-side provider/)
Итак, вместоOption, we будем использоватьEither in наш методparseInput:
fun parseInput(s : String) : Either =
Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> ComputeProblem.NotANumber } )
Это означает, чтоEither будет заполнен числомeither или объектом ошибки.
Все остальные функции будут такими же, как и раньше. Однако методfilter отличается дляEither. Требуется не только предикат, но и поставщик левой части ложной ветви предиката:
fun computeWithEither(input : String) : Either {
return parseInput(input)
.filterOrElse(::isEven) { -> ComputeProblem.OddNumber }
.map (::biggestDivisor)
.map (::isSquareNumber)
}
Это потому, что нам нужно указать другую сторонуEither, в случае, если наш фильтр возвращаетfalse.
Теперь клиент будет точно знать, что было не так с их вводом:
fun computeWithEitherClient(input : String) {
val computeWithEither = computeWithEither(input)
when(computeWithEither) {
is Either.Right -> "The greatest divisor is square number: ${computeWithEither.b}"
is Either.Left -> when(computeWithEither.a) {
is ComputeProblem.NotANumber -> "Wrong input! Not a number!"
is ComputeProblem.OddNumber -> "It is an odd number!"
}
}
}
5. Заключение
Библиотека Arrow была создана для поддержки функциональных возможностей в Kotlin. Мы исследовали предоставленные типы данных в пакете arrow-core. Затем мы использовалиOptional иEither для обработки ошибок функционального стиля.
Как всегда доступен кодover on GitHub.