Scala Dynamics: Ability to add dynamic methods? - scala

I know I can add dynamic "fields" like this:
import collection.mutable
class DynamicType extends Dynamic {
private val fields = mutable.Map.empty[String, Any].withDefault {key => throw new NoSuchFieldError(key)}
def selectDynamic(key: String) = fields(key)
def updateDynamic(key: String)(value: Any) = fields(key) = value
def applyDynamic(key: String)(args: Any*) = fields(key)
}
I can then do stuff like this:
val foo = new DynamicType
foo.age = 23
foo.name = "Rick"
But, I want to extend this one step farther and add dynamic methods e.g:
foo.greet = (name: String) => s"Nice to meet you $name, my name is ${this.name}"
foo.greet("Nat"); //should return "Nice to meet you Nat, my name is Rick"
I tried storing all methods in separate map in updateDynamic but I could not figure out a generic way to handle the arity problem. So is there a way to use Macros + Dynamics to have something like this?
EDIT:
Based on #Petr Pudlak's answer, I tried implementing something like this:
import collection.mutable
import DynamicType._
/**
* An useful dynamic type that let's you add/delete fields and methods during runtime to a structure
*/
class DynamicType extends Dynamic {
private val fields = mutable.Map.empty[String, Any] withDefault { key => throw new NoSuchFieldError(key) }
private val methods = mutable.Map.empty[String, GenFn] withDefault { key => throw new NoSuchMethodError(key) }
def selectDynamic(key: String) = fields(key)
def updateDynamic(key: String)(value: Any) = value match {
case fn0: Function0[Any] => methods(key) = {case Seq() => fn0()}
case fn1: Function1[Any, Any] => methods(key) = fn1
case fn2: Function2[Any, Any, Any] => methods(key) = fn2
case _ => fields(key) = value
}
def applyDynamic(key: String)(args: Any*) = methods(key)(args)
/**
* Deletes a field (methods are fields too)
* #return the old field value
*/
def delete(key: String) = fields.remove(key)
//todo: export/print to json
}
object DynamicType {
import reflect.ClassTag
type GenFn = PartialFunction[Seq[Any],Any]
implicit def toGenFn1[A: ClassTag](f: (A) => Any): GenFn = { case Seq(a: A) => f(a) }
implicit def toGenFn2[A: ClassTag, B: ClassTag](f: (A, B) => Any): GenFn = { case Seq(a: A, b: B) => f(a, b) }
// todo: generalize to 22-args
}
Full code here
1) It correctly handles fields vs methods (even 0-args) but is quite verbose (currently works upto 2 arg methods only). Is there anyway to simplify my code?
2) Is there anyway to support dynamic method overloading (e.g. adding 2 dynamic methods with different signatures?) If I can get the signature of the function, I can use that as a key in my methods map.

In order to do that, we have to solve two problems:
Unify somehow all possible functions into one data type.
Deal with the fact that the dynamic select can return both values and functions and we can't determine it in advance.
Here is one possibility:
First, let's define the most generic function type: Take any number of any arguments and produce a result, or fail, if the number or types of the arguments dont' match:
type GenFn = PartialFunction[Seq[Any],Any]
Now we create a dynamic type where everything is GenFn:
class DynamicType extends Dynamic {
import collection.mutable
private val fields =
mutable.Map.empty[String,GenFn]
.withDefault{ key => throw new NoSuchFieldError(key) }
def selectDynamic(key: String) = fields(key)
def updateDynamic(key: String)(value: GenFn) = fields(key) = value
def applyDynamic(key: String)(args: Any*) = fields(key)(args);
}
Next, let's create implicit conversions that convert functions of different arities into this type:
import scala.reflect.ClassTag
implicit def toGenFn0(f: => Any): GenFn =
{ case Seq() => f; }
implicit def toGenFn1[A: ClassTag](f: (A) => Any): GenFn =
{ case Seq(x1: A) => f(x1); }
implicit def toGenFn2[A: ClassTag,B: ClassTag](f: (A,B) => Any): GenFn =
{ case Seq(x1: A, x2: B) => f(x1, x2); }
// ... other arities ...
Each conversion converts a function (or a value) to a GenFn - a partial function, which fails if it's given a wrong number/types of arguments.
We use ClassTag in order to be able to match the correct types of arguments. Note that we treat values as functions of zero arity. This way we deal with 2. at the cost of using retrieving values by giving zero arguments as in name().
Finally, we can do something like:
val foo = new DynamicType
foo.name = "Rick"
foo.greet = (name: String) =>
s"Nice to meet you $name, my name is ${foo.name()}"
println(foo.greet("Nat"));
In order to support method overloading, all we need is to chain PartialFunctions. This canbe accomplished as
def updateDynamic(key: String)(value: GenFn) =
fields.get(key) match {
case None => fields(key) = value
case Some(f) => fields(key) = f.orElse(value);
}
(note that it isn't thread safe). Then we can call something like
val foo = new DynamicType
foo.name = "Rick"
foo.greet = (name: String)
=> s"Nice to meet you $name, my name is ${foo.name()}"
foo.greet = (firstName: String, surname: String)
=> s"Nice to meet you $firstName $surname, my name is ${foo.name()}"
println(foo.greet("Nat"));
println(foo.greet("Nat Smith"));
Note that this solution works a bit differently than the standard method overloading. Here it depends on the order in which functions are added. If a more general function is added first, the more specific one will never be invoked. So always add more specific functions first.
It will be probably more difficult the way you've done it, because it seems you don't distinguish the types of functions (like my toGenFn... methods do), so if a function gets wrong arguments, it'll just throw an exception instead of passing them to the next in line. But it should work with functions with different number of arguments.
I don't think it's possible to avoid the verbosity caused by examining functions of various arguments, but I don't think it really matters. This is just a one-time work, the clients of DynamicType aren't affected by it.

Related

Changing implicit value in scala response of nested object

I have a controller
def getCars(notation: Option[Boolean] = Some(false)) = identified.auth(hasOceanScope).async { implicit request =>
carService.getCars().map {
case Seq() => Response.NotFound
case cars => Response.Ok(cars)
}
}
Car case class looks like this:
case class Car(
name: String,
createdAt: LocalDateTimeOffset,
wheels: Seq[Wheel]
)
object Car{
implicit val wheelFormat = Wheel.format
implicit def toOffset(date: LocalDateTime): LocalDateTimeOffset = LocalDateTimeOffset.apply(date)
implicit val format = Json.format[Car]
case class Wheel(
name: String,
createdAt: LocalDateTimeOffset
)
object Wheel{
implicit val format = Json.format[Wheel]
implicit def toOffset(date: LocalDateTime): LocalDateTimeWithOffset = LocalDateTimeWithOffset.apply(date)
)
When notation query parameter is true -> want to return createdAt Car object and Wheel object field with notation => 2022-10-22T00:00:00#1
When notation query parameter is false -> want to return createdAt Car object and Wheel object field without notation => 2022-10-22T00:00:00
That is why I have create two formats in LocalDateTimeOffset object
case class LocalDateTimeWithOffset(dt: LocalDateTime, offset: Int) {
val localDateTimeWithOffsetReads: Reads[LocalDateTimeWithOffset] = Reads.of[String].flatMap {
str => if (str.contains("#")) {
val (dt, offset) = str.splitAt(str.indexOf("#"))
Reads.pure(LocalDateTimeWithOffset(LocalDateTime.parse(dt), offset.drop(1).toInt))
} else {
Reads.pure(LocalDateTimeWithOffset(LocalDateTime.parse(str), 1))
}
}
val localDateTimeWithOffsetWrites: Writes[LocalDateTimeWithOffset] = new Writes[LocalDateTimeWithOffset] {
override def writes(a: LocalDateTimeWithOffset): JsValue = JsString(a.dt.format(dateTimeUTCFormatter) + s"#${a.offset}")
}
val localDateTimeWithOffsetWritesOff: Writes[LocalDateTimeWithOffset] = new Writes[LocalDateTimeWithOffset] {
override def writes(a: LocalDateTimeWithOffset): JsValue = JsString(a.dt.format(dateTimeUTCFormatter))
}
val localDateTimeWithoutOffsetFormat: Format[LocalDateTimeWithOffset] = Format(localDateTimeWithOffsetReads, localDateTimeWithOffsetWritesOff)
val localDateTimeWithOffsetFormat: Format[LocalDateTimeWithOffset] = Format(localDateTimeWithOffsetReads, localDateTimeWithOffsetWrites)
implicit var format: Format[LocalDateTimeWithOffset] = localDateTimeWithoutOffsetFormat
}
But how can I use two different formats from controller based on notation query parameter value?
Well just looking at your question's title, changing implicit value is not something you would see Scala developers do, because compiler is responsible to lookup for implicit values, and you would definitely want to avoid ambiguous implicits found error. instead, you see developers using something so called type class instance constructor or something similar. This is how it works in your case:
Assuming you have a class A, which can be formatted to/from Json in many ways:
case class A(field1: String) // could have more fields
object A {
val formatFirstApproach: Format[A] = ???
val formatSecondApproach: Format[A] = ???
// note that the above instances are not implicit
def getFormat(somePredicate: Boolean): Format[A] = {
// input parameters can be anything, these are the parameters you need,
// in order to be able to decide which instance to return
if (somePredicate) formatFirstApproach else formatSecondApproach
}
}
And then, given a class B which has an instance variable of type A in it, you can use the type class instance constructor:
case class B(a: A, field2: Int)
object B {
// this is the type class instance constructor, since it constructs an instance of a type class (Format in this case)
implicit def format(implicit aFormatter: Format[A]): Format[B] = Json.format
}
And the thing is, you probably would not care about the serialization unless in the controller layer, so in the controller layer you can do:
def someApi(flag: Boolean) = Action async { req =>
implicit val aFormatter = A.getFormat(flag) // that's it, you don't need to mention anything more anywhere
businessLogic().map {
case Seq() => Response.NotFound
case cars => Response.Ok(Json.toJson(cars))
}
}

What's the best way to store different types in a collection

I have a generic class:
class GenericType[T] {
def func(t: T) = ???
}
I need to implement a function that takes a List[String] and outputs the corresponding GenericType[T]. For example, if a client passes in List("SomeType1", "SomeType2"), the function should return List(GenericType[SomeType1], GenericType[SomeType2]). Basically, there's a string that maps to a type.
I don't find a good way to represent the return type for such function. Seq[GenericType[_]] as the return type can be an option but it requires the client to cast it into corresponding subclasses to invoke func as the type info is lost.
Alternatively, a case class can be used but this is not flexible as I need to modify the case class every time a new subclass is added.
case class (s1: Option[GenericType[SomeType1]] = None, s2: Option[SomeType2] = None, ...)
I'm curious what's a good way to represent the return type?
The easiest is
def stringToGenericType(s: String): GenericType[_] = s match {
case "SomeType1" => new GenericType[SomeType1]
case "SomeType2" => new GenericType[SomeType2]
}
GenericType[_] (or GenericType[Any] if you make GenericType covariant: class GenericType[+T]) would be an honest return type. You can't know at compile time which specific GenericType[...] you return based on a runtime string. Anyway you can't avoid casting somewhere because you can't guarantee the type statically. Anyway this kind of programming can't be type-safe.
Since we're doing a choice based on a runtime string, compile-time techniques (macros, implicits, Shapeless) are off the table. (*)
Actually, currently you don't need even runtime reflection. Your class GenericType[T] and method func in it seem not to do anything at runtime differently depending on type T. GenericType[SomeType1] and GenericType[SomeType2] are just GenericType[_] at runtime. So even the following implementation is possible
def stringToGenericType(s: String): GenericType[_] = new GenericType[Any]
Another situation would be if you created instances of different classes
class GenericType1[T]
class GenericType2[T]
import scala.reflect.runtime
import scala.reflect.runtime.universe._
val runtimeMirror = runtime.currentMirror
def stringToGenericTypeX(s: String): Any = {
val classSymbol = runtimeMirror.staticClass(s)
val constructorSymbol = classSymbol.typeSignature.decl(termNames.CONSTRUCTOR).asMethod
runtimeMirror.reflectClass(classSymbol).reflectConstructor(constructorSymbol).apply()
}
or you called different methods
class GenericType[T] {
def func1(t: T) = ???
def func2(t: T) = ???
}
def callFuncX(methodName: String, t: Any) = {
val classSymbol = runtimeMirror.classSymbol(classOf[GenericType[_]])
val methodSymbol = classSymbol.typeSignature.decl(TermName(methodName)).asMethod
runtimeMirror.reflect(new GenericType[Any]).reflectMethod(methodSymbol).apply(t)
}
or something behaved at runtime differently depending on type T
class GenericType[T: ClassTag] {
def func(t: T) = println(classTag[T].runtimeClass)
}
import scala.tools.reflect.ToolBox
val toolbox = runtimeMirror.mkToolBox()
def stringToGenericType(s: String): GenericType[_] = {
toolbox.eval(q"new GenericType[${TypeName(s)}]").asInstanceOf[GenericType[_]]
}
(*) Well, actually the first pattern matching (as I wrote it) can be automated with a macro
// in a different subproject
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
def stringToGenericType(s: String): GenericType[_] = macro stringToGenericTypeImpl
def stringToGenericTypeImpl(c: blackbox.Context)(s: c.Tree): c.Tree = {
import c.universe._
val cases = List("SomeType1", "SomeType2").map(str =>
cq"$str => new GenericType[${TypeName(str)}]"
)
q"$s match { case ..$cases }"
}
val s = "SomeTime1"
stringToGenericType(s)
// scalacOptions += "-Ymacro-debug-lite"
//s match {
// case "SomeType1" => new GenericType[SomeType1]()
// case "SomeType2" => new GenericType[SomeType2]()
//}

How to return subtype of a generic trait in scala?

I am trying to create a factory pattern, and the return type is a parametrized trait, and the actual return type will be a subtype of that trait, but I do not know the generic type beforehand, so I don't know how to specify the return type. A simplified version is like this:
trait Mapper[T] {
def createMap(a: T, b: T): T
}
class MapperA extends Mapper[String]{
override def createMap(a: String, b: String): String = {
a + b
}
}
class MapperB extends Mapper[Int]{
override def createMap(a: Int, b: Int): Int = {
a + b + b + b
}
}
def factory(typeOfMapper: String): Mapper = {
typeOfMapper match {
case "mapperA" => new MapperA()
case "mapperB" => new MapperB()
}
}
val mapper = factory("mapperA")
This will give me and error of
trait Mapper takes type parameters
but I do not know the generic type beforehand. What should the return type be for the factory method here?
Well, you could return Mapper[_] ... that will compile, but isn't really very useful, as pointed out in the comments: you won't be able to use the returned value in any meaningful way, because actual type of create will be unknown.
If different instances of your mapper will always have different type parameters (at least, within the same scope), then a neat solution would be using implicits:
object Mapper {
implicit def strMapper: Mapper[String] = new MapperA
implicit def intMapper: Mapper[Int] = new MapperB
}
Then everywhere you have these visible, you can just do:
val m1: Mapper[String] = implicitly[Mapper[String]]
val m2: Mapper[Int] = implicitly[Mapper[Int]]
You could also write your factory function (though, I am not sure why you'd want to) like this:
def mapperFactory[T: Mapper] = implicitly[Mapper[T]]
and use it like this:
val m: Mapper[String] = mapperFactory
or like this
def intMapper = mapperFactory[Int]
If you want different mappers for the same type parameter, it's basically the same idea, except it does't look as neat without implicits. The key is different factories for different types:
class Mapper {
def str(`type`: String): Mapper[String] = `type` match {
case "foo" => FooMapper()
case "bar" => BarMapper()
}
def int(`type`: String): Mapper[Int] = `type` match {
case "baz" => BazMapper()
case "bak" => BakMapper()
}
}
val fooMapper = Mapper.str("foo")
val bakMapper = Mapper.int("bak")

Scala Design using reflection, implicit and generics

I have different sources and corresponding parameters
Source1, Source2, Source3
Parameter1, Parameter2, Parameter3,
Source: is trait (can be changed)
trait Source[T] {
def get(Parameter)(implicit c: Context): MyData[T]
}
Parameter is also a trait
trait Parameter
I have different OutputType class: T1, T2, T3
I need output as: MyData[OutputType]
Fixed API signature (changes to the signature not quite preferable):
val data1: MyData[T1] = MyAPI.get[T1](Parameter1("a", "b")) // this should give MyData from Source1 of type T1
val data2: MyData[T2] = MyAPI.get[T2](Parameter3(123)) // this should give MyData from Source3 of type T2
Some source supports some output types (say T1, T2), but some may not.
What I did:
I tried using scala reflection typeTag to determine the type at runtime, but since return type will be MyData[T], and is in contra-variant position, it wont know the actual return type. (Why does TypeTag not work for return types?)
e.g.
object MyAPI {
get[T: TypeTag](p: Parameter)(implicit c: Context): MyData[T] = {}
}
I also tried using type-class pattern. Scala TypeTag Reflection returning type T
I can work with different OutputType creating implicit val for each, but would only work for single Source1. I can't manage to work for all sources.
I was trying to do:
object MyAPI {
get[T: SomeConverter](p: Parameter)(implicit c: Context): MyData[T] = {
p match {
case Parameter1 => Source1[T].read(p.asInstanceOf(Parameter1)
case Parameter2 => Source2[T].read(p.asInstanceOf(Parameter2)
}
}
}
Disclaimer: I think I figured out what you want. I'm also learning to design type-safe APIs, so here's one.
Provided variant uses implicits. You have to manually establish mapping between parameter types and results they yield, which may or may not include sources. It does not work on runtime, however, so I also removed common trait Parameter. It also does not impose any restrictions on the Sources at all.
It also "looks" the way you wanted it to look, but it's not exactly that.
case class User(id: Int) // Example result type
// Notice I totally removed any and all relation between different parameter types and sources
// We will rebuild those relations later using implicits
object Param1
case class Param2(id: Int)
case class Param3(key: String, filter: Option[String])
// these objects have kinda different APIs. We will unify them.
// I'm not using MyData[T] because it's completely irrelevant. Types here are Int, User and String
object Source1 {
def getInt = 42
}
object Source2 {
def addFoo(id: Int): Int = id + 0xF00
def getUser(id: Int) = User(id)
}
object Source3 {
def getGoodInt = 0xC0FFEE
}
// Finally, our dark implicit magic starts
// This type will provide a way to give requested result for provided parameter
// and sealedness will prevent user from adding more sources - remove if not needed
sealed trait CanGive[Param, Result] {
def apply(p: Param): Result
}
// Scala will look for implicit CanGive-s in companion object
object CanGive {
private def wrap[P, R](fn: P => R): P CanGive R =
new (P CanGive R) {
override def apply(p: P): R = fn(p)
}
// there three show how you can pass your Context here. I'm using DummyImplicits here as placeholders
implicit def param1ToInt(implicit source: DummyImplicit): CanGive[Param1.type, Int] =
wrap((p: Param1.type) => Source1.getInt)
implicit def param2ToInt(implicit source: DummyImplicit): CanGive[Param2, Int] =
wrap((p: Param2) => Source2.addFoo(p.id))
implicit def param2ToUser(implicit source: DummyImplicit): CanGive[Param2, User] =
wrap((p: Param2) => Source2.getUser(p.id))
implicit val param3ToInt: CanGive[Param3, Int] = wrap((p: Param3) => Source3.getGoodInt)
// This one is completely ad-hoc and doesn't even use the Source3, only parameter
implicit val param3ToString: CanGive[Param3, String] = wrap((p: Param3) => p.filter.map(p.key + ":" + _).getOrElse(p.key))
}
object MyApi {
// We need a get method with two generic parameters: Result type and Parameter type
// We can "curry" type parameters using intermediate class and give it syntax of a function
// by implementing apply method
def get[T] = new _GetImpl[T]
class _GetImpl[Result] {
def apply[Param](p: Param)(implicit ev: Param CanGive Result): Result = ev(p)
}
}
MyApi.get[Int](Param1) // 42: Int
MyApi.get[Int](Param2(5)) // 3845: Int
MyApi.get[User](Param2(1)) // User(1): User
MyApi.get[Int](Param3("Foo", None)) // 12648430: Int
MyApi.get[String](Param3("Text", Some(" read it"))) // Text: read it: String
// The following block doesn't compile
//MyApi.get[User](Param1) // Implicit not found
//MyApi.get[String](Param1) // Implicit not found
//MyApi.get[User](Param3("Slevin", None)) // Implicit not found
//MyApi.get[System](Param2(1)) // Same. Unrelated requested types won't work either
object Main extends App {
sealed trait Parameter
case class Parameter1(n: Int) extends Parameter with Source[Int] {
override def get(p: Parameter): MyData[Int] = MyData(n)
}
case class Parameter2(s: String) extends Parameter with Source[String] {
override def get(p: Parameter): MyData[String] = MyData(s)
}
case class MyData[T](t: T)
trait Source[T] {
def get(p: Parameter): MyData[T]
}
object MyAPI {
def get[T](p: Parameter with Source[T]): MyData[T] = p match {
case p1: Parameter1 => p1.get(p)
case p2: Parameter2 => p2.get(p)
}
}
val data1: MyData[Int] = MyAPI.get(Parameter1(15)) // this should give MyData from Source1 of type T1
val data2: MyData[String] = MyAPI.get(Parameter2("Hello World")) // this should give MyData from Source3 of type T2
println(data1)
println(data2)
}
Does this do what you want?
ScalaFiddle: https://scalafiddle.io/sf/FrjJz75/0

Same method, different argument types, implemented in a single go

I have a (Java) class with operations like this:
abstract class Holder {
def set(i: Int): Unit
def set(s: String): Unit
def set(b: Boolean): Unit
...
}
Essentially, the all perform the same task, but just take different argument types. I would love to create a generic Accessor[T] that performs something like this:
class Accessor[T](holder: Holder) {
def set(value: T) { holder.set(value) }
}
... but that gives:
<console>:16: error: overloaded method value set with alternatives:
(s: String)Unit <and>
(i: Int)Unit
(b: Boolean)Unit
cannot be applied to (T)
def set(value: T) { holder.set(value) }
Is there any way out?
Use reflection.
class Setter(obj: AnyRef) {
val clazz = obj.getClass
def set[T : Manifest](v: T): Boolean = try {
val paramType = manifest[T].erasure
val method = clazz.getMethod("set", paramType)
method.invoke(obj, v.asInstanceOf[AnyRef])
true
} catch {
case ex => false
}
}
val holder = ..
val setter = new Setter(holder)
setter.set(5) // returns true
setter.set(1.0) // double not accepted, returns false
There was an experimental shortcut for that in Scala, but it got removed before 2.8.0 was released.
I think matching should work nicely
def set(value: T) {
value match {
case s: String => holder.set(s)
case i: Int => holder.set(i)
case b: Boolean => holder.set(b)
}
}
I don't fully understand your use case, but one thing that you might try doing--if performance is not of utmost importance--is creating a wrapper class that converts to a universal form for you, and then have all your methods take that wrapper class (with appropriate implicit conversions in place). For example:
class Wrap(val data: String)
implicit def wrapString(s: String) = new Wrap(s)
implicit def wrapBoolean(b: Boolean) = if (b) new Wrap("T") else new Wrap("F")
implicit def wrapLong(l: Long) = new Wrap(l.toString+"L")
class User {
private[this] var myData = ""
def set(w: Wrap) { println("Setting to "+w.data); myData = w.data }
}
val u = new User
u.set(true)
u.set(50L)
u.set(50) // Int gets upconverted to Long for free, so this works
u.set("Fish")
// u.set(3.14159) // This is a type mismatch
This is a little bit like taking an Any except that you can restrict the types however you like and specify the conversion into whatever universal representation you have in mind. However, if there does not exist a universal form, then I'm not sure in what sense that you mean the code is doing the same thing each time. (Maybe you mean that you can conceive of a macro (or another program) that would generate the code automatically--Scala doesn't have that support built in, but you can of course write a Scala program that produces Scala code.)
Looking back at the results gathered so far, there are a couple of solutions suggested:
Use pattern matching (leads to fragmentation of the different strategies of dealing with the different parameter types)
Use reflection (to expensive for something that ideally should be super fast)
... and adding the one that I eventually ended up implementing: write an adapter per type of parameter.
To be a little more precise, the whole exercise was about writing a wrapper around Kyoto Cabinet. Kyoto Cabinet has methods for associating byte array keys with byte array values and String keys with String values. And then it basically replicates most of the operations for dealing with keys and values for both byte array as well as Strings.
In order to create a Map wrapper around Kyoto Cabinet's DB class, I defined a trait TypedDBOperations[T], with T being the type of parameter, and had it implemented twice. If I now construct a Map[Array[Byte], Array[Byte]], an implicit conversion will automatically assign it the proper instane of TypedDBOperations, calling the Array[Byte] based operations of the DB class.
This is the trait that I have been talking about:
trait TypedDBOperations[K,V] {
def get(db: DB, key: K): V
def set(db: DB, key: K, value: V): Boolean
def remove(db: DB, key: K): Boolean
def get(cursor: Cursor): (K, V)
}
And these are the implementations for both type of key value combinations:
implicit object StringDBOperations extends TypedDBOperations[String] {
def get(cursor: Cursor) = {
val Array(a, b) = cursor.get_str(false)
(a, b)
}
def remove(db: DB, key: String) = db.remove(key)
def set(db: DB, key: String, value: String) = db.set(key, value)
def get(db: DB, key: String) = db.get(key)
}
implicit object ByteArrayOperations extends TypedDBOperations[Array[Byte]] {
def get(cursor: Cursor) = {
val Array(a, b) = cursor.get(false)
(a, b)
}
def remove(db: DB, key: Array[Byte]) = db.remove(key)
def set(db: DB, key: Array[Byte], value: Array[Byte]) = db.set(key, value)
def get(db: DB, key: Array[Byte]) = db.get(key)
}
Not the most satisfying solution ever, but it gets the job done. Again, note there still is quite a bit of duplication, but it seems there's no way to get rid of it.