rsan
rsan

Reputation: 1887

Is there a way to refactoring this match case function?

I have this code that will repeat multiple times (more than 15 times) across my app.

val entityList = params.map(paramPair => {
//THIS LINE DEPENDS ON THE ENTITY
val entityX = new Entity1
paramPair.foreach { case (key, value) => (key, value) match {
  //THIS BLOCK CHANGES DEPENDING THE ENTITY
  case (KEY1, v: TYPE1) => entityX setX v
  case (KEY2, v: TYPE2) => entityX setY v
  ...
  //END BLOCK
  case _ =>
  }
}
entityX
})

The only code that changes are the type of entity and the pattern matching case clauses. Is there a way to create a function that receive those cases to avoid code repetition?

Upvotes: 0

Views: 522

Answers (2)

som-snytt
som-snytt

Reputation: 39577

Just for fun. Because the point is to have fun, right?

Since I forget how to use mutable fields, I prefer to construct the immutable Dog.

Impersonator could extend AnyVal by refactoring the PartialFunction (since the anonymous nested class is not permitted for AnyVal, "This restriction is planned to be removed in subsequent releases.")

package populate

import scala.reflect._
import scala.reflect.runtime.{ currentMirror => cm }
import scala.reflect.runtime.universe._
import java.util.{ Calendar, Date }

case class Person(var name: String, var birth: Date) { def this() = this(null,null) }
case class Book(var title: String, var pub: Date) { def this() = this(null,null) }
case class Dog(val name: String, val rabies: Date, val goodBoy: Boolean = true)

object Test extends App {
  def dateOf(y:Int,m:Int,d:Int) = { val c = Calendar.getInstance; c.set(1974, 2, 14); c.getTime }
  val input = Seq("id" -> "Sybil", "n" -> dateOf(1974, 2, 14), "garbage" -> "error")

  trait Completer[A] extends Any {
    def filler: PartialFunction[A, Unit]
    def complete(info: Seq[A]) = info foreach (filler orElse {
      case (k, v)             => println(s"Can't use $k -> $v")
    })
  }
  type Info = (String, Any)
  implicit class Impersonator(val p: Person) extends Completer[Info] {
    override def filler = {
      case ("id", s: String)  => p.name = s
      case ("n", d: Date)     => p.birth = d
    }
  }
  implicit class Booker(val b: Book) extends Completer[Info] {
    override def filler = {
      case ("id", s: String)  => b.title = s
      case ("n", d: Date)     => b.pub = d
    }
  }

  def personify(p: Person, info: Seq[(String, Any)]) = info foreach {
    case ("id", s: String)  => p.name = s
    case ("n", d: Date)     => p.birth = d
    case (k, v)             => println(s"Can't use $k -> $v")
  }
  def bookish(b: Book, info: Seq[(String, Any)]) = info foreach {
    case ("id", s: String)  => b.title = s
    case ("n", d: Date)     => b.pub = d
    case (k, v)             => println(s"Can't use $k -> $v")
  }
  def inject[A: ClassTag](a: A, info: Seq[(String, Any)]): A = {
   implicitly[ClassTag[A]] match {
    case ClassTag(k) if k == classOf[Person]  => personify(a.asInstanceOf[Person], info)
    //case ClassTag(k) if k == classOf[Book]    => bookish(classOf[Book] cast a, info)
    case ClassTag(k) if k == classOf[Book]    => a.asInstanceOf[Book] complete info
    case k => println(s"Unknown $k")
   }
   a
  }
  def entity[A: ClassTag](info: Seq[(String, Any)]): A = {
    val e = implicitly[ClassTag[A]].runtimeClass.newInstance.asInstanceOf[A]
    inject(e, info)
  }

  val v = entity[Person](input)
  println(v)
  Console println entity[Book](input)

  Console println Defaulter((input map {
    case ("id", v)  => ("name", v)
    case ("n", v)   => ("rabies", v)
    case p          => p
  }).toMap).newCase[Dog]
}

case class Defaulter(input: Map[String, Any]) {
  def newCase[A]()(implicit t: ClassTag[A]): A = {
    val claas = cm classSymbol t.runtimeClass
    val modul = claas.companionSymbol.asModule
    val im = cm reflect (cm reflectModule modul).instance
    defaut[A](im, "apply")
  }

  def defaut[A](im: InstanceMirror, name: String): A = {
    val at = newTermName(name)
    val ts = im.symbol.typeSignature
    val method = (ts member at).asMethod

    // either defarg or default val for type of p
    def valueFor(p: Symbol, i: Int): Any = {
      val defarg = ts member newTermName(s"$name$$default$$${i+1}")
      if (input contains p.name.toString) {
        input(p.name.toString)
      } else if (defarg != NoSymbol) {
        println(s"default $defarg")
        (im reflectMethod defarg.asMethod)()
      } else {
        println(s"def val for $p")
        p.typeSignature match {
          case t if t == typeOf[String] => null
          case t if t == typeOf[Int]    => 0
          case t if t == typeOf[Date]   => new Date(0L)
          case x                        => throw new IllegalArgumentException(x.toString)
        }
      }
    }
    val args = (for (ps <- method.paramss; p <- ps) yield p).zipWithIndex map (p => valueFor(p._1,p._2))
    (im reflectMethod method)(args: _*).asInstanceOf[A]
  }
}

Upvotes: 1

om-nom-nom
om-nom-nom

Reputation: 62835

You can turn

paramPair.foreach { case (key, value) => (key, value) match {
  //THIS BLOCK CHANGES DEPENDING THE ENTITY
  case (KEY1, v: TYPE1) => entityX setX v
  case (KEY2, v: TYPE2) => entityX setY v
  ...
  //END BLOCK
  case _ =>
  }
}

into

paramPair.foreach { case (key, value) => 
  case (KEY1, v: TYPE1) => entityX setX v
  case (KEY2, v: TYPE2) => entityX setY v
  ...
  //END BLOCK
  case _ =>
  }
}

If you are ok with anonymous classes, you can go further with converting whole code into:

val entityList = params.map(paramPair => {
  new Entity1 {
    paramPair.foreach { case (key, value) => 
      //THIS BLOCK CHANGES DEPENDING THE ENTITY
      case (KEY1, v: TYPE1) => setX v
      case (KEY2, v: TYPE2) => setY v
      ...
      //END BLOCK
      case _ =>
    }
}})

If block changes depending on entity you may, as @pst said, turn into function using so called function literal: expression with curly braces and case statements will be turned into PartialFunction, e.g.

val fillEntitityOne: PartialFunction[A,B] = {
  case (KEY1, v: TYPE1) => entityX setX v
  case (KEY2, v: TYPE2) => entityX setY v
}

Where A and B input and return types, e.g.

val foobar: Int => String = { case 1 => "1" }

That's why you can ommit match part in second snippet: foreach expects Function instance (and PartialFunction inherits from Function), of course with some types, so actually we can desugar

.foreach {
  case x => 
}

into

.foreach({
  case x => 
})

and then

val anon = new PartialFunction[A,B]{....}
.foreach(anon)

Upvotes: 1

Related Questions