Partially functional

You can't wait for inspiration. You have to go after it with a club.

Polymorphism and higher kinds

• scala

We’ll talk briefly about parametric, subtype and ad-hoc polymorphism, then focus on ad-hoc polymorphism and its relationship with the type class pattern and higher-kinded types like Functor, then move on to discuss higher-kinded types shipped with the Cats library.

The following polymorphism discussion is based in part on this Herding Cats article.

Polymorphism

1. Parametric polymorphism

Using parametric polymorphism, a function or a data type can be written generically so that it can handle values identically without depending on their type.[1] When the type of a value contains one or more unconstrained type variables, the value may adopt any type that results from substituting those variables with concrete types.

  def fun[A](a:A): Unit = println(a)

2. Subtype polymorphism

We can constrain the type parameter by an upper bound:

trait PlusInt[A] {
  def plus(i2: A): A
}
def plusBySubtype[A <: PlusIntf[A]](a1: A, a2: A): A =
  a1.plus(a2)

This requires the types used in plusBySubtype to extend the trait. This means, it could not be an Int.

3. Ad-hoc polymorphism

We can constrain the type parameter by a context bound:

trait CanPlus[A] {
  def plus(a1: A, a2: A): A
}
def plus[A: CanPlus](a1: A, a2: A): A =
  implicitly[CanPlus[A]].plus(a1, a2)

This places a special kind of constraint on the type parameter. The actual type of A must be from a specific class of types (type class).

A context bound like the above will be de-sugared to:

def plus[A](a1: A, a2: A)(implicit cpa: CanPlus[A]): A =
  cpa.plus(a1, a2)

This means that in order to call the plus method, the compiler will need to be able to find evidence that behaviour/properties described by CanPlus have been implemented for the actual type of A (or an explicit instance must be provided by the programmer).

Type classes

What determines membership of a type T in a type class TC is the ability to provide an implementation of the trait TC for the specific type T (i.e. an instance of TC[T]). For example, the String type could be a member of the Party type class, if the compiler could find an implicit instance of Party[String] in scope. This would serve as evidence that the type String indeed belongs to the class of types that know how to Party.

(Read Eugene’s article to understand how implicits are resolved.)

Values, Types and Kinds

Kinds are to types what types are to values. Just like different values can be of the same type, Different types can be of the same kind. For example String, Int, Boolean and Banana are of the same kind - namely, they are all proper types. The kind of a proper type is usually denoted as *.


Some types are parameterised with other proper types: e.g. List, Option, Try and Future. Also known as type constructors, they require type argument(s) in order to construct a proper type. These would be classified as first-order types. There are various kinds of first-order types:

  • * -> * for a type constructor like List or Option or Function0
  • * -> * -> * for a type constructor that takes two type arguments like Either or Map or Function1
  • * -> * -> * -> * -> * -> * for something as highly parameterised as Function4

Higher-order types declare a first-order (or higher order) type parameter(s). Higher-order types declare type constructors as their type parameters. They are of a higher kind. Many higher-kinded types are provided by the cats library (or other libraries like ScalaZ):

  • (* -> *) -> * as is Functor[F[_]] or Monad[F[_]]
  • (* -> *) -> * -> * as is for example the OptionT[F[_], A] monad transformer
  • (* -> *) -> * -> * -> * like the EitherT[F[_], A, B] monad transformer
  • (* -> *) -> (* -> *) -> * -> * as in EitherK[F[_], G[_], A]

Cats

There’s a good discussion of Type classes on the Typelevel Cats site, from which we’ll “lift” this line:

Functional programming tends towards a combination of parametric polymorphism […] and ad-hoc polymorphism.

The cats library revolves around a rich collection of type classes that encode a variety of higher-level abstractions. List is an abstraction, Option is another, and both have something in common: both could be viewed as mappable containers - functors. Thus Functor is a higher-level abstraction - an abstraction over abstractions.

Laws

The cats library provides a number of lawful type class instances. All type classes come with a set of implicit laws - laws that are not explicitly laid out in the type class itself. For example a Semigroup implementation must provide an associative binary combine operation. The name, number and type of parameters and type of return value of the operation is known from its signature:

def combine(a1:T, a2:T):T

What is not obvious from the above is the requirement that the combine operation be associative. You, as the library client could attempt to hand-craft an instance of Semigroup[Int] for the integer subtraction operation, perhaps something like this:

def combine(a1:Int, a2:Int):Int = a1 - a2

However the integer subtraction operation is not associative, hence a lawful Semigroup instance for it does not exist. This thing we created above is not a semigroup, and it will not work as expected. Code may use an instance of Semigroup[Int] to combine while folding left or right, assuming that the Semigroup[Int] instance adheres to the associativity law.