The general technique for doing this is type-aligned data structures. The downside is that you cannot use the built-in collections directly for this; you must design a structure that fits your needs.
You can preserve all the type information in an hlist, but all you are doing that way is shifting the problem, if I’m interpreting your explanation correctly. (Of course, if you can use the singleton types of those strings instead of their runtime values, then there is no problem there; you can do all the lookups at compile-time. I’m not going to explain that because there’s plenty written elsewhere about that.)
Scalaz 8 includes some type-aligned structures that are commonly needed. For example, c2
can be modeled directly like so (tried in Scala 2.12.4, Scalaz 8 fcd2d2b3):
scala> import scalaz.data.AList1
scala> (func1 _) :: (func2 _) :: AList1(func3 _)
res0: scalaz.data.AList1[Function1,Int,Boolean] = AList1($$Lambda$5543/1591607484@3f1325e5, $$Lambda$5544/1336807449@52e210ee, $$Lambda$5545/1147158014@74782b5a)
The type of this structure works by throwing away type information that you won’t need and keeping type information that you do. In this case, only the argument type Int
of the first function, and the return type Boolean
of the last function, are preserved. The presumption is that you don’t care about the types in between; indeed, you can’t usefully take sections of this list because you wouldn’t know the argument type and/or return type anymore. You can think of the element types as follows:
// there are existential types
type e1
type e2
// and the function types are
Int => e1, e1 => e2, e2 => Boolean
So Function1
isn’t an interesting type to connect these. But what if you passed [a, b] => a => Either[Result[A], b]
?
// there are existential types
type e1
type e2
// and the function types are
Int => Either[Result[A], e1], e1 => [Either[Result[A], e2], e2 => Either[Result[A], Boolean]
That affords some more interesting possibilities.
So here is the trouble with your string-selection idea. How do you limit the selection of functions to one that makes sense, i.e. one that takes an Int
and returns a Boolean
? You want to be able to pick func1
and func3
or all three, or func2
multiple times, but no other combination makes sense.
I don’t know exactly your goals, but here is an example of letting a particular dynamic selection goal design the datatype for me. So, like AList
, I’m fixing the input and output types to T
and R
, but at each stage, you get to pick a “branch”, which leads either to another choice or a terminator, indicating you’ve reached the goal type.
package atree
object Funcs {
def func1(a:Int): Double = a.toDouble
def func2(a:Double): Double = a * 2
def func3(a:Double): Boolean = a > 3
}
// Note the 'U', this is intended to be existential
final case class ResPair[T, U, R](next: T => U, tree: AChoiceTree[U, R])
final case class AChoiceTree[T, R](
done: Option[T =:= R],
// this existential means each `ResPair` in the map can have an
// entirely different 2nd tparam (U); accordingly, there is no way
// to tell what the Us internal to an AChoiceTree are
choose: Map[String, ResPair[T, _, R]]
)
object AChoiceTree {
/** Return the goal, or None if we weren't at an endpoint when choices
* was exhausted. You don't need a `start` value to calculate a
* function, though; you could even use a `tree` and `choices` to
* calculate a `Option[scalaz.data.AList[Function1, T, R]]`.
*
* You don't have to drive this with a `List`; you can step through
* the tree at your leisure. Just keep in mind that the internal
* tree types are existential; *none* of the internal trees of an
* `AChoiceTree[T, R]` have the same type.
*
* There are three reasons for failure: you ran out of choices
* before getting to a terminus, you got to a terminus but still
* had [valid or invalid] choices left, or you tried an invalid
* choice. You can differentiate between these as you like; I did
* not for this example.
*/
@annotation.tailrec
def interpret[T, R](tree: AChoiceTree[T, R],
choices: List[String],
start: T): Option[R] =
choices match {
case Nil => tree.done map (_(start))
case s :: ss => tree.choose.get(s) match {
case None => None
case Some(p: ResPair[T, u, R]) => // look up "variable type pattern"
interpret(p.tree, ss, p.next(start))
}
}
def finalFunction[T, R](f: T => R): ResPair[T, _, R] =
ResPair(f, AChoiceTree(terminus, Map()))
def terminus[R]: Option[R =:= R] = Some(implicitly)
import Funcs._
val tree: AChoiceTree[Int, Boolean] =
AChoiceTree(None, Map("func1" -> ResPair((func1 _),
AChoiceTree(None, Map("func3" -> finalFunction(func3 _),
"func2" -> ResPair((func2 _),
AChoiceTree(None, Map("func3" -> finalFunction(func3 _)))))))))
// 1st and 4th are only ones that should succeed
val trials = (interpret(tree, List("func1", "func3"), 3),
interpret(tree, List("func1"), 1),
interpret(tree, List("func1", "func3", "func2"), 4),
interpret(tree, List("func1", "func2", "func3"), 8))
}
scala> atree.AChoiceTree.trials
res0: (Option[Boolean], Option[Boolean], Option[Boolean], Option[Boolean]) =
(Some(false),None,None,Some(true))
Again, if you want this kind of dynamic choice strategy, the burden is placed upon you to design a datatype like AChoiceTree
with the kind of dynamism you want.