scala功能 - 案例类内外的方法/函数?

时间:2016-06-25 07:04:11

标签: scala functional-programming purely-functional

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

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
}

使用这种方法,您可以执行类似方法链接的操作,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[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

使用这种方法,管道运算符功能可以使上面的第3行更具可读性:

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

val buffer2 = buffer |> shiftLeft |> shiftLeft

您能否总结一下您对特定方法的进展/不满意的看法,以及何时使用哪种方法(如果有的话)的常见规则?

非常感谢。

2 个答案:

答案 0 :(得分:3)

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

以下是ADT的示例,其中将逻辑与数据分离具有一些好处:

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相比,它们有{{1}版本})。

在大多数程序中,逻辑变化比数据更频繁,因此这种方法允许您添加额外的逻辑,而无需更改/修改现有代码(更少的错误,更少破坏现有代码的机会)。

将代码添加到配套对象

Scala为使用隐式转换和类型类概念向类添加逻辑提供了很大的灵活性。以下是从ScalaZ借来的一些基本想法。在此示例中,数据(案例类)仅保留数据,并且所有逻辑都添加在伴随对象中。

foo

然后您可以使用这些隐式运算符:

// 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)
  }
}

或者在泛型函数中使用Bar,比如Monoid Type Class。

import MonoidOps._
Bar(2) |+| Bar(4)  // or Bar(2).ap(Bar(4))
res: Bar = Bar(6)

答案 1 :(得分:0)

反对“类外的函数”方法的论据,例如https://www.martinfowler.com/bliki/AnemicDomainModel.html,以及:例如D. Ghosh的“功能和反应域建模”(第3章)。 (另见https://underscore.io/books/essential-scala/ ch.4。) 根据我的经验,除了少数例外,前一种方法更可取。它的一些优点是:

  • 更容易专注于数据或仅仅关注行为,而不是仅仅在一个类中对它们进行处理;并分别进化它们
  • 单独模块中的功能往往更为通用
  • 清理接口隔离(ISP):当客户端只需要数据时,不应该将其暴露给行为
  • 更好的组合性。例如,

     case class Interval(lower: Double, upper: Double)
    
     trait IntervalService{ 
    def contained(a: Interval, b: Interval) }
    object IntervalService extends IntervalService
    trait MathService{ //methods}
    

    简单地组成object MathHelper extends IntervalService with MathService。对于行为丰富的类,它并不那么简单。

所以通常我会保留数据的案例类;工厂和验证方法的配套对象;和其他行为的服务模块。我可以在案例类中使用一些方法来促进数据访问:def row(i:Int)用于具有表的类。 (事实上​​,OP的例子似乎与此相似。)

有缺点:需要额外的课程/特质;客户端可以要求类实例和服务对象;方法定义可能令人困惑:例如,

import IntervalService._
contains(a, b)
a.contains(b)

第二个更清楚w.r.t.哪个区间包含哪个。

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