Scala: implementing method with return type of concrete instance

前端 未结 4 530
北恋
北恋 2021-01-30 07:23

I need a way to enforce a method in an abstract class to have a return type of the concrete class of the object it is called on. The most common example is a copy

相关标签:
4条回答
  • 2021-01-30 07:49

    Do not force the type bound on the declaration side, unless you need that bound within the definition of A itelf. The following is sufficient:

    abstract class A(id: Int) {
      type Self
      def copy(newId: Int): Self
    }
    

    Now force the Self type on the use site:

    def genNewId(): Int = ???
    def createCopies[A1 <: A { type Self = A1 }](seq: Seq[A1]): Seq[A1] = 
      seq.map(_.copy(genNewId()))
    
    0 讨论(0)
  • 2021-01-30 07:49

    I don't think it's possible in scala to do what you want.

    If I were to:

    class Base { type A }
    class Other extends Base
    class Sub extends Other
    

    Now... we want type A to refer to "the type of the subclass."

    You can see that from the context of Base, it's not particularly clear (from the compiler's perspective) what the specific "type of the subclass" means, nevermind what the syntax would be to refer to it in the parent. In Other it would mean an instance of Other, but in Sub it might mean an instance of Sub? Would it be OK to define an implementation of your method returning an Other in Other but not in Sub? If there are two methods returning A's, and one is implemented in Other and the other in Sub, does that mean the type defined in Base has two different meanings/bounds/restrictions at the same time? Then what happens if A is referred to outside of these classes?

    The closest thing we have is this.type. I'm not sure if it would be theoretically possible to relax the meaning of this.type (or provide a more relaxed version), but as implemented it means a very specific type, so specific that the only return value satisfying def foo:this.type is this itself.

    I'd like to be able to do what you suggest, but I'm not sure how it would work. Let's imagine that this.type meant... something more general. What would it be? We can't just say "any of the defined types of this," because you wouldn't want class Subclass with MyTrait{type A=MyTrait} to be valid. We could say "a type satisfying all of the types of this," but it gets confusing when someone writes val a = new Foo with SomeOtherMixin... and I'm still not sure it could be defined in a way that would enable an implementation of both Other and Sub defined above.

    We're sort-of trying to mix static and dynamically defined types.

    In Scala, when you say class B { type T <: B }, T is specific to the instance, and B is static (I'm using that word in the sense of static methods in java). You could say class Foo(o:Object){type T = o.type}, and T would be different for every instance.... but when you write type T=Foo, Foo is the statically specified type of the class. You could just as well have had an object Bar, and had referred to some Bar.AnotherType. The AnotherType, since it's essentially "static," (though not really called "static" in Scala), doesn't participate in inheritance in Foo.

    0 讨论(0)
  • 2021-01-30 07:51

    However, none of them really forces a implementation to return its own type. For example, the following classes would be valid.

    But isn't it normal? Otherwise it would mean that you could not merely extend A to add a new method by example, as it would automatically break the contract that you are trying to create (that is, the new class's copy would not return an instance of this class, but of A). The very fact of being able to have a perfectly fine class A that breaks as soon as you extend it as class B feels wrong to me. But to be honest I have trouble putting words on the problems it causes.

    UPDATE: After thinking a bit more about this, I think this could be sound if the type check ("return type == most-derived class") was made only in concrete classes and never on abstract classes or traits. I am not aware of any way to encode that in the scala type system though.

    The fact that I can do that causes that, if I am doing copies of objects of which the only information I have is that they are of a given subclass of A's

    Why can't you just return a Seq[Ca#Self] ? By example, with this change passing a list of B to createCopies will as expected return a Seq[B] (and not just a Seq[A]:

    scala> def createCopies[CA <: A](seq: Seq[CA]): Seq[CA#Self] = seq.map(_.copy(123))
    createCopies: [CA <: A](seq: Seq[CA])Seq[CA#Self]
    
    scala> val bs = List[B]( new B(1, "one"), new B(2, "two"))
    bs: List[B] = List(B@29b9ab6c, B@5ca554da)
    
    scala> val bs2: Seq[B] = createCopies(bs)
    bs2: Seq[B] = List(B@92334e4, B@6665696b)
    
    0 讨论(0)
  • 2021-01-30 07:52

    The only solution I could think of was this one:

    trait CanCopy[T <: CanCopy[T]] { self: T =>
      type Self >: self.type <: T
      def copy(newId: Int): Self
    }
    
    abstract class A(id: Int) { self:CanCopy[_] =>
      def copy(newId: Int): Self
    }
    

    The following would compile:

    class B(id: Int, x: String) extends A(id) with CanCopy[B] {
      type Self = B
      def copy(newId: Int) = new B(newId, x)
    }
    
    class C(id: Int, y: String, z: String) extends A(id) with CanCopy[C] {
      type Self = C
      def copy(newId: Int) = new C(newId, y, z)
    }
    

    The following would not compile:

    class D(id: Int, w: String) extends A(id) with CanCopy[D] {
      type Self = A
      def copy(newId: Int) = new D(newId, w) // returns an A
    }
    
    class E(id: Int, v: String) extends A(id) with CanCopy[E] {
      type Self = B
      def copy(newId: Int) = new B(newId, "")
    }
    

    Edit

    I actually forgot to remove the copy method. This might be a bit more generic:

    trait StrictSelf[T <: StrictSelf[T]] { self: T =>
      type Self >: self.type <: T
    }
    
    abstract class A(id: Int) { self:StrictSelf[_] =>
      def copy(newId:Int):Self
    }
    
    0 讨论(0)
提交回复
热议问题