Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get the function name in Scala?

Tags:

scala

Consider a simple task. Just show the whole expression like:

    a operator b = c

In Common Lisp it might look like this:

    (defun applyOp (a b op) 
        (format t "~S ~S ~S = ~S" a op b (apply op (list a b))))

    $ (applyOp 1 2 #'logand)
    1 #<FUNCTION LOGAND> 2 = 0

In Scala it seems not so trivial:

    def applyOperator(x: Int, y: Int, op: (Int, Int) => Int) = { 
        println(x + s" $op " + y + " = " + op(x,y)) 
    }

    scala> applyOperator(1, 2, _ & _)
    1 <function2> 2 = 0 // how to get the function name? 

What's the name of <function2>?

like image 546
Happy Torturer Avatar asked Nov 18 '15 21:11

Happy Torturer


2 Answers

Another full-featured approach would be a simple macro.

Suppose you write simplest macro extracting string representation of expression and typename:

import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros

class VerboseImpl(val c: Context) {
  import c.universe._

  def describe[T: c.WeakTypeTag](expr: c.Expr[T]): c.Expr[(String, String, T)] = {
    val repr = expr.tree.toString
    val typeName = weakTypeOf[T].toString
    c.Expr[(String, String, T)](q"($repr, $typeName, $expr)")
  }

}

object Verbose{
  def apply[T](expr: T): (String, String, T) = macro VerboseImpl.describe[T]
}

Next in another source (preferrable in another subproject) you can write

val a = 2
val b = 3
println(Verbose(a + b + 3))

and see magic string

(a.+(b).+(3),Int,8)

From this point you can enhance you macro to display any information about methods, parameters, types, etc...

Note that macro is evaluated at compile time. So overhead of Verbose.apply call is like creating tuple with two string constants, so while it's the only boilerplatless, extensible approach, it's the definitely most performant

like image 98
Odomontois Avatar answered Sep 30 '22 19:09

Odomontois


a op b is a call to a method named op

a op b is syntactically equivalent to a.op(b) (except if the name op ends with a colon, in which case it is equivalent to b.op(a)).

This is of course completely different from to something like a.callOperator(op,b), where you need the name op. In scala, there is a different method for each operator, and accessing the name of the operator inside the method that implements this very operator is pointless.

This is necessary for type checking

The nice thing with the callOperator(op,b) is that you can implement all operator in the same place, maybe very generically and concisely.

The nice thing with one method for each if that the compiler will check that you can call only the one which are actually implemented, with the proper arguments. Also, the type of the result will be known at compile time.

Scala being a typed language, prefers very much the second one.

There is an escape hatch, Dynamic

There is however, in some remote corner of the language, a way to turn a call to method not available at compile time to some backup call, which get the method (or operator) name and the arguments.

The target of the call (that is the left operand) must extend trait Dynamic. As this is a rather special feature, you must allow it with import scala.language.dynamics.

Then, every call to a non existent method will be rewritten as a call to a method applyDynamic, with two arguments list, the first one getting the name of the method, the second one the actual arguments Depending of whether and how you have defined applyDynamic, this rewriting may or may not be allowed by the compiler.

Here is an example

case class A(string name) {
  def applyDynamic(methodOrOperatorName: String)(arg: Any) : A {
    A(s"($name $methodOrOperatorName ${arg.mkString(", ")})
  }
}

Here I choose to allow only one arg in the call, which is fine if I want only binary operators (but there is no way to distinguish a op b from a.op(b), scala consider them equivalent). Otherwise I would write args: Any*. I allowed any type (Any), but I could restrict that, for instance force arg: A. I'm not forced to use A for the result type, but if my result type is not known to be dynamic, I will not be able to chain a op b op' c.

There might be a few problems. applyDynamic will be called only if the compiler fails to compile a op b by other means. If there is e.g some implicit conversions that makes op available, this will have priority. For instance, Predef makes + available for every object, for string concatenation. So if your operator is +, this is what will be called. To avoid that, you might define +in A :

def +(arg: Any): A = applyDynamic("+")(arg) 
  // Arg and result type as in applyDynamic

This would make + safe, but any operator available made available at the call site through implicit will take priority too.

The non magical way

If you have a limited list of allowed operators, you might prefer to avoid magic altogether.

class A {
   def +(arg: A): A = callOp("+", b) // or another signature
   def -(arg: A): A = callOp("-", b) 
   def callOp(name: String, arg: A): A = {...}
}
like image 21
Didier Dupont Avatar answered Sep 30 '22 19:09

Didier Dupont