Search code examples
scalalambdareflection

Scala: Convert string to lambda


I want to be able to parse strings like "x => x + 1" and be able to actually use it as a lambda function like this:

    val string = "x => x + 1"
    val lambda = parseLambda(string)
    val numbers = List(1, 2, 3)
    val result = numbers.map(lambda) // Should be [2, 3, 4]

I have the following implementation for the parseLambda function:

  def parseLambda(string: String): Any => Any = {
      val toolbox = runtimeMirror(getClass.getClassLoader).mkToolBox()
      val tree = toolbox.parse(string)
      val lambdaFunction = toolbox.compile(tree)().asInstanceOf[Any => Any]
      lambdaFunction
  }

It works for strings like "(x: Int) => x * 2" or "(s: String) => s.split(" ")" but if I omit the types e.g. "x => x * 2" or "s => s.split(" ")" I get the following error

';' expected but '=>' found.

Is there a way to be able to omit the types in the string? Any help would be appreciated!


Solution

  • Without type hint x => x * 2 would fail to compile in both Scala 2 (https://scastie.scala-lang.org/ChqbxOxRSvGhFTsXeyhOWA) and Scala 3 (https://scastie.scala-lang.org/1uYVoRXLTeKEwfoPCwrKzQ). This is simply bad code. toolkit.parse is returning rather unhelpful message, but there is no magic way in which compiler would guess what should the input be.

    However in your code there is something which knows what the type should be: the type of values in numbers.

    trait TypeName[A] {
      def name: String
    }
    
    def parseLambda[Input: TypeName](string: String): Input => Any = {
      val code = s"""val result: ${implicitly[TypeName[Input]].name} => Any = ${string}
                    |result
                    |""".stripMargin
      val toolbox = runtimeMirror(getClass.getClassLoader).mkToolBox()
      val tree = toolbox.parse(code)
      val lambdaFunction = toolbox.compile(tree)().asInstanceOf[Input => Any]
      lambdaFunction
    }
    

    Then it should work:

    val string = "x => x + 1"
    val lambda = parseLambda[Int](string) // Int hinted
    val numbers = List(1, 2, 3)
    val result = numbers.map(lambda) // Should be [2, 3, 4]
    
    // or
    
    numbers.map(parseLambda(string)) // Int inferred 
    

    That is, it should work as long as you provide the right TypeName[A] instance.

    In Scala 2 it should works with something similar to:

    object TypeName {
    
      import scala.reflect.runtime.universe._
    
      implicit def provide[A](implicit tag: WeakTypeTag[A]): TypeName[A] =
        new TypeName[A] {
          def name: String = tag.toString() // should work for simple cases
        }
    }
    

    and for Scala 3 (your code looks like Scala 2, but in case you wanted to port it to 3) something like:

    object TypeName {
    
      import scala.quoted.*
      
      def provideImpl[A: Type](using quotes: Quotes): Expr[TypeName[A]] =
        import quotes.*, quotes.reflect.*
        '{
          new TypeName[A] {
            def name: String = ${ TypeRepr.of[A].show(using TypeReprCode) }
          }
        }
    
      inline given provide[A]: TypeName[A] = ${ TypeName.provideImpl[A] }
    }
    

    (Disclaimer, I haven't compiled it, so it should be something like that, but might need some adjustments).

    What if this type cannot be provided because you don't know? (E.g. if this lambda was sent to you?)

    Well, if you don't know the type compiler wouldn't know as well, so you simply cannot do that.