Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Records as parameters, composition

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:

  • getting the value of a field from a record (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.
  • I can't get to extract the right sub-record types to pass to underlying generators when composing two of them (L52-53.)
  • L69: somehow the provided record do not match the required type for reason I don't understand.

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?

like image 664
Alexandre Mazari Avatar asked Oct 30 '22 23:10

Alexandre Mazari


1 Answers

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.

like image 132
Travis Brown Avatar answered Nov 15 '22 06:11

Travis Brown