When working with dynamic systems, code generation, or metaprogramming in Scala, you often need to inspect classes and methods at runtime. While doing this in Java via standard Java Reflection (java.lang.reflect) is straightforward, Scala introduces nuances such as nullary methods (no parentheses), empty parameter lists (()), and multiple parameter groups (currying).

In this guide, we will walk through how to dynamically retrieve a method's return type in Scala given its fully qualified class name, method name, and parameter types using the Scala Reflection API.

The Problem Example

Consider the following sample class containing different kinds of method signatures:

package invalid.so

class MyClass {
  def noParamGroups: String = "ABC"
  def zeroArgs(): Int = 123
  def multiArgs(a: String, b: Int): Boolean = false
  def multiParamGroups(a: String)(b: Int, c: Boolean): Double = 0.0
}

Given the string "invalid.so.MyClass" and the method name "multiParamGroups", our goal is to inspect the method and determine that its return type is Double.

The Solution: Scala Runtime Reflection

Scala provides runtime reflection via scala.reflect.runtime.universe. Unlike Java reflection, Scala reflection is aware of Scala-specific features such as multiple parameter lists, type aliases, and path-dependent types.

Step-by-Step Implementation

Here is a complete, reusable utility function that inspects a class by name, matches the target method by name and parameter types, and extracts the final return type:

import scala.reflect.runtime.universe._

object MethodInspector {
  private val mirror = runtimeMirror(getClass.getClassLoader)

  /**
   * Retrieves the return type of a method.
   * 
   * @param className  Fully qualified class name (e.g., "invalid.so.MyClass")
   * @param methodName Method name to look up
   * @param paramTypes A List of List[Type] representing each parameter list group.
   *                   Use Nil or List(Nil) to match methods with no/empty parameters.
   * @return Option[Type] containing the method's return type if found.
   */
  def getReturnType(
    className: String,
    methodName: String,
    paramTypes: List[List[Type]] = Nil
  ): Option[Type] = {
    try {
      val classSymbol = mirror.staticClass(className)
      val classType = classSymbol.toType
      val termName = TermName(methodName)

      // Collect all method symbols matching the name (handles overloads)
      val methodCandidates = classType.member(termName).alternatives.collect {
        case sym if sym.isMethod => sym.asMethod
      }

      // Find the method whose parameter signatures match paramTypes
      val matchedMethod = methodCandidates.find { m =>
        val methodParamTypes = m.paramLists.map(_.map(_.typeSignatureIn(classType)))
        if (paramTypes.isEmpty && methodParamTypes.isEmpty) true
        else methodParamTypes == paramTypes
      }

      matchedMethod.map(_.returnType)
    } catch {
      case _: ScalaReflectionException => None
    }
  }
}

Usage and Testing

Let's see how to query each method type using Scala's typeOf[...] helper:

import scala.reflect.runtime.universe._

val className = "invalid.so.MyClass"

// 1. Method with no parameter groups: def noParamGroups
val res1 = MethodInspector.getReturnType(className, "noParamGroups", Nil)
println(res1) // Some(String)

// 2. Method with empty parameter list: def zeroArgs()
val res2 = MethodInspector.getReturnType(className, "zeroArgs", List(Nil))
println(res2) // Some(Int)

// 3. Method with single parameter list: def multiArgs(a: String, b: Int)
val res3 = MethodInspector.getReturnType(
  className,
  "multiArgs",
  List(List(typeOf[String], typeOf[Int]))
)
println(res3) // Some(Boolean)

// 4. Method with multiple parameter groups: def multiParamGroups(a: String)(b: Int, c: Boolean)
val res4 = MethodInspector.getReturnType(
  className,
  "multiParamGroups",
  List(
    List(typeOf[String]),
    List(typeOf[Int], typeOf[Boolean])
  )
)
println(res4) // Some(Double)

Key Details Explained

  • asMethod.returnType: The .returnType property on a MethodSymbol returns the ultimate result type after all parameter groups have been applied. This makes extracting return types for curried methods straightforward.
  • paramLists: In Scala, a method can have multiple parameter lists (List[List[Symbol]]). By checking against List[List[Type]], you can accurately distinguish between def f: Int, def f(): Int, and def f()(x: String): Int.
  • typeSignatureIn: Using typeSignatureIn(classType) ensures that generic type parameters inherited from a parent class or trait are properly instantiated to their concrete types.

Alternative: Java Reflection (Caveats)

You can also use Java reflection (Class.forName(...).getDeclaredMethods), but note that Java sees Scala methods through standard JVM bytecode:

  • Multiple parameter lists (e.g., def f(a: String)(b: Int)) are flattened into a single parameter list (f(String a, int b)).
  • Scala type aliases and value classes (AnyVal) might be unboxed or represented by primitive JVM equivalents.

For native Scala code, using Scala Reflection is always the most accurate approach.