scala functional-case类内部或外部的方法/函数?

scala functional-case类内部或外部的方法/函数?,scala,functional-programming,purely-functional,Scala,Functional Programming,Purely Functional,作为Scala-functional方式的初学者,我有点困惑,我是否应该将我的case类的函数/方法放在这样的类中(然后使用方法链接、IDE提示),还是在case类之外定义函数更具功能性。让我们考虑两种方法非常简单地实现环形缓冲区: 1/案例类内的方法 case class RingBuffer[T](index: Int, data: Seq[T]) { def shiftLeft: RingBuffer[T] = RingBuffer((index + 1) % data.size, d

作为Scala-functional方式的初学者,我有点困惑,我是否应该将我的case类的函数/方法放在这样的类中(然后使用方法链接、IDE提示),还是在case类之外定义函数更具功能性。让我们考虑两种方法非常简单地实现环形缓冲区:

1/案例类内的方法

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
}
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
使用这种方法,您可以进行方法链接,IDE将能够在这种情况下提示方法:

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/案例类以外的功能

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
}
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
这种方法对我来说似乎更实用,但我不确定它有多实用,因为例如,在前面的示例中,IDE不能像使用方法链接一样提示您所有可能的方法调用

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
使用此方法,管道操作符功能可以使上述第三行更具可读性:

implicit class Piped[A](private val a: A) extends AnyVal {
  def |>[B](f: A => B) = f( a )
}

val buffer2 = buffer |> shiftLeft |> shiftLeft

你能总结一下你对特定方法的优缺点的看法吗?什么时候使用哪种方法(如果有的话)有什么共同的规则


非常感谢。

在这个例子中,第一种方法比第二种方法有更多的好处。我将在case类中添加所有方法

以下是一个将逻辑与数据分离的示例:

sealed trait T
case class X(i: Int) extends T
case class Y(y: Boolean) extends T
现在,您可以继续添加逻辑,而无需更改数据

def foo(t: T) = t match {
   case X(a) => 1
   case Y(b) => 2 
}
此外,
foo()
的所有逻辑都集中在一个块中,这使得很容易看到它如何在X和Y上运行(相比之下,X和Y有自己版本的
foo

在大多数程序中,逻辑更改的频率比数据更改的频率要高得多,因此这种方法允许您添加额外的逻辑,而无需更改/修改现有代码(更少的错误,破坏现有代码的可能性更小)

将代码添加到伴随对象中 Scala在如何使用隐式转换和类型类的概念向类添加逻辑方面提供了很大的灵活性。下面是从ScalaZ那里借用的一些基本想法。在本例中,数据(case类)只保留数据,所有逻辑都添加到伴随对象中

// 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)
  }
}
然后可以使用这些隐式运算符:

import MonoidOps._
Bar(2) |+| Bar(4)  // or Bar(2).ap(Bar(4))
res: Bar = Bar(6)
或者在泛型函数中使用Bar构建,比如说,Monoid类型类

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)

对于D.Ghosh(第3章)提出的“类外函数”方法和“函数和反应域建模”方法,都有反对意见。(另见第4章。) 根据我的经验,除少数例外,前者更可取。它的一些优点是:

  • 只关注数据或只关注行为比在一个类中处理数据更容易;并分别对它们进行进化
  • 单独模块中的函数往往更通用
  • Cleaner interface Separation(ISP):当客户端只需要数据时,它不应该暴露于行为
  • 更好的组成性。比如说,

     case class Interval(lower: Double, upper: Double)
    
     trait IntervalService{ 
    def contained(a: Interval, b: Interval) }
    object IntervalService extends IntervalService
    trait MathService{ //methods}
    
    仅由
    对象MathHelper与MathService扩展IntervalService组成
    。对于行为丰富的阶级来说,这并不是那么简单

所以通常我会为数据保留case类;工厂和验证方法的配套对象;以及其他行为的服务模块。我可以在一个case类中放置几个方法来促进数据访问:
def row(I:Int)
,用于一个带有表的类。(事实上,OP的例子与此类似。)

缺点是:额外的课程/特质是必要的;客户机可能需要类实例和服务对象;方法定义可能会混淆:例如,在

import IntervalService._
contains(a, b)
a.contains(b)
第二个是更清楚的w.r.t.哪个区间包含哪个区间


有时,在类中组合数据和方法似乎更自然(尤其是在UI层中使用中介/控制器)。然后我将用方法和私有字段定义
类控制器(a:a,b:b)
,以区别于仅数据的案例类

在IMO中,这太主要是基于观点的。您还可以定义第三种方法,其中隐式定义了方法(可能在此类的伴奏中)。一旦它们在作用域中可用,您将获得IDE完成,就好像它们是在类本身上定义的一样。对于case类,我通常喜欢在它们的伴生对象中定义方法,使类本身尽可能干净。@YuvalItzchakov您能提供一些非常简单的例子来说明您使用伴生对象的方法吗?在我的环形缓冲区实现的情况下,您仍然必须将case类实例作为参数传递给在伴随对象中定义的方法/函数(例如,
def head[T](rb:RingBuffer[T])
),对吗?取决于您如何创建这些方法。例如,如果使用
隐式类
,则可以像扩展方法一样使用这些方法。参见示例。