3
votes

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.

1
What is the C[X] <: Option[X] good for? Do you expect to have any strict subclasses of Option in your code? (there should be only None and Some anyway, because the trait is sealed).Andrey Tyukin
I was using that as an attempt to return Some or None. I'm pretty new at this so I'm not sure what is required, and what isn't, to make this workErik
The A <: Array[A] also looks somewhat mysterious. Could you maybe elaborate what exactly you wanted to achieve with that?Andrey Tyukin
I think it could have been written as def nullGuard[A, B](arr: Array[A], default: Res[B])... . (I updated my sample code above to reflect this)Erik

1 Answers

5
votes

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)