Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shapeless find instance of Some among Nones in an Hlist of Options

lets say I have the following class hierarchy:

sealed trait Animal

case class Cat(isFriendly: Boolean) extends Animal
case class Dog(color: String) extends Animal
case class Fish(isFreshWater: Boolean) extends Animal

Now I have an instance of type

Option[Cat] :: Option[Dog] :: Option[Fish] :: HNil

But there is a restriction on the instance. It can only be of one of the following forms

Some(Cat(???)) :: None :: None :: HNil

or

None :: Some(Dog(???)) :: None :: HNil

or

None :: None :: Some(Fish(???)) :: HNil

First, excuse any incoherence - it is part of a larger problem that I am trying to solve that is not yet well articulated

Second, the ??? is just my contrived place holder for real instance such as:

None :: Some(Dog(brown)) :: None :: HNil

Thing is, I am rather new to shapeless and I don't exactly know if the value of the ??? makes a difference.


Onwards to the question

Is there a way to "iterate" over the HList and extract the Some?

I understand that when speaking generically it is not possible as shown in the following two questions. But I wonder whether adding the restrictions I set above would make a difference

https://stackoverflow.com/a/28598157/101715

https://stackoverflow.com/a/29572541/101715

like image 335
Yaneeve Avatar asked Jun 06 '17 06:06

Yaneeve


1 Answers

As explained in the link you gave, such an operation is only possible on HList if your values are statically typed as Some and None, so that the compiler can do anything about it.

If you have additional information that what the type gives (here, the fact that exactly one of the options can be a Some), it means that you're using the wrong type, since types are the information you have on values at compile time. In this case, the type you should use is Coproduct:

type AnimalCoproduct = Cat :+: Dog :+: Fish :+: CNil

val dog = Coproduct[AnimalCoproduct](Dog("brown"))

Now, back to your question, assuming you know which are None and which are Some at compile time.

First, you need to check which HList have the property that they are a list of None.

trait IsNoneList[L <: HList]

object IsNoneList {
  //all values in an HNil are None, since there aren't any
  implicit val hnil: IsNoneList[HNil] = new IsNoneList[HNil] {}

  //if all the values in the tail are None, and the head is None, then all the values are None
  implicit def hcons[T <: HList: IsNoneList]: IsNoneList[None.type :: T] = new IsNoneList[None.type :: T] {}
}

So now, if there is an implicit IsNoneList[L] in scope, it means that L is an HList of None.type. Let's do the same with the property we're looking for:

trait IsOneSomeHList[L <: HList] {
  type OneSome
  def get(l: L): OneSome
}

object IsOneSomeHList {
  type Aux[L <: HList, O] = IsOneSomeHList[L] { type OneSome =  O }

  def apply[L <: HList](implicit L: IsOneSomeHList[L]) = L

  // if the tail is full of None, and the head is a Some, then the property is true
  implicit def someHead[A, T <: HList: IsNoneList]: Aux[Some[A] :: T, A] = new IsOneSomeHList[Some[A] :: T] {
    type OneSome = A
    def get(l: Some[A] :: T) = l.head.get
  }

  //if the head is None, and the tail has the property, then the HCons also has the property, with the same extraction function
  implicit def noneHead[T <: HList](implicit T: IsOneSomeHList[T]): Aux[None.type :: T, T.OneSome] = new IsOneSomeHList[None.type :: T] {
    type OneSome = T.OneSome
    override def get(l: ::[None.type, T]): T.OneSome = T.get(l.tail)
  }
}

Notice that if we have an implicit IsOneSomeHList[L] in scope, we know that L has the property we want, but we can also use this implicit to get the type and the value of the only Some in the list.

EDIT

Let's give an example:

val cat = Some(Cat(isFriendly = true)) :: None :: None :: HNil

IsOneSomeHList[Some[Cat] :: None.type :: None.type :: HNil].get(cat) == Cat(true)
like image 125
Cyrille Corpet Avatar answered Nov 02 '22 22:11

Cyrille Corpet