EDIT: I reformulate the question to something simpler and less domain specific: In the following code, I'd like to implement the mplus function that combines two functions constrained by the presence of a specific field. The resulting function should be constrained by the presence of the two fields. Thanks !
import shapeless._, ops.record.Selector, record._, syntax.singleton._
def requiresIntervalKey[L <: HList](l: L)
(implicit sel: Selector.Aux[L, Witness.`"interval"`.T, Int]): Unit = {
println(sel(l))
}
def requiresPlatformField[L <: HList](l: L)
(implicit sel: Selector.Aux[L, Witness.`"platform"`.T, String]): Unit = {
println(sel(l))
}
def mplus = ??? // That is the function I'd like to implement. Eventually it will be the additive operator of a monoid
// needsBothFields: L <: HList -> (implicit) Selector.Aux[L, Witness.`"interval"`.T, Int] -> (implicit) Selector.Aux[L, Witness.`"platform"`.T, String] -> Unit
val needsBothField = mplus(requiresIntervalKey _, requiresIntervalKey _ )
// Usage
requiresIntervalKey(("interval" ->> "a string") :: HNil) // Shoudn't compile, value type is wrong
requiresIntervalKey(("wrongKey" ->> "a string") :: HNil) // Shoudn't compile, "interval" key not provided
requiresIntervalKey(("interval" ->> 42) :: HNil) // Compiles
requiresPlatformField(("platform" ->> "EU") :: HNil) // Compiles
needsBothFields(("interval" ->> 42) :: ("platform" ->> "EU") :: HNil) // Should compile
// Previous question version
Quite new to shapeless and typelevel programming, I am struggling to achieve my objective and get a clear understanding. Your help would be very appreciated!
I basically have several functions looking like the following :
trait PathGenerator[T] {
def apply(T): Set[Seq[String]]
}
val hourlyIntervalPathGenerator: PathGenerator[Interval] = ???
val constantGenerator: PathGenerator[String] = ???
// A Monoid[PathGenerator]
// an implicit class adding a '/' operator to PathGenerator
// Usage
val hourlyRegionPathGenerator = hourlyIntervalPathGenerator / constantGenerator
val paths = hourlyRegionPathGenerator(StaticIntervals.lastDay, "EU")
Now when adding more than two generators together, user has to juggle with nested tuples. Furthermore, naming the generators parameters would be very useful for generating a parser (ex: cmd-line.)
Shapeless records seem to be a great fit, so I went on and implemented the following non-working solution: https://gist.github.com/amazari/911449b55270a5871d14
Several issues arise from this non-buildable code and my shapeless misunderstanding:
L26: keys("interval")
and L35: keys(constant)
) doesn't return a value of the expected type, even tough a Witness and a selector are provided.So, are records the right tool to achieve this use case? Not shown in the snippet is a command-line parser generated from the record shape/template of a PathPattern.
How could I enforce that the record provided to a generator (simple or resulting from the combination of several) has exactly the right fields in term of names and types?
Thanks for your help!
EDIT: This series of questions and Travis Brown's answers are very relevant:
Passing a Shapeless Extensible Record to a Function Passing a Shapeless Extensible Record to a Function (continued) Passing a shapeless extensible record to a function (never ending story?
This is kind of possible with Shapeless, although not in exactly the form you're asking for. Here's a quick example:
import shapeless._, ops.record.Selector, record._, syntax.singleton._
class UseKey[K <: String, V, R](w: Witness.Aux[K])(f: V => R) extends Poly1 {
implicit def onRecord[L <: HList](implicit
sel: Selector.Aux[L, K, V]
): Case.Aux[L, R] = at[L](l => f(sel(l)))
}
class Combine[P1 <: Poly1, P2 <: Poly1, R1, R2, R](
p1: P1,
p2: P2
)(f: (R1, R2) => R) extends Poly1 {
implicit def onRecord[L <: HList](implicit
c1: p1.Case.Aux[L, R1],
c2: p2.Case.Aux[L, R2]
): Case.Aux[L, Unit] = at[L](l => f(c1(l), c2(l)))
}
class mplus[P1 <: Poly1, P2 <: Poly1](p1: P1, p2: P2)
extends Combine(p1, p2)((_: Unit, _: Unit) => ())
And then:
object requiresIntervalKey extends UseKey(Witness("interval"))(
(i: Int) => println(i)
)
object requiresPlatformField extends UseKey(Witness("platform"))(
(s: String) => println(s)
)
object needsBothFields extends mplus(requiresIntervalKey, requiresPlatformField)
(As a side note, I would have guessed that you could just write UseKey("platform")
here, but for some reason the implicit conversion that captures the witness doesn't work. Fortunately Witness("platform")
isn't too bad.)
And next to show it works:
scala> import test.illTyped
import test.illTyped
scala> illTyped("""requiresIntervalKey("interval" ->> "a string" :: HNil)""")
scala> illTyped("""requiresIntervalKey("wrongKey" ->> "a string" :: HNil)""")
scala> requiresIntervalKey("interval" ->> 42 :: HNil)
42
scala> requiresPlatformField("platform" ->> "EU" :: HNil)
EU
scala> needsBothFields("interval" ->> 42 :: "platform" ->> "EU" :: HNil)
42
EU
The reason we need Poly1
instead of regular old functions is that we can't collect the different pieces of evidence we need when combining ordinary functions—instead we need polymorphic function values (which Shapeless provides as PolyN
).
It's also worth noting that this isn't really a "monoid". A monoid instance for a type provides an operation that takes two values of that type and returns another. The mplus
here takes two operations, each of which has its own implicit requirements, and gives us another operation which has the combined requirements of both.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With