Read case class object from string in Scala (something like Haskell's "read" typeclass)

12,228

Solution 1

dflemstr answered more towards setting up the actual read method- I'll answer more for the actual parsing method.

My approach has two objects that can be used in scala's pattern matching blocks. AsInt lets you match against strings that represent Ints, and PersonString is the actual implementation for Person deserialization.

object AsInt {
  def unapply(s: String) = try{ Some(s.toInt) } catch {
    case e: NumberFormatException => None
  }
}

val PersonRegex = "Person\\((.*),(\\d+)\\)".r

object PersonString {
  def unapply(str: String): Option[Person] = str match {
    case PersonRegex(name, AsInt(age)) => Some(Person(name, age))
    case _ => None
  }
}

The magic is in the unapply method, which scala has syntax sugar for. So using the PersonString object, you could do

val person = PersonString.unapply("Person(Bob,42)")
//  person will be Some(Person("Bob", 42))

or you could use a pattern matching block to do stuff with the person:

"Person(Bob,42)" match {
  case PersonString(person) => println(person.name + " " + person.age)
  case _ => println("Didn't get a person")
}

Solution 2

Scala does not have type classes, and in this case, you cannot even simulate the type class with a trait that is inherited from, because traits only express methods on an object, meaning that they have to be "owned" by a class, so you cannot put the definition of a "constructor that takes a string as the only argument" (which is what "read" might be called in OOP languages) in a trait.

Instead, you have to simulate type classes yourself. This is done like so (equivalent Haskell code in comments):

// class Read a where read :: String -> a
trait Read[A] { def read(s: String): A }

// instance Read Person where read = ... parser for Person ...
implicit object ReadPerson extends Read[Person] {
  def read(s: String): Person = ... parser for Person ...
}

Then, when you have a method that depends on the type class, you have to specify it as an implicit context:

// readList :: Read a => [String] -> [a]
// readList ss = map read ss
def readList[A: Read] (ss: List[String]): List[A] = {
  val r = implicitly[Read[A]] // Get the class instance of Read for type A
  ss.map(r.read _)
}

The user would probably like a polymorphic method like this for ease of use:

object read {
  def apply[A: Read](s: String): A = implicitly[Read[A]].read(s)
}

Then one can just write:

val person: Person = read[Person]("Person(Bob,42)")

I am not aware of any standard implementation(s) for this type class, in particular.

Also, a disclaimer: I don't have a Scala compiler and haven't used the language for years, so I can't guarantee that this code compiles.

Solution 3

Starting Scala 2.13, it's possible to pattern match a Strings by unapplying a string interpolator:

// case class Person(name: String, age: Int)
"Person(Bob,42)" match { case s"Person($name,$age)" => Person(name, age.toInt) }
// Person("Bob", 42)

Note that you can also use regexes within the extractor.

Which in this case, helps for instance to match on "Person(Bob, 42)" (age with a leading space) and to force age to be an integer:

val Age = "[ ?*](\\d+)".r

"Person(Bob, 42)" match {
  case s"Person($name,${Age(age)})" => Some(Person(name, age.toInt))
  case _ => None
}
// Person = Some(Person(Bob,42))

Solution 4

The answers on this question are somewhat outdated. Scala has picked up some new features, notably typeclasses and macros, to make this more easily possible.

Using the Scala Pickling library, you can serialize/deserialize arbitrary classes to and from various serialization formats:

import scala.pickling._
import json._

case class Person(name: String, age: Int)
val person1 = Person("Bob", 42)
val str = person1.pickle.value // { tpe: "Person", name: "Bob", age: 42 }
val person2 = JSONPickle(str).unpickle[Person]

assert(person1 == person2) // Works!

The serializers/deserializers are automatically generated at compile time, so no reflection! If you need to parse case classes using a specific format (such as the case class toString format), you can extend this system with your own formats.

Solution 5

The uPickle library offers a solution for this problem.

Share:
12,228

Related videos on Youtube

emchristiansen
Author by

emchristiansen

Updated on September 16, 2022

Comments

  • emchristiansen
    emchristiansen over 1 year

    I'd like to read a string as an instance of a case class. For example, if the function were named "read" it would let me do the following:

    case class Person(name: String, age: Int)
    val personString: String = "Person(Bob,42)"
    val person: Person = read(personString)
    

    This is the same behavior as the read typeclass in Haskell.

  • Eric Bowman - abstracto -
    Eric Bowman - abstracto - almost 12 years
    One nice trick I learned recently, is to use import scala.util.control.Exception._ and then catching(classOf[NumberFormatException]).opt(s.toInt) or even allCatch.opt(s.toInt).
  • Dylan
    Dylan almost 12 years
    That is a pretty nice little language for exception handling. My problem with it is that for simple things like this, it doesn't really end up saving much in the way of typing, and it can be just another step in the way of a beginner's understanding of an example. That being said, you get my +1
  • ChenZhou
    ChenZhou about 9 years
    Things have changed in the meantime, see DCKing's answer.
  • matanster
    matanster over 8 years
    The library does fall back to reflection, for above vanilla use cases.