【问题标题】:Scala immutable objects and traits with val fields具有 val 字段的 Scala 不可变对象和特征
【发布时间】:2011-03-28 10:35:21
【问题描述】:

我想只使用不可变对象来构建我的域模型。但我也想将特征与 val 字段一起使用,并将一些功能移至特征。请看下面的例子:

trait Versionable {
 val version = 0
 def incrementVersion = copy(version=version+1)
}

不幸的是,这样的代码不起作用 - trait Versionable 的复制方法未知。

我认为为每个特征和类生成复制方法会很好。这种方法应该创建对象的浅表副本,并使用与原始对象相同的类型返回它,并根据传递给方法的参数修改给定字段。

所以在下面的例子中:

class Customer(val name: String) extends Versionable {
 def changeName(newName: String) = copy(name = newName)
}

val customer = new Customer("Scot")

customer.changeName("McDonnald") 应该返回一个对象实例Customer(version = 0, name = "McDonnald")

customer.incrementVersion 也应该返回一个对象实例Customer(version = 1, name = "Scot")

据我所知,目前 Scala 中缺乏此类功能,不允许使用不可变类和特征,而不会污染带有特征字段的类构造函数。在我的示例中,我不想将名为 version 的参数引入 Customer 类,因为我希望将版本处理的功能封装在 Versionable trait 中。

我知道案例类中复制方法的功能以及使用默认参数在类中编写自己的复制方法的能力 - 但我认为这个功能不能解决我的问题,因为不可能使用这种复制方法在特质。现有功能的另一个缺点是使用复制方法的父类返回父类,而不是实际复制的对象的类。

我的问题:

1) 你知道如何优雅地处理上面的例子吗?我对 Scala 很陌生,所以也许已经有了很好的解决方案。在我看来,优雅的解决方案应该具有以下特点:

  • 不应使用反射

  • 不应使用序列化

  • 应该很快

  • 应该在编译时可验证

2) 对于我上面的例子,你对编写编译器插件来生成复制方法的代码有什么看法?是否可以使用编译器插件来做到这一点?您有任何示例或提示吗?

【问题讨论】:

  • 您是否知道编译器赋予案例类以 copy 方法,例如您所描述的?案例类不能(从技术上讲,不应该)从其他案例类派生。特别是,派生的案例类将继承超(案例)类的copy 方法。
  • 是的,我知道这一点,并且不会使用案例类来解决所描述的问题。我正在尝试寻找解决方案来编写具有特征的不可变类,这些特征不仅是 Java 中的接口,而且还增加了实现。此类特征中的“Mutator”方法应返回具有修改的“变异”字段的对象副本,但如何以优雅的方式做到这一点?
  • 也许这个问题可以为您提供一些有用的方法来处理这个问题:stackoverflow.com/questions/3007464/…
  • 我添加了一个相关问题:stackoverflow.com/questions/3471708/… 这也与我下面的(非工作)解决方案有关。

标签: scala immutability scala-2.8 traits


【解决方案1】:

很难看出这将如何工作并且与 Scala 的语义保持一致——特别是在 trait 中定义的不可变字段的语义。考虑 Versionable 特征:

trait Versionable {
   val version = 0
}

该声明表明,除非被覆盖,否则版本字段的值将始终为 0。更改 version 的值“不使用 trait 字段污染类构造函数”(即不显式覆盖版本字段)将违反这些语义。

【讨论】:

    【解决方案2】:

    尽管您说过,您不想使用案例类。这是使用它们的解决方案:

    case class Version(number: Int) {
      override def toString = "v" + number
      def next = copy(number+1)
    }
    
    case class Customer(name: String, version: Version = Version(0)) {
      def changeName(newName: String) = copy(newName)
      def incrementVersion = copy(version = version.next)
    }
    

    现在你可以这样做了:

    scala> val customer = new Customer("Scot")
    customer: Customer = Customer(Scot,v0)
    
    scala> customer.changeName("McDonnald")
    res0: Customer = Customer(McDonnald,v0)
    
    scala> customer.incrementVersion
    res1: Customer = Customer(Scot,v1)
    
    scala> customer // not changed (immutable)
    res2: Customer = Customer(Scot,v0)
    

    【讨论】:

      【解决方案3】:

      这是另一种解决方案,就像 OP 的代码一样,它不起作用。但是,它可以为扩展语言提供一个更简单(并且更普遍有用)的起点。

      trait Versionable[T] {
         self: { def copy(version: Int): T } =>
         val version = 0
         def incrementVersion = copy(version = version + 1)
      }
      
      case class Customer(name: String, override val version: Int) 
            extends Versionable[Customer] {
         def changeName(newName: String) = copy(name = newName)
      }
      

      如果编译器将 Customer 类的 copy 方法识别为符合 Versionable 的自类型注释中定义的方法(这似乎是使用命名参数和默认参数的自然方式),则代码将起作用。

      【讨论】:

        【解决方案4】:

        您最干净的解决方案可能是从Versionable 中删除一些实现逻辑,并将其从类型堆栈下推到案例类(您可以使用copy 方法)。给版本属性一个默认值来完成设计。

        trait Versioned {
          def version : Int
          def nextVersion = version + 1 
        }
        
        case class Customer(name: String, version : Int = 0) extends Versioned {
          def withName(newName: String) = copy(name = newName, version = nextVersion)
        }
        

        如果需要,您还可以在某处为版本编号定义类型别名:

        type Version = Int
        val initialVersion = 0
        
        trait Versioned {
          def version : Version
          def nextVersion = version + 1 
        }
        
        case class Customer(name: String, version : Version = initialVersion)
        extends Versioned {
          def withName(newName: String) = copy(name = newName, version = nextVersion)
        }
        

        【讨论】:

          【解决方案5】:

          这应该可以满足您的需求:

          trait Request[T <: Request[T]] extends Cloneable {
            this: T =>
            private var rets = 0
            def retries = rets
            def incRetries:T = {
              val x = super.clone().asInstanceOf[T]
              x.rets = rets + 1
              x
            }
          }
          

          那么你就可以像这样使用它了

          case class Download(packageName:String) extends Request[Download]
          val d = Download("Test")
          println(d.retries) //Prints 0
          val d2 = d.incRetries
          println(d2.retries) //Prints 1
          println(d.retries) //Still prints 0   
          

          【讨论】:

          • 哇,这太疯狂了。据我所知,这具有不可变的行为。但我只是想仔细检查一下:这个解决方案是否存在线程安全问题?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-12-11
          • 2011-09-08
          • 1970-01-01
          • 1970-01-01
          • 2012-11-22
          相关资源
          最近更新 更多