Введение в Стрелу в Котлине

Введение в Стрелу в Котлине

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.