web-dev-qa-db-ja.com

scala機能的-ケースクラスの内部または外部のメソッド/関数?

Scalaの初心者として-機能的な方法で、ケースクラスの関数/メソッドをそのようなクラス内に配置する必要があるかどうか(そして、メソッドチェーン、IDEのヒントなど)を使用するかどうかについて少し混乱していますまたは、ケースクラスの外部で関数を定義するための機能的アプローチかどうか。 ring bufferの非常に単純な実装で両方のアプローチを検討してみましょう:

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

特定のアプローチの進歩/不進歩についてのあなた自身の見方と、(もしあれば)どのアプローチを使用する場合の一般的なルールは何ですか?

どうもありがとう。

17
xwinus

この特定の例では、最初のアプローチには2番目のアプローチよりもはるかに多くの利点があります。すべてのメソッドをケースクラスに追加します。

以下は [〜#〜] 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が独自のバージョンのfooを持っているのと比較して)。

ほとんどのプログラムでは、ロジックはデータよりも頻繁に変更されるため、このアプローチにより、既存のコードを変更/修正する必要なく、追加のロジックを追加できます(バグが少ない、既存のコードが壊れる可能性が少ない)。

コンパニオンオブジェクトにコードを追加する

Scalaは、暗黙の変換と型クラスの概念を使用してクラスにロジックを追加する方法に多くの柔軟性を与えます。 ScalaZから借りたいくつかの基本的なアイデアを以下に示します。この例では、データ(ケースクラス)はデータのままで、すべてのロジックがコンパニオンオブジェクトに追加されます。

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

または、たとえばMonoid Type Classなどの一般的な関数でBarを使用します。

_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)
_
5
marios

「クラス外の関数」のアプローチ、たとえば https://www.martinfowler.com/bliki/AnemicDomainModel.html に対する引数と、たとえば「関数型ドメインと反応性ドメインモデリング」 D.ゴーシュ(ch。3)。 ( https://underscore.io/books/essential-scala/ ch。4も参照してください。)私の経験では、いくつかの例外を除いて、前者のアプローチが望ましいです。その利点のいくつかは次のとおりです。

  • 1つのクラスでデータを壊すよりも、データのみまたは動作のみに焦点を合わせる方が簡単です。それらを別々に進化させる
  • 別のモジュールの関数はより一般的な傾向があります
  • よりクリーンなインターフェイス分離(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)
_

2番目は、より明確です。どの間隔にどれが含まれるか。

クラスでデータとメソッドを組み合わせる方が自然なように見える場合があります(特に、UIレイヤーのメディエーター/コントローラーを使用)。次に、class Controller(a: A, b: B)をメソッドとプライベートフィールドで定義して、データのみのケースクラスと区別します。

1
esc_space