Alfredo Gimenez
Alfredo Gimenez

Reputation: 2224

Scala Function to create a conversion function from a string to a matched value

I want to read in a CSV file with data and an accompanying CSV file with the types of each column in the first file and cast the data types appropriately. Example:

data file:

a,b,c
1,two,3.333

types file:

column, type
a, int
b, string
c, double

My attempt so far has been to write a function that determines the correct conversion function from a String to the appropriate type. The problem is, the "appropriate type" can be multiple different possible types, depending on the input.

def converter[T](columnType: String): String => T = {
    columnType match {
        case "int" => (s: String) => s.toInt
        case "double" => (s: String) => s.toDouble
        case "string" => (s: String) => s
    }
 }

This does not compile because the type T cannot be inferred.

I looked into the possibility of using the Either keyword but the syntax becomes cumbersome with more than 2 possible return types.

Is there a nice way to do this?

Upvotes: 1

Views: 961

Answers (2)

Andrew M.
Andrew M.

Reputation: 852

Another alternative to the excellent answer above is this, if you'd like just one conversion method to call:

// Define typeclass (plus companion object for convenience)

trait FromStringConverter[T] {
  def convert(string: String): T
}

object FromStringConverter {
  def apply[T](f: String => T): FromStringConverter[T] = new FromStringConverter[T] {
    override def convert(string: String): T = f(string)
  }
}

// Define converters

implicit val intConverter = FromStringConverter[Int] { string =>
  string.toInt
}

implicit val doubleConverter = FromStringConverter[Double] { string =>
  string.toDouble
}

implicit val identityConverter = FromStringConverter[String] { string =>
  string
}

// Define conversion method

def convertStringTo[T : FromStringConverter](string: String): T =
  implicitly[FromStringConverter[T]].convert(string)

// OR alternately (this has the same effect as 'convertStringTo')

def convertStringTo2[T](string: String)(implicit ev: FromStringConverter[T]): T =
  ev.convert(string)

// Test

convertStringTo[Int]("3")         // => 3
convertStringTo[Double]("3.0")    // => 3.0
convertStringTo[String]("three")  // => "three"

convertStringTo2[Int]("3")        // => 3
convertStringTo2[Double]("3.0")   // => 3.0
convertStringTo2[String]("three") // => "three"

You can easily add more implicit converters to support any type, so long as you can provide the logic for converting a string to that type.

Upvotes: 1

JRomero
JRomero

Reputation: 4868

A very unethical solution would be to simply cast the instance to "assist" the compiler

def converter[T](columnType: String): String => T = {
  columnType match {
    case "int"  => (s: String) => s.toInt.asInstanceOf[T]
    case "double" => (s: String) => s.toDouble.asInstanceOf[T]
    case "string" => (s: String) => s.asInstanceOf[T]
  }
}

val stringConverter = converter[String]("string")
val intConverter = converter[Int]("int")
val doubleConverter = converter[Double](doubleConverter)

stringConverter("hello")
intConverter("1")
doubleConverter("3.00")

A cleaner yet still unorthodox solution would be to use the type class to determine the type.

def converter[T : Manifest]: String => T = {
  implicitly[Manifest[T]].runtimeClass match {
    case x if classOf[Int] == x => (s: String) => s.toInt.asInstanceOf[T]
    case x if classOf[Double] == x => (s: String) => s.toDouble.asInstanceOf[T]
    case x if classOf[String] == x => (s: String) => s.asInstanceOf[T]
  }
}

val stringConverter = converter[String]
val intConverter = converter[Int]
val doubleConverter = converter[Double]

stringConverter("hello")
intConverter("1")
doubleConverter("3.00") 

What you probably want to look into is typeclasses: https://youtu.be/sVMES4RZF-8

Here is an example of what a typeclass solution would look like:

trait Reader[T] {
  def convert(s: String): T
}

implicit val doubleReader = new Reader[Double] {
  override def convert(s: String) = s.toDouble
}

implicit val intReader = new Reader[Int] {
  override def convert(s: String) = s.toInt
}

implicit val stringReader = new Reader[String] {
  override def convert(s: String) = s
}

def converter[T: Reader]: String => T = implicitly[Reader[T]].convert

val stringConverter = converter[String]
val intConverter = converter[Int]
val doubleConverter = converter[Double]

stringConverter("hello")
intConverter("1")
doubleConverter("3.00")

Upvotes: 2

Related Questions