24

Assuming you have a string containing the name of a method, an object that supports that method and some arguments, is there some language feature that allows you to call that dynamically?

Kind of like Ruby's send parameter.

Myles Gray
  • 8,711
  • 7
  • 48
  • 70
Geo
  • 93,257
  • 117
  • 344
  • 520

4 Answers4

39

You can do this with reflection in Java:

class A {
  def cat(s1: String, s2: String) = s1 + " " + s2
}
val a = new A
val hi = "Hello"
val all = "World"
val method = a.getClass.getMethod("cat",hi.getClass,all.getClass)
method.invoke(a,hi,all)

And if you want it to be easy in Scala you can make a class that does this for you, plus an implicit for conversion:

case class Caller[T>:Null<:AnyRef](klass:T) {
  def call(methodName:String,args:AnyRef*):AnyRef = {
    def argtypes = args.map(_.getClass)
    def method = klass.getClass.getMethod(methodName, argtypes: _*)
    method.invoke(klass,args: _*)
  }
}
implicit def anyref2callable[T>:Null<:AnyRef](klass:T):Caller[T] = new Caller(klass)
a call ("cat","Hi","there")

Doing this sort of thing converts compile-time errors into runtime errors, however (i.e. it essentially circumvents the type system), so use with caution.

(Edit: and see the use of NameTransformer in the link above--adding that will help if you try to use operators.)

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • I am getting error on the following. `List(1, 2) call ("take", 1)`. The error is `type mismatch; found : Int(1) required: AnyRef Note: an implicit exists from scala.Int => java.lang.Integer, but methods inherited from Object are rendered ambiguous. This is to avoid a blanket implicit which would convert any scala.Int to any AnyRef. You may wish to use a type ascription: x: java.lang.Integer.` Is there a way to handle primitive types? ( I want to dynamically generate the argument list...may be a list of `Any`) – dips Feb 25 '13 at 06:14
  • 1
    @dips - You can `.asInstanceOf[AnyRef]` on the way in (works with `Any` also), or `1: java.lang.Integer` as the error suggests. Or you can match primitives and handle case-by-case if you really need to, though you ought not need to here. – Rex Kerr Feb 25 '13 at 07:15
  • Thanks! So my `call` function signature now takes `args: Any*` and I convert it to `AnyRef*` in the body of `call` as `args map {_.asInstanceOf[AnyRef]}`. Hope this is fine. Don't know the subtleties, hence asking. – dips Feb 25 '13 at 08:20
  • This solution doesn't work for lists. The list is passed as `scala.collection.immutable.$colon$colon` to the method instead of `List[Type]`. Any ideas? – Arthur C Apr 26 '14 at 17:23
  • @ArthurC - You can encode the name the same way the compiler does, either by hand (see http://www.codecommit.com/blog/java/interop-between-java-and-scala for a translation table), or I think there's an externally-callable compiler method that does it whose name I forget. – Rex Kerr Apr 26 '14 at 20:06
  • Thank you. I changed the parameter type from `List[Ticket]` to `scala.collection.immutable.$colon$colon[Ticket]`. It's not pretty but it works. – Arthur C Apr 27 '14 at 10:42
  • I am getting a `NullPointerException` at `def argtypes = args.map(_.getClass)` when I pass an argument as null to this function. Any suggestions how to avoid that? – Wasae Shoaib Jun 06 '18 at 12:33
7

Yes. It's called reflection. Here's a link to one way, using some experimental stuff However you should remember that Scala is not a dynamic language, and may not be able to easily do some things that scripting languages can do. You're probably better doing a match on the string, and then calling the right method.

Jim Barrows
  • 3,634
  • 1
  • 25
  • 36
4

Yes you can! You would need .invoke() method of the method object. Simple example below:

 import scala.util.Try

 case class MyCaseClass(i: String) {
 def sayHi = {
     println(i)
   }
 }
 val hiObj = MyCaseClass("hi")
 val mtdName = "sayHi"
 // Method itself as an object
 val mtd = hiObj.getClass.getMethod(mtdName)
 Try {mtd.invoke(hiObj)}.recover { case _ => ()}

see code here: https://scastie.scala-lang.org/vasily802/WRsRpgSoSayhHBeAvogieg/9

Vasily802
  • 1,703
  • 2
  • 18
  • 35
2
scala> val commandExecutor = Map("cleanup" -> {()=> println("cleanup successfully")} )
commandExecutor: scala.collection.immutable.Map[String,() => Unit] = Map(cleanup -> <function0>)

scala> val command="cleanup"
command: String = cleanup

scala> commandExecutor(command).apply
cleanup successfully
Ravi
  • 66
  • 1
  • 9