Skip to content

Instantly share code, notes, and snippets.

@caente
Last active February 13, 2017 15:51
Show Gist options
  • Save caente/77ee1bff4471bd39cdc908eab1761c9f to your computer and use it in GitHub Desktop.
Save caente/77ee1bff4471bd39cdc908eab1761c9f to your computer and use it in GitHub Desktop.
package features
import shapeless._
import ops.hlist.Selector
import syntax.std.function._
import ops.function._
import simulacrum._
import scalaz._, Scalaz._
object Main {
import utils.Find
import utils.Subset
import utils.applyContext
trait FeatureGenerator[X] {
type Context
def features(x: X, context: Context): Map[String, Int]
}
// features for String, each feature generator can process several contexts
// the implicit `Find`s are for the types it _might_ need in its internal generators
implicit def StringFeatures[L <: HList, F](
implicit
si: Find[L, Int],
ss: Find[L, String],
sd: Find[L, Double]
) = new FeatureGenerator[String] {
type Context = L
private def featureGenerator1(x: String): (Int, Double) => Map[String, Int] =
(i, d) =>
Map("ave" -> (d.toInt + i))
private def featureGenerator2(x: String): (Int) => Map[String, Int] =
i =>
Map("dev" -> i)
private def featureGenerator3(x: String): (String, Int) => Map[String, Int] =
(s, i) =>
Map("sum" -> (s.size + i + x.size))
//is possible to have generators with the same context, they will all be used
private def featureGenerator3_1(x: String): (String, Int) => Map[String, Int] =
(s, i) =>
Map("sum2" -> (s.size + i * 2 + x.size))
private def featureGenerator4(x: String): () => Map[String, Int] =
() =>
Map("size" -> (x.size))
def features(x: String, context: Context): Map[String, Int] = {
applyContext(context)(featureGenerator1(x)) ++
applyContext(context)(featureGenerator2(x)) ++
applyContext(context)(featureGenerator3(x)) ++
applyContext(context)(featureGenerator3_1(x)) ++
applyContext(context)(featureGenerator4(x))
}
}
implicit def LabelessFeatures[L <: HList, F](
implicit
si: Find[L, Int],
ss: Find[L, String]
) = new FeatureGenerator[FeatureGenerator.Labeless.type] {
type Context = L
private def featureGenerator1: (Int) => Map[String, Int] =
i =>
Map("dev" -> i)
private def featureGenerator2: (String, Int) => Map[String, Int] =
(s, i) =>
Map("sum" -> (s.size + i))
private def featureGenerator3: () => Map[String, Int] =
() =>
Map("size" -> 0)
def features(x: FeatureGenerator.Labeless.type, context: Context): Map[String, Int] = {
applyContext(context)(featureGenerator1) ++
applyContext(context)(featureGenerator2) ++
applyContext(context)(featureGenerator3)
}
}
object FeatureGenerator {
implicit class Ops[X](x: X) {
//instead of Product, we could do C <: BaseContext,
//to be more strict regarding the context possible values
def features[C <: Product, L <: HList](context: C)(
implicit
gen: Generic.Aux[C, L],
fg: FeatureGenerator[X] { type Context = L }
) =
fg.features(x, gen.to(context))
def features[C](context: C)(
implicit
fg: FeatureGenerator[X] { type Context = C :: HNil }
) =
fg.features(x, context :: HNil)
}
case object Labeless
case class NoContext()
def emptyContext: NoContext = NoContext()
}
def main(args: Array[String]): Unit = {
import FeatureGenerator.Ops
// it works with tuples and case classes
assert(
"hi".features(FeatureGenerator.emptyContext) === Map("size" -> 2)
)
assert(
"hi".features(1) === Map("dev" -> 1, "size" -> 2)
)
assert(
"hi".features(1, 2d) === Map("ave" -> 3, "dev" -> 1, "size" -> 2)
)
assert(
"hi".features("a", 1) === Map("sum" -> 4, "sum2" -> 5, "dev" -> 1, "size" -> 2)
)
// the order of the arguments doesn't matter
assert(
"hi".features(2d, 1, "a") === Map("sum" -> 4, "sum2" -> 5, "dev" -> 1, "ave" -> 3, "size" -> 2)
)
// feature generators with no X
assert(
FeatureGenerator.Labeless.features(FeatureGenerator.emptyContext) === Map("size" -> 0)
)
assert(
FeatureGenerator.Labeless.features(1) === Map("dev" -> 1, "size" -> 0)
)
assert(
FeatureGenerator.Labeless.features("a", 1) === Map("sum" -> 2, "dev" -> 1, "size" -> 0)
)
// the order of the arguments doesn't matter
assert(
FeatureGenerator.Labeless.features(1, "a") === Map("sum" -> 2, "dev" -> 1, "size" -> 0)
)
}
}
package features
import shapeless._
import ops.hlist.Selector
import syntax.std.function._
import ops.function._
import simulacrum._
import scalaz._, Scalaz._
object utils {
def applyContext[L <: HList, Args <: HList, F, R](c: L)(f: F)(
implicit
fp: FnToProduct.Aux[F, Args => R],
subset: Subset.Aux[L, Args],
mr: Monoid[R]
): R =
subset(c).map {
args =>
f.toProduct(args)
}.getOrElse(mr.zero)
trait Find[L <: HList, A] {
def find(l: L): Option[A]
}
object Find {
def apply[A, L <: HList](implicit f: Find[L, A]) = f
implicit class Ops[L <: HList](l: L) {
def find[A](implicit f: Find[L, A]) = f.find(l)
}
implicit def hconsFound[A, H, T <: HList](implicit ev: H =:= A) = new Find[H :: T, A] {
def find(l: H :: T) = Some(l.head)
}
implicit def hconsNotFound[A, H, T <: HList](implicit f: Find[T, A]) = new Find[H :: T, A] {
def find(l: H :: T) = f.find(l.tail)
}
implicit def hnil[A] = new Find[HNil, A] {
def find(l: HNil) = None
}
}
import Find.Ops
trait Subset[L <: HList] {
type Out <: HList
def apply(l: L): Option[Out]
}
object Subset {
type Aux[L <: HList, S <: HList] = Subset[L] { type Out = S }
def apply[L <: HList, S <: HList](implicit f: Subset.Aux[L, S]) = f
implicit def hcons[L <: HList, H, T <: HList](
implicit
find: Find[L, H],
subset: Lazy[Subset.Aux[L, T]]
) = new Subset[L] {
type Out = H :: T
def apply(l: L) =
(l.find[H] |@| subset.value(l)) {
(h, t) => h :: t
}
}
implicit def hnil[L <: HList]: Subset.Aux[L, HNil] = new Subset[L] {
type Out = HNil
def apply(l: L) = Some(HNil)
}
}
}
@caente
Copy link
Author

caente commented Feb 12, 2017

some comments:
1 - if two feature generators produce the same keys in the map, the latest one will replace the previous, but that's an issue with Map
2 - for now the only way to provide context of one element is by wrapping it up in a case class
3 - I'd like to add tests to the typeclasses

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment