I'm trying to write an interface through which users should be able to pass arbitrary arithmetic functions independent of numeric type, and then helper code would bind them to appropriate backing Numeric used by the system. (The need for this is complicated, but it's a DSL design thing and the numeric type which will ultimately be used is not known at compile time, though it can be restricted to a small set.)
So the goal is to get something like:
case class numericOp(
intVersion : (Int, Int) -> Int,
floatVersion : (Float, Float) -> Float
)
def wrap(f : ???) : NumericOp = NumericOp(f[Int], f[Float])
//use site
wrap( (x, y) => x + y)
//use in engine
def applyArithmetic(arg1 : Any, arg2 : Any, op : NumericOp) : Any{
(arg1, arg2) match {
case (a : Int, b : Int) => op.intVersion(a, b)
case (a : Float, b : Float) => op.floatVersion(a, b)
case _ => throw new Exception("Invalid types") //shouldn't actually be reachable
}
The above - which obviously doesn't compile - would have wrap take a generic lambda with an un-applied type parameter, and then apply the backing types to it to create real functions.
I understand why this doesn't work - type parameter application would have to happen before runtime, i.e., before the lambda even exists. That said, the core of what I'm trying to do - describe arithmetic logic as data, then create specific implementations later once a type is available - seems like it should be possible. (My reasoning is that I could describe a complete DSL of all operations under Numeric, have users specify their math in that DSL, and then create implementations for Int/Float/Whatever later.)
Is there a way to make this work?
I very much want to keep the use site concise, and more importantly, abstracted from the backing types.
To give some more explicit detail and requirements:
- First, the user specifies their arithmetic operation in some type-agnostic way. It's fine for them to know they're dealing in some
Numerictype, but the should not have to know the specific type, or even the number of possible types. I don't know what format this input could take, thus the ??? in the code. - Their specification - call this
f- is passed to a handler function (wrap, above). This function can know the set of possible backing types, and produces an intermediate structure to store any needed type-specific data (numericOp, above) - The intermediate structure gets used in an interpreter that operates over dynamic
Anyvalues (type checking is done on the input DSL code, and casting is done when needed). This is done inapplyArithmetic, above.
To take a more temporal view of it:
- The user specifies some arithmetic logic, passing it into the
wrapfunction - The system maintainer re-arranges the set of possible backing numeric types, adding/removing them without telling the user
- The code is compiled
- During runtime, the specific values are passed to an
applyArithmeticfunction asAnys along with whatever intermediate typewrapoutput.
If you use something like
Numeric[Int]compiler will resolve it in compile time to some value and then use it. So it would be hardcoded as you suspect.The same would not be true though if you did:
Why?
is syntactic sugar for
Therefore
is syntactic sugar for
If you consistently used implicits with a type class - and often also extension methods to make it more readable:
this parametrized interface with implementation resolved for the type - deferring the specification of
T(and resolving implicit to value of known type) it's an example of something called tagless final.Alternative is to use free algebra:
The way you use it is to wrap all values in this wrapper type, and let this wrapper type "record" operations (without knowing the exact implementation) which you would later "replay" providing implementation as the last step.
EDIT:
If you want tagless final-like approach there are 2 options:
Scala 2:
Scala 3:
See demo.