Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simulate partial type parameter inference with implicits?

Tags:

scala

I'm making a simple dependency injection framework, for constructor injection, in Scala. The idea is that objects that are DI'd put their required services in their constructor like regular parameters, and implement a typeclass that determines which of their arguments are taken from the container and which are passed by the user on instantiation.

So, it should look something like:

trait Container {
  private singletons: Map[Class, AnyRef]
  def getSingleton[T: Manifest] =
    singletons(implicitly[Manifest[T]].erasure).asInstanceOf[T]
  ... methods for adding singletons, etc ...
}

class Foo(arg: String, svc: FooService) {
  ...
}

trait Constructor[T] { ??? }    

object FooConstructor extends Constructor[Foo] {
  def construct(arg: String)(implicit container: Container) =
    new Foo(arg, container.getSingleton[FooService])
}

Now basically I'd like to be able to have a method called construct, which I can call as construct[Foo]("asd") and get a new instance of Foo with "asd" passed in to the constructor, and FooService gotten from the local container and passed in to the constructor. The idea is that it should grab the instance of the Constructor type class for Foo and in a typesafe way, know the number and types of arguments it should have. Also, and this is the hard part, I don't want to have to write out the types of the arguments - just the object to be constructed.

I've tried a couple things:

trait Constructor1[T, A] {
  def construct(arg: A): T
}

trait Constructor2[T, A1, A2] {
  def construct(arg1: A1, arg2: A2): T
}

def construct[T, A](arg1: A): T = implicitly[Constructor1[T, A]].construct(arg1)

...

This approach doesn't work though because it seems like in order to "summon" the Constructor type class instance, we need to write the types of the arguments, which is a lot of nasty boilerplate:

construct[Foo, String]("asd") // yuck!

Is there a way to use type classes (or anything else) to sort of partially infer the type parameters? We have the types of the constructor parameters for Foo defined in the Constructor instance definition, so if we can summon the instance we should be able to just call construct and get the right argument types. The issue is getting that instance without having to specify the constructor type arguments. I've played around with a bunch of different ideas for this, and I feel like with Scala's power and bag of tricks there just has to be a way I can write construct[Foo]("asd") and have the argument list be type safe. Any ideas?

UPDATE: Thanks to Miles Sabin's excellent answer + a slight modification, here's a method that only requires one type parameter and works for all different argument list lengths. This is a pretty simple way to painlessly wire up dependencies, without the cost of reflection:

trait Constructor1[T, A] { def construct(arg1: A)(implicit c: Container): T }
trait Constructor2[T, A, B] { def construct(arg1: A, arg2: B)(implicit c: Container): T }

implicit object FooConstructor extends Constructor1[Foo, String] {
  def construct(arg1: String)(implicit c: Container) = 
    new Foo(arg1, c.getSingleton[FooService])
}

implicit object BarConstructor extends Constructor2[Bar, String, Int] {
  def construct(arg1: String, arg2: Int)(implicit c: Container) = 
    new Bar(arg1, arg2, c.getSingleton[FooService])
}

class Construct[T] {
  def apply[A](arg1: A)(implicit ctor: Constructor1[T, A], container: Container) =
    ctor.construct(arg1)
  def apply[A, B](arg1: A, arg2: B)(implicit ctor: Constructor2[T, A, B], container: Container) =
    ctor.construct(arg1, arg2)
}

def construct[T] = new Construct[T]

construct[Foo]("asd")
construct[Bar]("asd", 123)
like image 364
lvilnis Avatar asked Feb 21 '23 09:02

lvilnis


1 Answers

Type parameter inference in Scala is an all or nothing affair: if you explicitly supply any of the type arguments for a type parameter block then you must supply them all. Consequently, if you want to supply only some of a set of type arguments you must arrange for them to belong to separate type parameter blocks.

The way to do that in this case is to split the construct method into two stages: the first, which takes an explicit type argument and returns a function-like value; and a second, which applies the function-like value to the arguments for which you want the types to be inferred.

Here's how it might go,

// Function-like type
class Construct1[T] {
  def apply[A](arg1: A)(implicit ctor : Constructor1[T, A]): T =
    ctor.construct(arg1)
}

def construct[T] = new Construct1[T]

The result of invoking construct[Foo] is a value of type Construct1[Foo]. This has an apply method with a type parameter, which can be inferred, and an implicit parameter whose type is determined by both T and A. The invocation you want to make now looks like,

construct[Foo].apply("asd")  // T explicit, A inferred as String

Scala's semantic sugaring rules around apply applies here which mean that this can be rewritten as,

construct[Foo]("asd")

which is exactly the result you want.

like image 96
Miles Sabin Avatar answered Feb 22 '23 23:02

Miles Sabin