2

I'm working with a Java API that passes around identifiers as strings. It seems a bit nicer to me to use typed symbols for this, so I wrote this:

  object Helpers {
    implicit def actionToString(action: Action): String =
      action.getClass.getName.stripSuffix("$").replaceAll(".*\\$", "")

    object Action{
      def apply(name: String): Action = {
        Class.forName("tutorial.HelloInput$Helpers$" + name).newInstance()
          .asInstanceOf[Action]
      }
    }
    sealed abstract class Action {
      override def toString: String = actionToString(this)
    }
    final case class Rotate() extends Action
    final case class Right() extends Action
    final case class Left() extends Action
    final case class Pause() extends Action
  }

This allows "serialization" and "deserialization" of strings and Actions in a natural way, e.g., I can pattern match on Action(Pause), but I can also pass Pause() to a Java library that expects a string thanks to the implicit conversion.

Is there a better way to do this, especially in terms of performance? Are there any problems with this method that may come back to bite me later?

I was reading a little about Phantom types in Dotty, and wonder if they might be used to improve performance for dealing with symbols (or maybe the new Enums would be the better alternative).

bbarker
  • 11,636
  • 9
  • 38
  • 62

2 Answers2

5

Implicit conversions have a habit of coming back to bite you. In this case, it means that you can use one of your action types in any method where a String is required, not just where you want it to. It also doesn't prevent you from just passing some other string into the library.

Instead of using the conversion to interface with the Java library directly, I would create a wrapper around it that accepts and returns your action types. This way you get a nice typed api and no need for any conversions.

Since your classes have no parameters it makes sense to use case objects instead, so you only need one instance for each action type instead of creating a new one each time.

The use of reflection concerns me as well. In creating an action from a string could be implemented using pattern matching and conversion to string could be done by having each type define it's string value, or even just use productPrefix if the name of the class is the always same as the string you need (though I would prefer defining it explicitly). I suspect this method would be faster as well, but you would need to benchmark it to be sure.

puhlen
  • 8,400
  • 1
  • 16
  • 31
  • "create a wrapper around it that accepts and returns your action types" This is the definitive answer, I just wanted to add a few alternative for the problem itself (ignoring the fact that it's for interfacing with the Java world) – OlivierBlanvillain Jun 20 '17 at 07:43
  • Lots of good suggestions, thanks! I've been using implicit value classes when possible, for a wrapper, but the downside is that the Java API still leaks. – bbarker Jun 22 '17 at 01:06
2

What are the benefits of your approach compared to the following:?

object Helpers {
  type Action = String
  val Rotate: Action = "Rotate"
  val Right:  Action = "Right"
  val Left:   Action = "Left"
  val Pause:  Action = "Pause"
}

You can trade boilerplate for type safety (without losing any performance) using a value class:

object Helpers {
  // This won't allocate anything more than the previous solution!
  final case class Action(name: String) extends AnyVal
  val Rotate: Action = Action("Rotate")
  val Right:  Action = Action("Right")
  val Left:   Action = Action("Left")
  val Pause:  Action = Action("Pause")
}

I think both of the above approaches are more robust than using reflection + implicit conversion. For instance, your solution will silently break when move code around, or renaming the tutorial package.

Are there any problems with this method that may come back to bite me later?

Implicit conversion are the devil. I would strongly advise you to never even consider them in your design, it's so easy to "get something that works" and regret it a week or two later... The fact that with your solution the following compiles would, IMO, be unacceptable:

val myAction: Action = ...
myAction.foreach(...)
myAction.map(...)

I was reading a little about Phantom types in Dotty, and wonder if they might be used to improve performance for dealing with symbols (or maybe the new Enums would be the better alternative).

I think union types + literal singleton types could be useful in this case, you could define the following type alias:

type Action = "Rotate" | "Right" | "Left" | "Pause"

Then if a method is only supposed to be called with a subset of these types, you case put that very precisely in it's API! (Except that we can't have literal singleton types in or types, but I sure they are going to be supported at some point, see this issue). Enum are just syntax for what you can already do with sealed trait + case classes, they shouldn't help with anything other than "save a few keystrokes".

OlivierBlanvillain
  • 7,701
  • 4
  • 32
  • 51
  • Thanks, these are all great ideas - I wish I could accept two answers. I'm currently using the second suggestion but may adapt to something else later. – bbarker Jun 22 '17 at 01:04