How do I control field visibility in EK9?

← Classes and OOP · Ref: Q95

EK9 fields have fixed visibility rules that vary by construct type. There are no access modifiers on fields; the construct type determines visibility.

CLASSES: ALWAYS PRIVATE

Class properties are always private. Access them through explicit accessor methods:

  defines class
    Person
      name <- String()
      name() as pure
        <- rtn as String: name

You write accessor methods to expose properties. There is no getter/setter annotation.

RECORDS: ALWAYS PUBLIC

Record properties are always public. Access them directly:

  defines record
    Point
      x <- 0.0
      y <- 0.0

Callers use 'point.x' directly. Records are data carriers by design.

COMPONENTS: ALWAYS PRIVATE

Component properties are always private, just like classes. Components are large-scale containers for dependency injection:

  defines component
    Processor as abstract
      process()
        -> input as String
        <- rtn as String

Concrete components access their own fields internally.

TRAITS: NO FIELDS

Traits cannot have properties at all. They define method contracts with no retained state:

  defines trait
    Printable
      show() as pure abstract
        <- rtn as String

NO ACCESS MODIFIERS ON FIELDS

Unlike methods (which can be public, protected, or private), fields have no access modifier in the grammar. Writing 'private name <- String()' or 'public x <- 0.0' causes a parse error. The construct type alone determines field visibility.

FIELDS MUST BE INITIALISED

All fields must be initialised at declaration, set in a constructor, or marked for injection with '!' on components. Uninitialised fields cause E08180.

See Q93 for class basics. See Q94 for constructors. See Q97 for records vs classes. See Q106 for traits. See Q111 for components. See Q573 for method access modifier rules.

Example

defines module qa.oop.visibility

  defines trait

    Printable
      show() as pure abstract
        <- rtn as String?

  defines class

    Person with trait of Printable
      name <- String()
      age <- Integer()

      Person()
        ->
          name as String
          age as Integer
        this.name: name
        this.age: age

      name() as pure
        <- rtn as String: name

      age() as pure
        <- rtn as Integer: age

      override show() as pure
        <- rtn as String: `Person: ${name}, age ${age}`

      default operator

  defines record

    Point
      x <- 0.0
      y <- 0.0

      Point()
        ->
          x as Float
          y as Float
        this.x: x
        this.y: y

      default operator

  defines component

    Processor as abstract

      process() abstract
        -> input as String
        <- rtn as String?

      default operator ?

  defines component

    UpperCaseProcessor extends Processor
      prefix <- "PROCESSED"

      override process()
        -> input as String
        <- rtn as String: `${prefix}: ${input.upperCase()}`

      default operator ?

  defines program

    VisibilityDemo()
      stdout <- Stdout()

      // === CLASS FIELDS: always private, use accessor methods ===

      person <- Person("Alice", 30)
      stdout.println(`Name via accessor: ${person.name()}`)
      stdout.println(`Age via accessor: ${person.age()}`)
      stdout.println(person.show())

      // === RECORD FIELDS: always public, access directly ===

      point <- Point(3.0, 4.0)
      stdout.println(`Direct access x: ${point.x}, y: ${point.y}`)

      // === COMPONENT FIELDS: always private, used internally ===

      processor <- UpperCaseProcessor()
      stdout.println(processor.process("hello world"))

      // === TRAIT: no fields, just method contracts ===

      printable as Printable: person
      stdout.println(`Via trait: ${printable.show()}`)

      // === COMPARISON ===

      point2 <- Point(3.0, 4.0)
      stdout.println(`Points equal: ${point == point2}`)

      person2 <- Person("Alice", 30)
      stdout.println(`Persons equal: ${person == person2}`)

Common mistakes

E06180 — Class fields are always private in EK9. There is no 'private' keyword because it is the only option. Access fields through accessor methods like 'name()'. See ek9 -h E06180 for details.

Incorrect:

person.name

Correct:

person.name()

E50060 — EK9 does not use Java-style 'getX()' accessors. The accessor method matches the field name: 'name()' not 'getName()'. See ek9 -h E50060 for details.

Incorrect:

person.getName()

Correct:

person.name()

E06180 — Component fields are always private. Even though the field 'prefix' exists on UpperCaseProcessor, it cannot be accessed from outside the component. Use the public methods instead. See ek9 -h E06180 for details.

Incorrect:

processor.prefix

Correct:

processor.process("hello world")
Other ways to ask this
  • Are class properties public or private by default in EK9?
  • How does encapsulation work in EK9?
  • What is the difference between class, record, and component field visibility?

Coming from another language?

Java: fields private by convention with explicit getters/setters, no enforcement unless you add private. Python: no true private, underscore convention, properties via @property decorator. Rust: struct fields private by default outside module, pub for public. Go: uppercase for exported, lowercase for unexported. Kotlin: val/var properties with automatic getters/setters, backing field access. C#: fields private by default, properties with get/set accessors. EK9: class and component properties always private (no modifier allowed), record properties always public, traits have no fields. The construct type determines visibility with no override possible.

Keywords: accessor, property, trait, field, getter, object-oriented, encapsulation, class, record, E08180, component, visibility, public, private, setter