The anatomy of a class

A class is its private properties, the constructors that give them values, the operator that says when it counts as 'set', and, when it needs one, a single definition of sameness. The compiler checks all four.

Why

A class in EK9 is built from four things, and the compiler checks each: what it holds, how an instance is first made, when an instance counts as 'set', and, if the class defines one, what 'the same' means. Nothing is left to convention, because convention is what fails under a deadline.

What it holds. Properties are always private, and a property written 'as T?' has no value until a constructor gives it one. The compiler then checks every constructor. A property that any public constructor leaves unset is an error (E08060); only a private constructor may leave properties unset, and it can then be used only from another constructor that completes the object. Where the properties have no initial value and the class has constructors of its own, the no-argument constructor the compiler would otherwise provide must be declared 'default private' (E07175), because a public one would hand out an Account with no owner.

What 'set' means. A class with properties must say when one of its objects counts as set: 'default operator ?' (set when every property is set), or an 'operator ?' of its own (E07235). EK9 will not guess, because every guard, every 'if x?' and every stream stage asks that question of every value.

Sameness is defined once. Choose '<=>' and the compiler generates '==', '<>', '<', '<=', '>' and '>=' from it; choose '==' when only equality makes sense and it generates '<>'. Writing them yourself, or asking for them with 'default operator', is E07181. Two comparisons that can disagree about the same pair of objects cannot be written, so they cannot be in the program.

A class is closed unless it says 'as open' (E05030). Extension is a design decision, not a default: an open class promises that every subclass can stand in for it, and EK9 asks for that promise to be made out loud.

None of this is style advice. It is the same conviction as the rest of EK9: a class that leaves a property unset, or disagrees with itself about equality, is not something to catch in review. It does not compile.

The correct form

#!ek9
defines module site.classes.anatomy

  defines class

    //A class is closed unless it says 'as open' - SavingsAccount below extends it.
    Account as open
      //Properties are always private; 'as T?' means "no value until a constructor gives it one".
      owner as String?
      balance as Integer?

      //The properties have no initial value, so a no-argument constructor must be private:
      //nothing outside the class can make an Account with no owner.
      default private Account()

      //A constructor's parameters go in one '->' block; 'this.x: x' copies each into its property.
      Account()
        ->
          owner as String
          balance as Integer
        this.owner: owner
        this.balance: balance

      //A method that changes the object.
      deposit()
        -> amount as Integer
        balance += amount

      //A method that returns a value: there is no 'return', the result is the '<-' variable.
      describe() as pure
        <- rtn as String: `${owner} holds ${balance}`

      //'?' (set when every property is set), '<=>' (sameness - '==', '<' and the rest are generated
      //from it), '#?' (goes with sameness) and '$' (the String form).
      default operator ?
      default operator <=>
      default operator #?
      default operator $

    SavingsAccount extends Account
      rate as Integer?

      SavingsAccount()
        ->
          owner as String
          balance as Integer
          rate as Integer
        super(owner, balance)
        this.rate: rate

      //'override' is required to replace a method of the super class; 'super.' calls the original.
      override describe() as pure
        <- rtn as String: `${super.describe()} at ${rate}%`

      //A sub class with its own properties gives its own '?', '$' and sameness (with '#?'):
      //Account's cannot know 'rate', so each of these calls Account's first, then adds it.
      default operator ?
      default operator $
      default operator <=>

  defines program

    ClassAnatomy()
      stdout <- Stdout()

      ann <- Account("Ann", 120)
      bob <- SavingsAccount("Bob", 80, 3)

      stdout.println(ann.describe())
      stdout.println(bob.describe())

      bob.deposit(40)
      stdout.println(bob.describe())

      stdout.println($ann)

      twin <- Account("Ann", 120)
      carl <- Account("Carl", 50)
      stdout.println(`ann == twin: ${ann == twin}`)
      stdout.println(`ann < carl: ${ann < carl}`)
      stdout.println(`ann is set: ${ann?}`)

Output

Ann holds 120
Bob holds 80 at 3%
Bob holds 120 at 3%
Account(owner=Ann, balance=120)
ann == twin: true
ann < carl: true
ann is set: true

Wrong forms

Each of these is written by people and by models, and each fails to compile - the error is what the compiler tells you.

Extending a class that is not open

Classes are closed by default; the base class must be declared 'as open' (or 'as abstract') before another class can extend it.

Fails with E05030: not open to be extended

#!ek9
defines module bad.site.classes.anatomy.closedbase

  defines class

    Account
      owner as String?

      default private Account()

      Account()
        -> owner as String
        this.owner: owner

      describe() as pure
        <- rtn as String: `Owned by ${owner}`

      default operator ?

    SavingsAccount extends Account

      SavingsAccount()
        -> owner as String
        super(owner)

Uninitialised properties and no private default constructor

With a property that has no initial value, the no-argument constructor the compiler would provide is public and would leave it unset; declare 'default private Account()' so nothing outside the class can build an incomplete Account.

Fails with E07175: default constructor must be 'private' when uninitialised properties exist

#!ek9
defines module bad.site.classes.anatomy.publicdefault

  defines class

    Account
      owner as String?

      Account()
        -> owner as String
        this.owner: owner

      describe() as pure
        <- rtn as String: `Owned by ${owner}`

      default operator ?

Requesting '==' beside '<=>'

A type defines sameness once, with '<=>' or '=='; '==', '<>', '<', '<=', '>' and '>=' are then generated from it and are never written or requested.

Fails with E07181: comparisons with the type itself are generated from its sameness ('<=>' or '=='), never written or requested

#!ek9
defines module bad.site.classes.anatomy.writtenequality

  defines class

    Account
      owner as String?

      default private Account()

      Account()
        -> owner as String
        this.owner: owner

      describe() as pure
        <- rtn as String: `Owned by ${owner}`

      default operator ?
      default operator <=>
      default operator ==
      default operator #?

A class with properties and no operator ?

A class with properties must say what 'set' means for its objects: add 'default operator ?' (every property set) or write 'override operator ?'.

Fails with E07235: aggregate with properties/fields requires 'override operator ?' (custom) or 'default operator ?' (auto-generated) to define set/unset semantics

#!ek9
defines module bad.site.classes.anatomy.noisset

  defines class

    Account
      owner as String?

      default private Account()

      Account()
        -> owner as String
        this.owner: owner

      describe() as pure
        <- rtn as String: `Owned by ${owner}`

A constructor that leaves a property without a value

Every constructor must give every 'as T?' property a value; a property one constructor forgets is read unset later.

Fails with E08060: is/may not be initialised before use

#!ek9
defines module bad.site.classes.anatomy.unsetproperty

  defines class

    Account
      owner as String?
      balance as Integer?

      default private Account()

      Account()
        -> owner as String
        this.owner: owner

      describe() as pure
        <- rtn as String: `${owner} holds ${balance}`

      default operator ?