Sebastian N.
Sebastian N.

Reputation: 1991

Generic value-classes in Scala

Let's say I have a type Pos (for position). In order to gain type-safety the column/row is not represented as Int but by types Col (column) and a Row:

case class Pos(col: Col, row: Row) {
  def +(other: Pos): Pos = Pos(col + other.col, row + other.row)
}

It's possible to add two positions, which consists of summing columns and rows respectively.

The definition of types Col and Row would look like this:

object Row {
  def apply(value: Int) = new Row(value)
  val zero = new Row(0)
}

object Col {
  def apply(value: Int) = new Col(value)
  val zero = new Col(0)
}

class Row(val value: Int) extends AnyVal {
  def +(other: Row): Row = Row(this.value + other.value)
}

class Col(val value: Int) extends AnyVal {
  def +(other: Col): Col = Col(this.value + other.value)
}

This is all fine, but I have the feeling of repeating myself. The definitions are almost identical.

Could I do something to generalize them?

Upvotes: 1

Views: 754

Answers (3)

Rich Henry
Rich Henry

Reputation: 1849

If you introduce Scalaz and create Monoid instances for Row and Col, you may not reduce your boilerplate, but it would shorten your definition of zero and append some:

case class Col(i: Int) extends AnyVal
case class Row(i: Int) extends AnyVal

implicit object rowMonoid extends Monoid[Row] {
  def zero = Row(0)
  def append(a: Row, b: => Row) = Row(a.i |+| b.i)
}

implicit object colMonoid extends Monoid[Col] {
  def zero = Col(0)
  def append(a: Col, b: => Col) = Col(a.i |+| b.i)
}  

And Monoids are composable, so if you stored Rows and Cols in a map, or tuple or the like, you could just compose them, without hitting the individual elements:

val pt1 = (Row(4), Col(15))
val pt2 = (Row(14), Col(5))

val res = pt1 |+| pt2
println(res) // (Row(18),Col(20))

I think simplifying the usage will save you more code overall than worrying about trimming down the definitions, assuming Row and Col are used and added often.

Upvotes: 2

Lucky Libora
Lucky Libora

Reputation: 139

You can use type variable in your trait

Something like this

trait TableElement{
  type T
  def +(t:T):T  
}

Upvotes: 0

Makis Arvanitis
Makis Arvanitis

Reputation: 1195

You can define a common trait for Both Row and Col classes:

trait Element {
  val value : Int
  def init(value: Int): Element
  def +(other: Element) = init(value + other.value)
}

and then use case classes so that you take advantage of the companion object's apply method:

case class Row(value: Int) extends Element {
  def init(v: Int) = Row(v)
}

case class Col(value: Int) extends Element {
  def init(v: Int) = Col(v)
}

So now you can add them like that:

case class Pos(col: Element, row: Element) {
  def +(other: Pos): Pos = Pos(col + other.col, row + other.row)
}

val p1 = Pos(Col(1), Row(2))
val p2 = Pos(Col(1), Row(2))   
p1 + p2 //res2: Pos = Pos(Col(2),Row(4))

However, this allows to create a position with only rows

val p3 = Pos(Row(2), Row(3))    
p1 + p3 //res3: Pos = Pos(Col(3),Row(5))

So a second step is to bound your Element type's + method.

trait Element[T <: Element[_]] {
  val value : Int
  def init(value: Int): Element[T]
  def +(other: Element[T]) = init(value + other.value)
}
case class Row(value: Int) extends Element[Row] {
  def init(v: Int) = Row(v)
}
case class Col(value: Int) extends Element[Col] {
  def init(v: Int) = Col(v)
}
case class Pos(col: Element[Col], row: Element[Row]) {
  def +(other: Pos): Pos = Pos(col + other.col, row + other.row)
}

What you get is that now a row should only add elements of a row type and a Col should only add elements of a Col type. You can still add two positions:

val p1 = Pos(Col(1), Row(2))
val p2 = Pos(Col(1), Row(2))
p1 + p2 //res0: Pos = Pos(Col(2),Row(4))

but this will not compile:

val p3 = Pos(Row(2), Row(3))

Upvotes: 1

Related Questions