Why does AI generate getters and setters in EK9?

← What AI Gets Wrong About EK9 · Ref: Q280

AI models trained on Java generate getName(), setName(), getValue(), isActive() patterns. Nothing is syntactically wrong with these names in EK9, but they violate EK9 conventions and miss the deeper design philosophy.

THE AI MISTAKE

AI generates Java bean methods: getName(), setName(value), getContent(), isActive(). EK9 does not follow this naming convention.

EK9 ACCESSOR CONVENTION

Use bare noun or verb names:

  content() not getContent()
  keys() not getKeys()
  hue() not getHue()
  trim() not getTrim()

EK9 SETTER ALTERNATIVE

Use bare noun with parameter or 'with' prefix:

  content(newContent) not setContent(newContent)
  withContent(newContent) for returning a modified copy

THE DEEPER ISSUE

Get and set patterns encourage breaking information hiding. You should be interested in a class's BEHAVIOR, not its internal state. Classes and components should keep internal state private and expose operations that make sense in the domain.

EXTERNAL STATE REPRESENTATION

If external state is needed, return a RECORD with copies of an external representation, not copies of internal state variables. The record is a deliberate public-facing view, decoupled from internals. If the internal structure changes, the record stays stable.

OPERATORS NOT GETTERS

For standard conversions, use operators:

  $ for string representation (replaces toString/getName patterns)
  $$ for JSON representation
  ? for state checking (replaces isActive/isValid patterns)

ONLY ENUMERATIONS HAVE AUTOMATIC OPERATORS

Only enumerations have full automatic operator generation. Records and classes can use 'default operator' to opt in, but this is explicit, not automatic.

See Q96 for classes. See Q98 for records. See Q238 for operator overview. See Q281 for verifying AI code.

Example

defines module qa.ai.mistakes.getterssetters

  defines record

    // Record as external state representation

    AccountSummary
      holder as String: String()
      balance as Float: Float()

      AccountSummary() as pure
        ->
          theHolder as String
          theBalance as Float
        holder :=? theHolder
        balance :=? theBalance

      default operator

  defines class

    // Class exposes BEHAVIOR, not internal state

    BankAccount
      ownerName as String: String()
      currentBalance as Float: Float()
      isOpen as Boolean: false

      BankAccount()
        ->
          initialOwner as String
          initialBalance as Float
        ownerName: initialOwner
        currentBalance: initialBalance
        isOpen: true

      // Bare noun accessor — NOT getOwnerName()
      owner() as pure
        <- rtn as String: String(ownerName)

      // Behavior method — NOT setBalance()
      deposit()
        -> amount as Float
        if amount > 0.0 and isOpen
          currentBalance: currentBalance + amount

      withdraw()
        -> amount as Float
        <- success as Boolean: false
        if amount > 0.0 and amount <= currentBalance and isOpen
          currentBalance: currentBalance - amount
          success: true

      closeAccount()
        isOpen: false

      // Return a RECORD as external state view
      summary() as pure
        <- rtn as AccountSummary: AccountSummary(ownerName, currentBalance)

      // $ operator replaces toString()
      operator $ as pure
        <- rtn as String: `${ownerName}: ${currentBalance}`

      // ? operator replaces isActive()
      override operator ? as pure
        <- rtn as Boolean: ownerName? and isOpen

  defines program

    GetterSetterDemo()
      stdout <- Stdout()

      account <- BankAccount("Alice", 1000.0)

      // === BARE NOUN ACCESSOR — NOT getName() ===

      stdout.println(`Owner: ${account.owner()}`)

      // === BEHAVIOR METHODS — NOT setBalance() ===

      account.deposit(500.0)
      stdout.println(`After deposit: ${account}`)

      if ok <- account.withdraw(200.0)
        stdout.println(`Withdrew 200: ${account}`)

      // === RECORD AS EXTERNAL STATE VIEW ===

      view <- account.summary()
      stdout.println(`Summary: ${view}`)

      // === ? OPERATOR REPLACES isActive() ===

      stdout.println(`Account active: ${account?}`)
      account.closeAccount()
      stdout.println(`After close, active: ${account?}`)

Common mistakes

E50001 — Renaming the variable means later references to 'account' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

accountXYZ <- BankAccount("Alice", 1000.0)

Correct:

account <- BankAccount("Alice", 1000.0)

E50001 — Renaming the variable means later references to 'view' become unresolved, triggering E50001. Variable names must be consistent. See ek9 -h E50001 for details.

Incorrect:

viewXYZ <- account.summary()

Correct:

view <- account.summary()
Other ways to ask this
  • Why does AI use getName() and setName() in EK9?
  • What is the EK9 convention for accessors?
  • How does EK9 handle information hiding without getters?

Coming from another language?

Java: JavaBean convention (getName/setName), IDE generates getters/setters, Lombok @Data generates them. Python: @property decorator, direct attribute access common. Rust: no getter convention, pub fields or methods. Go: exported fields (uppercase) or accessor methods. Kotlin: properties with get/set, data classes auto-generate. C#: properties with get/set accessors. EK9: bare noun accessors (content() not getContent()), operators for standard conversions ($ for string, ? for state), expose behavior not state, records for external state representation.

Keywords: hallucination, setter, property, encapsulation, hiding, getter, pitfall, common-error, migrate, field, behavior, information, wrong, accessor, bean, ai, mistake