Shapeless find instance of Some among Nones in an Hlist of Options
Asked Answered
S

1

6

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://mcmap.net/q/1779306/-generic-poly2-folder-case-for-shapeless-hlist

https://mcmap.net/q/1779307/-shapeless-flatmap-hlist-with-option-yielding-hlist

Sisson answered 6/6, 2017 at 6:20 Comment(3)
You shouldn't use an HList, but rather a Coproduct, since that is exactly what you want: a list of possible types only one of which has a value.Tonne
Thanks! I had actually thought of that, but for some reason I could not make it work, don't remember why already... perhaps it is the way that I had built the HLists... Anyway, is there a way to convert the HList to a Coproduct? Also, while I try to work it out myself, if you might point me in the right direction it would be highly appreciated :)Sisson
If you want to do it at runtime you could do. (None :: Some(1) :: HNil).toList.find(_.isDefined). But as a co-product would be better.Ernaldus
T
5

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)
Tonne answered 6/6, 2017 at 7:56 Comment(2)
ok... I feel like a complete bumblehead, but I have stared at this for quite some time, I have even tried incorporating it into my code base, but I haven't a clue as to how I should employ it. Also, should the defs within IsOneSomeHList not have also been set as implicit? Could you please elaborate your answer with a usage example? It may be obvious to someone skilled at type level programming but I am evidently not... Lastly, just to make sure... your code will work only when the HList is explicitly typed with Somes and Nones, right?Sisson
You're completely right, I didn't take the time to fully explain. I put the implicits I had forgotten, and added an example.Tonne

© 2022 - 2024 — McMap. All rights reserved.