Saturday, October 24, 2009

Clone objects with arguments override in Scala 2.8: best practices ?

I'm actively looking for best practices and patterns about how to create clone objects in Scala, with the possibility to change some values of the cloned object along the way.

I have the usual concern of the Java world on that topic: I would like to avoid Java "clone", I want to be able to use the pattern in a class hierarchy, I want to have the minimum amount of code (or at least, the simplest possible) to write in domain class to support my cloning API.
And as I'm in Scala, I want to deal with both vars and vals, if possible in the same consistent way.

Actually, I want to provide to my libraries consumer an effective way to do things like that (where *Child* extends *Parent* class):



    val p = new Parent("some value")
    // simple cloning
    val p2 = p.copy
    //clone, and change a val and a var along the way
    val p3 = p.copy(p_val1 = "new value for val", p_var1 = "new value for var")

    //and same things apply for children of Parent:
    val c = new Child("some parent value", "some child value")
    //override child val/var and/or parent val/var in the same fashion
    val c1 = c.copy(p_val1 = "new value for val defined in Parent class",
                    c_val1 = "new value for val defined in Child class",
                    p_var1 = "new value for var defined in Parent class",
                    c_var1 = "new value for var defined in Child class" )


As Scala 2.8 is advertised with "default and named arguments", such things should possible (well, hopefully easy too ;). I came with this solution:



class A(val val_a:String)  {
    var var_a = ""
}

object A {
    def merge[T](t:T)
    (
        var_a : String
    ) : T = {
        t match {
            case x:A => x.var_a = var_a
            case _ => error("")
        }
        t
    }
    
    def copy(source:A)
    (
        val_a : String = source.val_a, 
        var_a : String = source.var_a
    ) : A = merge(new A(val_a))(var_a)
}

class B(override val val_a:String, val val_b : Int) extends A(val_a) {
    var var_b = 0
}

object B {
    def merge[T](t:T)
    (
        var_a : String,
        var_b : Int
    ) : T = {
        A.merge(t)(var_a) match {
            case x:B => x.var_b = var_b
            case _ => error("")
        }
        t
    }
    
    def copy(source:B)
    (
        val_a : String = source.val_a,
        var_a : String = source.var_a,
        val_b : Int = source.val_b,
        var_b : Int = source.var_b
    ) : B = merge(new B(val_a,val_b))(var_a,var_b)
}


class C(override val val_a : String) extends A(val_a)

// ***************** 
// Example of use
// ***************** 

object TestCloning {
    
    def main(args:Array[String]) {
        val a1 = new A("la_init")
        a1.var_a = "ra_init"
            
        val b1 = new B("lb_init",1)
        b1.var_a = "rb_init"
        b1.var_b = 10

        //example with A

        val a2 = A.copy(a1)()
        assert(!(a1 eq a2))
        assert(a2.var_a == a1.var_a)
        assert(a2.val_a == a1.val_a)
        
        val a3 = A.copy(a1)(val_a = "la_mod")
        assert(a3.var_a == a1.var_a)
        assert(a3.val_a == "la_mod")
        
        val a4 = A.copy(a1)(val_a = "la_mod", var_a = "ra_mod")
        assert(a4.val_a == "la_mod")
        assert(a4.var_a == "ra_mod")

        val a5 = A.copy(new C("foo"))()
        assert(a5.var_a == "")
        assert(a5.val_a == "foo")

        val a6 = A.copy(b1)()
        assert(a6.val_a == "lb_init")
        assert(a6.var_a == "rb_init")
        
        //with B
            
        val b2 = B.copy(b1)()
        assert(b2.val_a == "lb_init")
        assert(b2.var_a == "rb_init")
        assert(b2.val_b == 1)
        assert(b2.var_b == 10)
            
        val b3 = B.copy(b1)(var_b = 5, val_a = "lb_mod")
        assert(b3.val_a == "lb_mod")
        assert(b3.var_a == "rb_init")
        assert(b3.val_b == 1)
        assert(b3.var_b == 5)
            
    }

That solution seems to work, and allows to do the kind of use case I wanted to build.
But it seems not satisfactory at all for (at least) these reasons:
1/ - the trailing "()" in real clone without override is mandatory and doesn't look right;
2/ - it seems to be a lot of tedious code to write in each class, and so seems to be really error prone;
3/ - I'm not at ease with the merge method, but I can't say why exactly;
4/ - I don't like to much the factory approach.

1/ could be easily addressed if in place of only a copy(source:X)(.....) we define a couple of method :
    def copyWith(source:A)( ... ) : A = merge(...)(...)
    def copy(source:A) = copyWith(source)()

3/ and 4/ are just feelings, and so could be shut up for now.

But 2/ is a real concern, and I would like to find a way to externalize more code in traits, so that domain classes have only simple, short code to write to provide a clone API.
 
So Scalazy, what are your best practices about the cloning topic in Scala ?

3 comments:

Aaron Harnly

Umm, have you looked at the copy
method that is defined automatically on case classes?

Fanf

No, but I didn't find it (any pointer ?).
But this method seems to have rather important limitation, as it is not generated if a supper-class has a "copy" method, and inheritance management is one of the hard part of the problem.

Nonetheless, I will try to look at it.

Dean Wampler

The copy method is a 2.8 addition for case classes. Note that copying is tricky to get right for class hierarchies, but case classes in general should not be subclassed by other case classes (due to similar problems with equals, etc. under inheritance).

  © Blogger template 'Minimalist G' by Ourblogtemplates.com 2008

Back to TOP