How do I make a deep or selective copy of an object?

← Classes and OOP · Ref: Q2100

THE COPY PRINCIPLE

- 'default operator :=:' is a SHALLOW copy: after 'target :=: source' each of target's properties refers to the SAME object as source's - exactly what 'b <- a' and passing a parameter do. Change a shared object in place ('points += 2') and both see it.
- A built-in collection copies at its OWN level: 'list :=: other' gives list its own container holding the same element objects. A value type copies its value: 'year :=: other.year'. Integer(x), String(x) and your own copy constructors make new objects.
- Anything deeper or SELECTIVE is your code: write 'operator :=:' yourself, or a domain method, and decide per property - duplicate it, share it, or leave it out. No generator can know which, so EK9 does not guess. There is no clone().

The program below renews a car insurance policy for the next period. The new period's policy:
- DUPLICATES the per-holder data: each named driver is a new Driver (Driver(driver)), so the new period can add a driver or penalty points without touching last period's record;
- SHARES the insurer's standard wordings: the same Wording objects, in the new policy's own List ('wordings :=: other.wordings'), so a correction to a wording reaches every policy that uses it;
- DROPS the endorsements that have expired.

The copying rules live in a hand-written 'operator :=:'; the renewal rules (next period, expired endorsements) live in 'renewForNextPeriod()'. The original period is printed after the renewal is changed: it is untouched, apart from the wording it shares on purpose.

Example

defines module qa.classes.deeporselectivecopy

  defines class

    <?- Per-holder data: each period has its own -?>
    Driver
      name as String: String()
      points as Integer: Integer()

      default private Driver()

      Driver()
        ->
          driverName as String
          driverPoints as Integer
        name :=: driverName
        points :=: driverPoints

      <?- A copy constructor: new objects for every property -?>
      Driver()
        -> other as Driver
        name :=: other.name
        points :=: other.points

      named() as pure
        -> driverName as String
        <- rtn as Boolean: name == driverName

      addPoints()
        -> morePoints as Integer
        points += morePoints

      operator $ as pure
        <- rtn as String: `${name}(${points})`

      default operator ?

    <?- The insurer's standard text: shared by every policy that uses it -?>
    Wording
      text as String: String()

      default private Wording()

      Wording()
        -> wordingText as String
        text :=: wordingText

      revise()
        -> revisedText as String
        text :=: revisedText

      operator $ as pure
        <- rtn as String: text

      default operator ?

    Endorsement
      text as String: String()
      lastPeriod as Integer: Integer()

      default private Endorsement()

      Endorsement()
        ->
          endorsementText as String
          endorsementLastPeriod as Integer
        text :=: endorsementText
        lastPeriod :=: endorsementLastPeriod

      coversPeriod() as pure
        -> periodToCheck as Integer
        <- rtn as Boolean: periodToCheck <= lastPeriod

      operator $ as pure
        <- rtn as String: `${text} to ${lastPeriod}`

      default operator ?

    Policy
      holder as String: String()
      period as Integer: Integer()
      drivers as List of Driver: List() of Driver
      wordings as List of Wording: List() of Wording
      endorsements as List of Endorsement: List() of Endorsement

      default private Policy()

      Policy()
        ->
          policyHolder as String
          policyPeriod as Integer
        holder :=: policyHolder
        period :=: policyPeriod

      addDriver()
        -> driver as Driver
        drivers += driver

      addWording()
        -> wording as Wording
        wordings += wording

      addEndorsement()
        -> endorsement as Endorsement
        endorsements += endorsement

      addPointsTo()
        ->
          driverName as String
          morePoints as Integer
        for driver in drivers
          if driver.named(driverName)
            driver.addPoints(morePoints)

      <?- The next period: a copy of this policy, moved on a period, without what has expired -?>
      renewForNextPeriod()
        <- renewed as Policy: Policy()
        renewed :=: this
        renewed.startNextPeriod()

      private startNextPeriod()
        period++
        stillInForce <- List() of Endorsement
        for endorsement in endorsements
          if endorsement.coversPeriod(period)
            stillInForce += endorsement
        endorsements: stillInForce

      describe() as pure
        <- rtn as String: `${period} ${holder} drivers=${drivers} wordings=${wordings} endorsements=${endorsements}`

      <?- A selective copy: duplicate the per-holder data, share the standard wordings -?>
      operator :=:
        -> other as Policy
        holder :=: other.holder
        period :=: other.period
        drivers: List() of Driver
        for driver in other.drivers
          drivers += Driver(driver)
        wordings :=: other.wordings
        endorsements :=: other.endorsements

      default operator ?

  defines program

    RenewPolicy()
      stdout <- Stdout()

      standardTerms <- Wording("Standard terms v1")

      current <- Policy("Pat", 2026)
      current.addDriver(Driver("Alex", 3))
      current.addWording(standardTerms)
      current.addEndorsement(Endorsement("Courtesy car", 2026))
      current.addEndorsement(Endorsement("Protected bonus", 2028))

      renewal <- current.renewForNextPeriod()
      renewal.addDriver(Driver("Sam", 0))
      renewal.addPointsTo("Alex", 2)

      stdout.println(current.describe())
      stdout.println(renewal.describe())

      standardTerms.revise("Standard terms v2")
      stdout.println(current.describe())
      stdout.println(renewal.describe())

Common mistakes

E50060 — EK9 has no clone(): no generated method can know which properties to duplicate, share or drop. Write the copy yourself - an 'operator :=:' or a domain method such as renewForNextPeriod(). See ek9 -h E50060 for details.

Incorrect:

      renewal <- current.clone()

Correct:

      renewal <- current.renewForNextPeriod()
Other ways to ask this
  • Is 'default operator :=:' a deep copy or a shallow copy?
  • How do I copy an object so that changing the copy does not change the original?
  • How do I clone an object in EK9?
  • How do I copy some fields, share others and leave some out?

Coming from another language?

Java: clone() is shallow and Cloneable is widely considered broken; deep copies are hand-written copy constructors or serialisation tricks. C#: MemberwiseClone() is shallow. Python: copy.copy() is shallow, copy.deepcopy() copies everything reachable (it cannot share or drop selectively). C++: the implicit copy constructor copies members, so a pointer member is shared. EK9: 'default operator :=:' is shallow, a collection copies at its own level, and a deep or selective copy is an 'operator :=:' or domain method you write.

Keywords: copy constructor, renewal, selective copy, copy, shared reference, shallow copy, deep copy, operator :=:, aliasing, clone, :=:, default operator