as a beginner in Scala - functional way, I'm little bit confused about whether should I put functions/methods for my case class inside such class (and then use things like method chaining, IDE hinting) or whether it is more functional approach to define functions outside the case class. Let's consider both approaches on very simple implementation of ring buffer:
1/ methods inside case class
case class RingBuffer[T](index: Int, data: Seq[T]) { def shiftLeft: RingBuffer[T] = RingBuffer((index + 1) % data.size, data) def shiftRight: RingBuffer[T] = RingBuffer((index + data.size - 1) % data.size, data) def update(value: T) = RingBuffer(index, data.updated(index, value)) def head: T = data(index) def length: Int = data.length }
Using this approach, you can do stuff like methods chaining and IDE will be able to hint methods in such case:
val buffer = RingBuffer(0, Seq(1,2,3,4,5)) // 1,2,3,4,5 buffer.head // 1 val buffer2 = buffer.shiftLeft.shiftLeft // 3,4,5,1,2 buffer2.head // 3
2/ functions outside case class
case class RingBuffer[T](index: Int, data: Seq[T]) def shiftLeft[T](rb: RingBuffer[T]): RingBuffer[T] = RingBuffer((rb.index + 1) % rb.data.size, rb.data) def shiftRight[T](rb: RingBuffer[T]): RingBuffer[T] = RingBuffer((rb.index + rb.data.size - 1) % rb.data.size, rb.data) def update[T](value: T)(rb: RingBuffer[T]) = RingBuffer(rb.index, rb.data.updated(rb.index, value)) def head[T](rb: RingBuffer[T]): T = rb.data(rb.index) def length[T](rb: RingBuffer[T]): Int = rb.data.length
This approach seems more functional to me, but I'm not sure how practical it is, because for example IDE won't be able to hint you all possible method calls as using methods chaining in previous example.
val buffer = RingBuffer(0, Seq(1,2,3,4,5)) // 1,2,3,4,5 head(buffer) // 1 val buffer2 = shiftLeft(shiftLeft(buffer)) // 3,4,5,1,2 head(buffer2) // 3
Using this approach, the pipe operator functionality can make the above 3rd line more readable:
implicit class Piped[A](private val a: A) extends AnyVal { def |>[B](f: A => B) = f( a ) } val buffer2 = buffer |> shiftLeft |> shiftLeft
Can you please summarize me your own view of advance/disadvance of particular approach and what's the common rule when to use which approach (if any)?
Thanks a lot.
case classes automatically have equality and nice toString methods based on the constructor arguments. case classes can have methods just like normal classes.
A case class has all of the functionality of a regular class, and more. When the compiler sees the case keyword in front of a class , it generates code for you, with the following benefits: Case class constructor parameters are public val fields by default, so accessor methods are generated for each parameter.
A class can extend another class, whereas a case class can not extend another case class (because it would not be possible to correctly implement their equality).
The one of the topmost benefit of Case Class is that Scala Compiler affix a method with the name of the class having identical number of parameters as defined in the class definition, because of that you can create objects of the Case Class even in the absence of the keyword new.
In this particular example, the first approach has much more benefits than the second one. I would go with adding all the methods inside the case class.
Here is an example on an ADT where decoupling the logic from the data has some benefits:
sealed trait T case class X(i: Int) extends T case class Y(y: Boolean) extends T
Now you can keep adding logic without needing to change your data.
def foo(t: T) = t match { case X(a) => 1 case Y(b) => 2 }
In addition, all the logic of foo()
is concentrated in a single block, which makes it easy to see how it operates on X and Y (compared to X and Y having their own version of foo
).
In most programs, logic changes much more often than the data, so this approach allows you to add extra logic without ever needing to change/modify existing code (less bugs, less chance of breaking existing code).
Scala gives a lot of flexibility in how you add logic to a class using implicit conversions and the concept of Type Classes. Here are some basic ideas borrowed from ScalaZ. In this example, the data (case class) remains just data and all the logic is added in the companion object.
// A generic behavior (combining things together) trait Monoid[A] { def zero: A def append(a: A, b: A): A } // Cool implicit operators of the generic behavior trait MonoidOps[A] { def self: A implicit def M: Monoid[A] final def ap(other: A) = M.append(self,other) final def |+|(other: A) = ap(other) } object MonoidOps { implicit def toMonoidOps[A](v: A)(implicit ev: Monoid[A]) = new MonoidOps[A] { def self = v implicit def M: Monoid[A] = ev } } // A class we want to add the generic behavior case class Bar(i: Int) object Bar { implicit val barMonoid = new Monoid[Bar] { def zero: Bar = Bar(0) def append(a: Bar, b: Bar): Bar = Bar(a.i + b.i) } }
You can then use these implicit operators:
import MonoidOps._ Bar(2) |+| Bar(4) // or Bar(2).ap(Bar(4)) res: Bar = Bar(6)
Or use Bar in generic functions build around, say, the Monoid Type Class.
def merge[A](l: List[A])(implicit m: Monoid[A]) = l.foldLeft(m.zero)(m.append) merge(List(Bar(2), Bar(4), Bar(2))) res: Bar = Bar(10)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With