Skip to main content

From other FP languages

Arrow is heavily influenced by functional programming. If you're used to working with those concepts, the journey to Arrow should be a pleasant one. In this section, we review the most important differences with other ecosystems.

scala

The Scala Standard Library contains many of the types provided by Arrow like Either. There's also a vibrant community which brings even more functional features, like the Typelevel ecosystem.

haskell

Haskell is often considered the prime example of a pure functional programming language. Most of the utilities in Arrow are found in their base library.

Computation blocks

From Either to Raise

You may be interested in our tutorial focusing specifically on transitioning from wrapper types for errors, like Either, to the Raise DSL.

Both Scala and Haskell have special support for types which define a flatMap or bind operation, namely for comprehensions and do notation. Either is one such type, so you can use for or do to perform validation.

def mkPerson(name: String, age: Int): Either[Problem, Person] = for {
name_ <- validName(name)
age_ <- validAge(age)
} yield Person(name_, age_)
mkPerson :: String -> Int -> Either Problem Person
mkPerson name age = do
name_ <- validName name
age_ <- validAge age
pure (Person name_ age_)

In Haskell you can get closer to this style using Applicative operators. The code still looks different than its pure counterpart, since you need to sprinkle (<$>) and (<*>).

mkPerson name age = Person <$> validName name <*> validAge age

Kotlin doesn't provide such a generic construct. However, Arrow provides a similar syntax for error types.

  • You must explicitly request to work with an error type, using either, result, or nullable, instead of for. Those functions live in the arrow.core.raise package.
  • Every usage of <- translates into a call to .bind().
fun mkPerson(name: String, age: Int): Either<Problem, Person> = either {
val name_ = validName(name).bind()
val age_ = validAge(age).bind()
Person(name_, age_)
}

Furthermore, the result of .bind() is just of regular type, so you can completely inline the calls if desired. This style is very similar to Haskell's use of Applicative operators, except that operators appear at the level of arguments, instead of at the level of functions.

fun mkPerson(name: String, age: Int): Either<Problem, Person> = either {
Person(validName(name).bind(), validAge(age).bind())
}
No zip

It's common to use functions like zip to combine values inside a wrapper, instead of a for comprehension. In Haskell this often takes the form of (<$>) and (<*>). In Arrow we prefer to use blocks, except when dealing with concurrency.

No traverse

If you want to apply an effectful operation to every element of a collection, you need to use a function different from map, usually called traverse. This split does not exist in Arrow: you can use the same functions you know and love from the collections API inside one of these blocks.

suspend instead of IO

The utilities provided by Arrow for working with side effects are based on coroutines, that is, functions marked as suspend. In contrast, Haskell introduces a special IO wrapper type to mark side-effects, as done by popular Scala libraries like Cats Effect.

Read more

Our Design section includes a post comparing the different approaches for effect handling.

Higher-kinded abstractions

Both Scala and Haskell allow abstractions that operate at the level of type constructors. For example, a function like flatMap which always has the form F<A>.flatMap(next: (A) -> F<B>): F<B> is part of an interface / type class called Monad. Kotlin doesn't provide this feature, but Arrow still follows the naming convention for consistency. The following list relates the names to abstractions in Cats and Haskell's base.

  • map comes from Functor.
  • contramap comes from Contravariant functors.
  • fold comes from Foldable.
  • zip comes from Applicative, although it's called product or (&&) in other languages.
  • traverse and sequence come from Traversable, but those functions are being deprecated, because the same behavior can be achieved with regular list functions and computation blocks.
Semigroup and Monoid

Arrow Core contains Semigroup and Monoid as interfaces. They are, however, marked as deprecated, and due for removal in Arrow 2.0. Functions that required a Semigroup or Monoid argument have been replaced by variants which take the combination function, and the corresponding empty element. This design fits better with other parts of the Kotlin ecosystem, like the fold function in kotlin.collections.