Reputation: 1991
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
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
Reputation: 139
You can use type variable in your trait
Something like this
trait TableElement{
type T
def +(t:T):T
}
Upvotes: 0
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