Search code examples
scalatypestypeclasshigher-kinded-types

scala type class with higher kinded types and variance


I have a question very similar to this one: Scala higher kinded type variance

This, however is slightly different in that, well, it doesn't compile (scala 2.11.8).

The basic idea is to take a provided array of "things". If the array is null, return a default value of some type (e.g. Boolean, Option, List[Int]), otherwise do work on the array and produce a result. The result and default value have the same type.

The challenge I'm having is getting this to work across a broad set of result types.

Here's a contrived example:

  trait NullGuard[F[_]] {
    def nullGuard[A, B](arr: Array[A], default: F[B])(expr: => F[B]): F[B] =
      if (arr == null || arr.length == 0) default else expr
  }

Let's create an implementation that returns an Option:

  implicit def optionNullGuard[F[X] <: Option[X]]: NullGuard[F] = new NullGuard[F]() {}

The above does compile, but the following attempt to use the above type class does not:

  def returnsOption[F[_], A, B](arr: Array[A])(implicit ng: NullGuard[F]): Option[B] = {
    ng.nullGuard(arr, None) {
      // sample work
      if (arr.length % 2 == 0) Option(1) else None
    }
  }

I get the following compile error:

type mismatch;
found   : None.type
required: F[?]
  ng.nullGuard(arr, None){

How can I get this to work? I'm also open to another approach, if there is one.


Solution

  • Since your typeclass does not have any abstract methods, it can be replaced by a single polymorphic nullGuard method:

    def nullGuard[A, B]
      (arr: Array[A], defaultValue: B)
      (processArray: Array[A] => B)
    : B = if (arr == null || arr.isEmpty) defaultValue else processArray(arr)
    

    The higher kinded type parameter F also seems no longer necessary: it doesn't cost you anything to provide a method that works for any B as return type, instead of just F[B].

    Here is your contrived, slightly modified example: extracting the last value from an array if it has an even number of elements:

    for (example <- List[Array[Int]](null, Array(), Array(42), Array(1, 42))) {
      val lastAtEvenIndex = nullGuard[Int, Option[Int]](example, Some(0)) { 
        a => if (a.size % 2 == 0) Option(a.last) else None
      }
      println(lastAtEvenIndex)
    }
    

    Output:

    Some(0)
    Some(0)
    None
    Some(42)
    

    It returns None for arrays of uneven length, and treats empty/null arrays as if they had 0 as the "last" element.


    Full example as a single code snippet with None as default value:

    def nullGuard[A, B]
      (arr: Array[A], defaultValue: B)
      (processArray: Array[A] => B)
    : B = if (arr == null || arr.isEmpty) defaultValue else processArray(arr)
    
    
    for (example <- List[Array[Int]](null, Array(), Array(42), Array(1, 42))) {
      val lastAtEvenIndex = nullGuard[Int, Option[Int]](example, None) { 
        a => if (a.size % 2 == 0) Option(a.last) else None
      }
      println(lastAtEvenIndex)
    }
    

    prints:

    None
    None
    None
    Some(42)