Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala case class copy constructor with dynamic fields

I have an immutable State, and I process a message queue, where each message is a list of new values to some fields in the state.

The new values may apply to a part of a field - for example setting or clearing one flag (bit) out of many, or changing just the low or high 8-bit part of a 16-bit field.

After processing the message, I want to get an immutable copy of the state, with the modifications applied.

object StateField {
  sealed abstract class StateField()
  sealed abstract class Register extends StateField
  sealed abstract class Flag extends StateField
  case object AX extends Register
  case object AH extends Register
  case object AL extends Register
  case object CF extends Flag
  case object OF extends Flag
}

class StateFieldModification(field: StateField, value: Int)

class ModificationMessage(content: List[StateFieldModification])

case class State(AX: Int, Flags: Int) {

  def readRegister(field: StateField.Register): Int = field match {
    case StateField.AX => this.AX
    case StateField.AH => this.AX & 0xFF
    case StateField.AL => (this.AX << 8) & 0xFF
  }

  def readFlag(field: StateField.Flag): Boolean = field match {
    case StateField.CF => (this.Flags & 0x0001) != 0
    case StateField.OF => (this.Flags & 0x0800) != 0
  }

  def flagsWithBit(flagBit: Int, newBitValue: Boolean): Int = {
    if (newBitValue) Flags | (1 << flagBit) else Flags & ~(1 << flagBit)
  }

  def withModification(modification: StateFieldModification): State = modification.field match {
    case StateField.AX => this.copy(AX = modification.value)
    case StateField.AH => this.copy(AX = (this.AX & 0x00FF) | (modification.value << 8))
    case StateField.AL => this.copy(AX = (this.AX & 0xFF00) | modification.value)
    case StateField.CF => this.copy(Flags = flagsWithBit(1, modification.value > 0))
    case StateField.CF => this.copy(Flags = flagsWithBit(12, modification.value > 0))
  }

  def withModifications(message: ModificationMessage) = ???
}

Q#1 - what's the best way to make a field getter based on a "field key"?

Q#2 - what's the best way to make a field setter based on a "field key"?

Q#3 - what's the best way to make a new object, given a mutability message?

Please note that:

  • I'm experienced with Java but just starting with Scala.
  • I'm doing a clean-sheet project, so you're more than welcome to comment about any part of the code, anything can be rewritten.
  • The code above works, but it feels too far from what Scala elegance should look like.
  • I don't want to use reflection, as suggested here: Scala case class copy with dynamic named parameter
  • I don't want to use key-value map of fields instead of real class fields, like Python does.

All help appreciated. Thanks!!!

like image 777
Romi Kuntsman Avatar asked Oct 30 '16 20:10

Romi Kuntsman


3 Answers

Lenses formalise the concept of having getter and non-mutating 'setter' functions for data structures, but we don't need the formalism to gain some of the advantages. We will use inversion of control to create modifier functions for your class. These modifier functions will encode ideas like:

  • 'Give me the current state and a new AX, and I will give you a new state'

  • 'Give me the current state and a function that calculates a new AX from the current AX, and I will give you a new state'

  • 'Give me the current state and the new flags, and I will give you a new state'

So, the code:

case class State(ax: Int, flags: Int) {
  private def setAx(newAx: Int): State = copy(ax = newAx)
  private def modAx(f: Int => Int): State = setAx(f(ax))
  private def setFlags(newFlags: Int): State = copy(flags = newFlags)

  def withModification(modification: StateFieldModification): State =
    modification.field match {
      case StateField.AX => setAx(modification.value)
      case StateField.AH =>
        modAx { ax => (ax & 0x00FF) | (modification.value << 8) }

      case StateField.AL =>
        modAx { ax => (ax & 0xFF00) | modification.value }

      case StateField.CF =>
        setFlags(flagsWithBit(1, modification.value > 0))

      case StateField.CF =>
        setFlags(flagsWithBit(12, modification.value > 0))
    }

  def withModifications(message: ModificationMessage): State =
    message.content.foldLeft(this) { (state, modification) =>
      state withModification modification
    }
}

P.S., you can probably simplify some of your types, e.g. StateField doesn't really need to be a multi-level hierarchy--you can split it up into separate Register and Flag enums; and you can unwrap or type alias ModificationMessage since it's just a list.

like image 93
Yawar Avatar answered Nov 04 '22 08:11

Yawar


You seem to be doing it all wrong. This would be an approach to take in a dynamic language like perl or ruby, but scala isn't like that. You can probably mimic something like that with scala, but it'll be hard enough to make you not want to.

.getDetail is not something you'll often see in scala. If you want to get a person's name, you'd usually just do person.name not, person.getDetail(PersonDetail.Name). Why? Well, why not? There is simply no reason to do the latter when you can do the former.

Likewise for the setter: person.copy(firstName = "foo") works way better than person.withModiciation(PersonDetail.Name, "foo")

The third case is, perhaps, the most complicated. What if you wanted to apply a whole bunch of modifications? Well, I'll, still argue, that something like

val list = List(
  PersonDetail.FirstName -> "foo", 
  PersonDetail.LastName -> "bar", 
  PersonDetail.OtherStuff -> "baz"
) 
person.withModifications(list)

isn't any better than the scala's customary

person.copy(firstName = "foo", lastName = "bar", otherStuff = "baz")
like image 42
Dima Avatar answered Nov 04 '22 08:11

Dima


Have you considered using a Lens library such as Monacle? Lens are functional abstractions that allow you apply functions to part of a data structure, such as your Person case class. A Lens allows you zoom in on part of the structure, so for Person, one could

import monocle.Lens
val firstName = Lens[Person, String]( p => f => p.copy(firstName = f))
val newPerson = firstName.set("someNewName")(person)

In this way each modification in PersonDetail could correspond to a suitable Lens. Lens also support other operations too. For more complex modifications, Lens can be composed as is shown on the Monacle README. Besides calling a set function, they also can modify data based on a current value, to correspond to the FirstNameFirstLetter case.

firstName.headOption.modify(_.toUpper)(person)
like image 36
Alan Effrig Avatar answered Nov 04 '22 08:11

Alan Effrig