scala - Shapeless case study -
in application, have bunch of components capable of rendering html:
class standardcomponent { def render: html }
they instantiated @ run-time componentdefinition
objects componentbuilder
, provides access run-time data:
class componentbuilder { def makecomponent(componentdef: componentdefinition): standardcomponent }
then there several helpers facilitate rendering of sub-components within components:
def fromcomponent(componentdef: componentdefinition)(htmlfn: html => future[html])(implicit componentbuilder: componentbuilder): future[html] def fromcomponents(componentdefs: seq[componentdefinition])(htmlfn: seq[html] => future[html])(implicit componentbuilder: componentbuilder): future[html] def fromoptionalcomponent(componentdefopt: option[componentdefinition])(htmlfn: option[html] => future[html])(implicit componentbuilder: componentbuilder): future[html] def fromcomponentmap[k](componentdefmap: map[k, componentdefinition])(htmlfn: map[k, html] => future[html])(implicit componentbuilder: componentbuilder): future[html]
the problem oftentimes, component needs utilize several of these from*
calls. although designed nestable, can turn bit of mess:
implicit val componentbuilder: componentbuilder = ??? val subcomponent: componentdefinition = ??? val subcomponents: seq[componentdefinition] = ??? val subcomponentopt: option[componentdefinition] = ??? fromcomponent(subcomponent) { html => fromcomoponents(subcomponents) { htmls => fromoptionalcomponent(subcomponentopt) { opthtml => ??? } } }
what i'd able like:
withsubcomponents( subcomponent, subcomponents, subcomponentopt ) { case (html, htmls, opthtml) => /* html, seq[html], , option[html] */ ??? }
so, want create withsubcomponents
variadic in arguments, , want create closure takes in sec argument list have argument list depends on first argument list in arity , type. ideally, takes implicit componentbuilder
, individual helpers do. that's ideal syntax, i'm open alternatives. provide examples of have far, have ideas far. feels need create hlist of coproduct, , need way tie 2 arguments together.
the first step in improving dsl can move methods implicit conversion this:
implicit class subcomponentenhancements[t](subcomponent: t)( implicit cb: componentbuilder[t]) { def fromcomponent(f: cb.htmltype => future[html]): future[html] = ??? }
note declared fromcomponent
valid each type t
has componentbuilder
defined. can see imagined componentbuilder
have htmltype
. in illustration seq[html]
, option[html]
, etc. componentbuilder
looks this:
trait componentbuilder[t] { type htmltype def render(componentdef: t): htmltype }
i imagined componentbuilder
able render component type of html
. let's declare component builders able phone call fromcomponent
method on different types.
object componentbuilder { implicit def single = new componentbuilder[componentdefinition] { type htmltype = html def render(componentdef: componentdefinition) = { // create standard component component definition val standardcomponent = new standardcomponent standardcomponent.render } } implicit def seq[t]( implicit cb: componentbuilder[t]) = new componentbuilder[seq[t]] { type htmltype = seq[cb.htmltype] def render(componentdef: seq[t]) = componentdef.map(c => cb.render(c)) } implicit def option[t]( implicit cb: componentbuilder[t]) = new componentbuilder[option[t]] { type htmltype = option[cb.htmltype] def render(componentdef: option[t]) = componentdef.map(c => cb.render(c)) } }
notice each of component builders specifies htmltype
in sync type of componentbuilder
. builders container types request component builder contents. allows nest different combinations without much effort. generalize concept further, fine.
as single
component builder, define more generically, allowing have different types of component definitions. converting them standard component done using converter
located in serveral different places (companion object of x
, companion object of converter
or separate object users need import manually).
trait converter[x] { def convert(c:x):standardcomponent } object componentdefinition { implicit val defaultconverter = new converter[componentdefinition] { def convert(c: componentdefinition):standardcomponent = ??? } } implicit def single[x](implicit converter: converter[x]) = new componentbuilder[x] { type htmltype = html def render(componentdef: x) = converter.convert(componentdef).render }
anyway, code looks following:
subcomponent fromcomponent { html => subcomponents fromcomponent { htmls => subcomponentopt fromcomponent { opthtml => ??? } } }
this looks familiar pattern, let's rename methods:
subcomponent flatmap { html => subcomponents flatmap { htmls => subcomponentopt map { opthtml => ??? } } }
note in wishful thinking space, above code not compile. if had way of making compile write following:
for { html <- subcomponent htmls <- subcomponents opthtml <- subcomponentopt } yield ???
that looks pretty amazing me, unfortunately option
, seq
have flatmap
function themselves, need hide those. next code looks clean , gives oportunity hide flatmap
, map
methods.
trait wrapper[+a] { def map[b](f:a => b):wrapper[b] def flatmap[b](f:a => wrapper[b]):wrapper[b] } implicit class htmlenhancement[t](subcomponent:t) { def html:wrapper[t] = ??? } { html <- subcomponent.html htmls <- subcomponents.html opthtml <- subcomponentopt.html } yield ???
as can see still in wishful thinking space, let's see if can fill in blanks.
case class wrapper[+a](value: a) { def map[b](f: => b) = wrapper(f(value)) def flatmap[b](f: => wrapper[b]) = f(value) } implicit class htmlenhancement[t](subcomponent: t)( implicit val cb: componentbuilder[t]) { def html: wrapper[cb.htmltype] = wrapper(cb.render(subcomponent)) }
the implementation not complicated because can utilize tools created earlier. note during wishful thinking returned wrapper[t]
while needed html, using htmltype
component builder.
to improve type inference, alter componentbuilder
slightly. alter htmltype
type fellow member type parameter.
trait componentbuilder[t, r] { def render(componentdef: t): r } implicit class htmlenhancement[t, r](subcomponent: t)( implicit val cb: componentbuilder[t, r]) { def html:wrapper[r] = wrapper(cb.render(subcomponent)) }
the different builders need alter well
object componentbuilder { implicit def single[x](implicit converter: converter[x]) = new componentbuilder[x, html] { def render(componentdef: x) = converter.convert(componentdef).render } implicit def seq[t, r]( implicit cb: componentbuilder[t, r]) = new componentbuilder[seq[t], seq[r]] { def render(componentdef: seq[t]) = componentdef.map(c => cb.render(c)) } implicit def option[t, r]( implicit cb: componentbuilder[t, r]) = new componentbuilder[option[t], option[r]] { def render(componentdef: option[t]) = componentdef.map(c => cb.render(c)) } }
the end result looks this:
val wrappedhtml = { html <- subcomponent.html htmls <- subcomponents.html opthtml <- subcomponentopt.html } yield { // interesting stuff html htmls ++ opthtml.toseq :+ html } // type of `result` `seq[html]` val result = wrappedhtml.value // or val wrapper(result) = wrappedhtml
as might have noticed, skipped future
, can add together please.
i not sure if how envisioned dsl, @ to the lowest degree gives tools create cool one.
scala shapeless