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.
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 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
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
, ornullable
, instead offor
. Those functions live in thearrow.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())
}
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.
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.
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 fromFunctor
.contramap
comes fromContravariant
functors.fold
comes fromFoldable
.zip
comes fromApplicative
, although it's calledproduct
or(&&)
in other languages.traverse
andsequence
come fromTraversable
, but those functions are being deprecated, because the same behavior can be achieved with regular list functions and computation blocks.
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
.