Scala Type Parameter with Either

744 views Asked by At
class OptionLikeMatcher[F[_], T, U](typeName: String, toOption: F[T] => Option[U]) extends Matcher[F[T]] { ... }
case class RightMatcher[T]() extends OptionLikeMatcher[({type l[a]=Either[_, a]})#l, T, T]("Right", (_:Either[Any, T]).right.toOption)
case class LeftMatcher[T]() extends OptionLikeMatcher[({type l[a]=Either[a, _]})#l, T, T]("Left", (_:Either[T, Any]).left.toOption)

Can some one explain what are they trying to achieve here with the type parameters? What type does the toOption: F[T] => Option[U] parameter have when used for RightMatcher and LeftMatcher?

Its from Scala Specs2 library.

2

There are 2 answers

1
Andrey Tyukin On

Let's first address the most complicated type constructor here:

({type l[a]=Either[_, a]})#l

is the original way to encode type lambdas. It could be written more succinctly as either

Either[_, ?]

with kind-projector, or as

[X] =>> Either[_, X]

in Scala 3. It basically just selects the right parameter of Either as the relevant one, and replaces the left parameter by a wildcard.


Now, the three type parameters of OptionLikeMatcher are:

  • F[_] - the type constructor for the thing that's being matched; It is expected to behave "somewhat similarly" to Option, i.e. there should be some way to get an Option out of it.
  • T is the type that is plugged into the F type constructor; Together F[T] gives the type of what's being matched.
  • U can be thought as a "slightly perturbed" version of T; It captures basically the same information as T, but it allows for some controlled sloppiness when transforming an F[T] into an Option[U] (otherwise, if one required F[T] => Option[T], that would practically be the same as asking for quite a rigid structure called natural transformation F ~> Option, which might be too restrictive in this case).

Putting it all together gives for RightMatcher:

  • F[A] = Either[_, A] - i.e. pick right parameter of Either, ignore left parameter
  • The T from the OptionLikeMatcher is bound to T from RightMatcher
  • The U from the OptionLikeMatcher is also bound to T from RightMatcher, i.e. the possibility to "slightly perturb" T is not used here, both T and U from OptionLikeMatcher are instantiated by the same type.

Thus, the toOption in RightMatcher has the type Either[_, T] => Option[T]. The (_:Either[Any, T]).right.toOption conforms to Either[_, T] => Option[T], because the Any appears in a covariant position within Either, and Either[Any, T] appears in a contravariant position within the function type, so that Either[Any, T] => Option[T] is a subtype of Either[_, T] => Option[T], because its input type is strictly more permissive, and it can be used everywhere an Either[_, T] => Option[T] is needed.


All that being said, I couldn't find any applications of F that were not of shape F[T] (neither in OptionLikeMatcher, nor in OptionLikeChekedMatcher), so it seems that they are splitting up the input type into a type constructor F and a type parameter T, only to use them as an indivisible unit F[T] later on. Maybe this was left over after a refactoring from some more complicated construct.

0
Mario Galic On

What type does the toOption: F[T] => Option[U] parameter have when used for RightMatcher and LeftMatcher

Perhaps a concrete drill exercise would help. Let's consider RightMatcher[Int]() where we instantiate type parameter T to Int. This gives us

F = ({type l[a]=Either[_, a]})#l
T = Int
U = T = Int

and plugging the above type instantiations into

toOption: F[T] => Option[U]

we get

toOption: (({type l[a]=Either[_, a]})#l)[Int] => Option[Int]

which simplifies to

toOption: Either[_, Int] => Option[Int]

Let's do another drill for LeftMatcher[String](). We have

F = ({type l[a]=Either[a, _]})#l
T = String
U = T = String

and plugging the above type instantiations into

toOption: F[T] => Option[U]

we get

toOption: (({type l[a]=Either[a, _]})#l)[String] => Option[String]

which simplifies to

toOption: Either[String, _] => Option[String]