1

Do i have a misunderstanding how implciits work in Scala - given the following trait in Scala,

trait hasConfig {

  implicit def string2array(s: java.lang.String): Array[String] = {
    LoadedProperties.getList(s)
  }

  implicit def string2boolean(s: java.lang.String) :  java.lang.Boolean = {
    s.toLowerCase() match {
      case "true" => true
      case "false" => false
    }
  }

  var config: Properties = new Properties()
  def getConfigs : Properties = config
  def loadConfigs(prop:Properties) : Properties = {
    config = prop
    config
  }

  def getConfigAs[T](key:String):T = {
    if (hasConfig(key)) {
      val value : T = config.getProperty(key).asInstanceOf[T]
      value
    }
    else throw new Exception("Key not found in config")
  }

  def hasConfig(key: String): Boolean = {
    config.containsKey(k)
  }
}

Though java.util.properties contains (String, String) key value pairs, I expect the following code to work due to the implicit converstion defined,

class hasConfigTest extends FunSuite {
  val recModel = new Object with hasConfig
  //val prop = LoadedProperties.fromFile("test") Read properties from some file
  recModel.loadConfigs(prop)

  test("test string paramater") {
    assert(recModel.getConfigAs[String]("application.id").equals("framework"))
  }

  test("test boolean paramater") {
    assert(recModel.getConfigAs[Boolean]("framework.booleanvalue") == true) 
    //Property file contains framework.booleanvalue=true
    //expected to return java.lang.boolean, get java.lang.string

  }
}

However, I get the following error,

java.lang.String cannot be cast to java.lang.Boolean
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean

Why is the implcit conversion not taking care of this?

erichfw
  • 334
  • 2
  • 15

3 Answers3

3

It doesn't work because casting (asInstanceOf) is something entirely different than implicit conversions. There are multiple ways in which you can solve this.

Implicit conversion

If you want to use the hardcore implicit conversions magic you should rewrite you getConfigAs method like this:

def getConfig(key:String): String = {
  if (hasConfig(key)) {
    val value: String = config.getProperty(key)
    value
  }
  else throw new Exception("Key not found in config")
}

You will have to import the conversions into the current scope when you use getConfig.

val recModel = new Object with hasConfig
import recModel._
recModel.loadConfigs(prop)
val value: Boolean = recModel.getConfig("framework.booleanvalue")

Implicit parameters

A better way would be to keep your current API, but then you will have to introduce an implicit parameter because the implementation of getConfigAs needs access to the conversion.

def getConfigAs[T](key:String)(implicit conv: String => T): T = {
  if (hasConfig(key)) {
    val value: String = config.getProperty(key)
    value
  }
  else throw new Exception("Key not found in config")
}

You will still need to import the necessary conversions at the use site though.

val recModel = new Object with hasConfig
import recModel._
recModel.loadConfigs(prop)
val value = recModel.getConfigAs[Boolean]("framework.booleanvalue")

Typeclasses

A way to avoid having to import your conversions (and possibly implicitly converting all kinds of Strings by accident) is to introduce a new type to encode your conversions. Then you can implement the conversions in its companion object, where implicit search can find them without importing them.

trait Converter[To]{
  def convert(s: String): To
}
object Converter {
  implicit val string2array: Converter[Array[String]] = new Converter[Array[String]] {
    def convert(s: String): Array[String] = 
      LoadedProperties.getList(s)
  }

  implicit val string2boolean: Converter[Boolean] = new Converter[Boolean] {
    def convert(s: String): Boolean = 
      s.toLowerCase() match {
        case "true" => true
        case "false" => false
      } 
  }
}

Then you can change your getConfigAs method.

def getConfigAs[T](key:String)(implicit conv: Converter[T]): T = {
  if (hasConfig(key)) {
    val value: String = config.getProperty(key)
    conv.convert(value)
  }
  else throw new Exception("Key not found in config")
}

And use it.

val recModel = new Object with hasConfig
recModel.loadConfigs(prop)
val value = recModel.getConfigAs[Boolean]("framework.booleanvalue")

You might also want to take a look over here.

Community
  • 1
  • 1
Jasper-M
  • 12,460
  • 1
  • 19
  • 34
  • Thank you very much for the response. Do you have any recommendation on what would be best practice in scala? I quite like the typeclasses answer as it is similar to @mavarazy below. – erichfw Jun 14 '16 at 08:02
  • Typeclasses and implicit parameters are almost always preferred. And when you need a real implicit conversion for some reason (dsl, some fancy api,...) it's also preferred to use them together with typeclasses, like `implicit def convert[T](t: T)(implicit writer: Writer[T]): String = writer.write(t)`. – Jasper-M Jun 14 '16 at 09:31
1

Implicit conversions should be defined in scope, for example in enclosing object or imported into the current scope. In your case they should be defined in scope of the hasConfigTest class.

http://docs.scala-lang.org/tutorials/FAQ/finding-implicits

Here's a simple reproducible example:

object m {
  implicit def string2boolean(s: String): Boolean = {
    s.toLowerCase() match {
      case "true"  => true
      case "false" => false
    }
  } //> string2boolean: (s: String)Boolean
  println(false || "true") //> true
  println(false || "false") //> false
}
Anton
  • 2,896
  • 3
  • 22
  • 36
1

I think what you are trying to say, is something like this:

import java.util.Properties

object LoadedProperties {
  def getList(s: String): Array[String] = Array.empty
}
object hasConfig {
  sealed trait ConfigReader[T] {
    def read(conf: String): T
  }
  implicit object BooleanConfigReader extends ConfigReader[Boolean] {
    override def read(conf: String): Boolean = conf.toLowerCase() match {
      case "true" => true
      case "false" => false
    }
  }
  implicit object ArrayConfigReader extends ConfigReader[Array[String]] {
    override def read(s: String): Array[String] = {
      LoadedProperties.getList(s)
    }
  }
  var config: Properties = new Properties()
  def getConfigs: Properties = config
  def loadConfigs(prop: Properties): Properties = {
    config = prop
    config
  }
  def getConfigAs[T](key: String)(implicit reader: ConfigReader[T]): T = {
    val prop = config.getProperty(key)
    if  (prop == null)
      throw new Exception("Key not found in config")
    reader.read(prop)
  }
}

val props = new Properties()
props.setProperty("a", "false")
props.setProperty("b", "some")
hasConfig.loadConfigs(props)
hasConfig.getConfigAs[Boolean]("a")
hasConfig.getConfigAs[Array[String]]("a")
mavarazy
  • 7,208
  • 1
  • 30
  • 56