Evidence-preserving LUB constraint for HList
Asked Answered
R

1

9

I think I need a HList that is constrained to have all of its elements being a subtype of a certain type. LUBConstraint seems to be what I want, and indeed it does constrain the construction of such a HList - but I can't see how to get the evidence out again, so that I can map (actually, traverse, because it needs to be monadic) over the HList and call a method (that exists in the LUB type) on each of the elements.

In addition, I want the type of the HList resulting from the traverse operation to be exactly the same type as the type of the input HList.

The use case is a kind of functional "listener list" - all of the elements of the HList are "listeners" which must be notified of "events", accept or reject them, and return new versions of themselves with updated "internal state". If that was all I needed, then I could just use an ordinary immutable Scala collection. But I also want direct typed access to individual elements without using asInstanceOf - hence the motivation for trying to use HList.

Rampageous answered 19/5, 2015 at 9:40 Comment(1)
I'm not sure if I understood correctly, but if you traverse the list modifying each element with the result of a method call on the LUB type, then to get the result with the same type you would surely need to call asInstanceOf on each element.Shulman
G
9

In general if you have some operation that you want to perform on all of the elements in an HList, you'll want to map a polymorphic function value over the HList. For example, suppose I have the following setup:

trait Listener[L <: Listener[L]] {
  def handle(s: String): Option[L]
}

class FooListener extends Listener[FooListener] {
  def handle(s: String) =
    if (s.size == 3) Some(this) else None
}

class BarListener extends Listener[BarListener ]{
  def handle(s: String) = Some(this)
}

import shapeless._

val listeners = new FooListener :: new BarListener :: HNil

Now I want to send a String to each of these listeners and gather the results. If I just wanted to send a fixed value, this would be easy:

object event123 extends Poly1 {
  implicit def listener[L <: Listener[L]] = at[L](_.handle("123"))
}

val result = listeners.map(event123)

Which will be appropriately typed as an Option[FooListener] :: Option[BarListener] :: HNil. If I'm using shapeless-contrib, I can sequence this HList:

import scalaz._, Scalaz._, shapeless.contrib.scalaz._

val sequenced: Option[FooListener :: BarListener :: HNil] = sequence(result)

Or just use traverse:

traverse(listeners)(event123)

Unfortunately there are restrictions on how polymorphic function values can be defined that mean that partial application isn't convenient, so if we don't know the String we're sending at compile time, this is a lot more complicated:

object event extends Poly1 {
  implicit def listener[L <: Listener[L]] = at[(L, String)] {
    case (listener, string) => listener.handle(string)
  }
}

traverse(listeners.zip(listeners.mapConst("123")))(event)

Where we've zipped the elements with the string and then mapped a polymorphic function that takes tuples over the result. There are other ways you could do this using more or less the same approach, but none of them are terribly clear.

A completely different approach is just to skip the polymorphic function values and define a new type class:

trait Notifiable[L <: HList] {
  def tell(s: String)(l: L): Option[L]
}

object Notifiable {
  implicit val hnilNotifiable: Notifiable[HNil] = new Notifiable[HNil] {
    def tell(s: String)(l: HNil) = Some(HNil)
  }

  implicit def hconsNotifiable[H <: Listener[H], T <: HList](implicit
    tn: Notifiable[T]
  ): Notifiable[H :: T] = new Notifiable[H :: T] {
    def tell(s: String)(l: H :: T) = for {
      h <- l.head.handle(s)
      t <- tn.tell(s)(l.tail)
    } yield h :: t
  }
}

def tell[L <: HList: Notifiable](s: String)(l: L) =
  implicitly[Notifiable[L]].tell(s)(l)

And then:

val sequenced: Option[FooListener :: BarListener :: HNil] =
  tell("123")(listeners)

This is less generic (it only works on Option, not arbitrary applicatives), but it doesn't require an extra dependency for sequencing, and it's arguably a little less muddled than jumping through hoops to partially apply a polymorphic function value because of weird limitations of the compiler.

Grandparent answered 19/5, 2015 at 13:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.