What is the type traversal limit and why does EK9 enforce it?

← Code Quality · Ref: Q731

EK9 limits method chains to 3 type transitions. Each time a dot-chain crosses from one type to another, it counts as a transition. Exceeding 3 triggers E11054.

IMPORTANT DISTINCTION

Same-type method chains do NOT count as transitions:

  name.trim().upperCase().lowerCase()  // 0 transitions (all String)

Only CROSS-TYPE chains count:

  dept.getManager().getAddress().getCity()  // 3 transitions

WHY 3 TRANSITIONS MAXIMUM

This enforces the Law of Demeter (principle of least knowledge):
1. Structural coupling: long chains couple your code to the internal structure of distant objects
2. Fragile chains: changing any intermediate type breaks all callers
3. Testability: mocking a 4-deep chain requires 4 mock objects
4. Encapsulation violation: reaching through objects exposes internal structure

HOW TO FIX

Add a convenience method that encapsulates the traversal:

  // Before: dept.getManager().getAddress().getRegion().getCity()
  // After:  dept.getManagerCity()
  // The Department class adds: getManagerCity() = getManager().getAddress().getRegion().getCity()

THIS EXAMPLE

The method chain below crosses exactly 3 type boundaries (Department to Manager to Address to String). The Address class also has a getRegion() method returning a Region object. Adding that extra traversal step to reach the city via region would push from 3 to 4 transitions, triggering E11054.

See Q310 for code quality overview. See Q696 for complexity limits. See Q322 for quality enforcement.

Example

defines module qa.codequality.typetraversalboundary

  defines class

    <?-
      Geographic region containing a city name.
    -?>
    Region
      regionCity <- String()

      Region()
        -> regionCity as String
        this.regionCity: regionCity

      getCity() as pure
        <- rtn as String: regionCity

      default operator ?

    <?-
      Address that belongs to a geographic region.
      Has both getCity() for direct access and getRegion() for region traversal.
    -?>
    Address
      cityLabel <- String()
      addressRegion <- Region()

      Address()
        ->
          cityLabel as String
          addressRegion as Region
        this.cityLabel: cityLabel
        this.addressRegion: addressRegion

      getCity() as pure
        <- rtn as String: cityLabel

      getRegion() as pure
        <- rtn as Region: addressRegion

      default operator ?

    <?-
      Manager type that holds an address.
    -?>
    Manager
      managerName <- String()
      homeAddress <- Address()

      Manager()
        ->
          managerName as String
          homeAddress as Address
        this.managerName: managerName
        this.homeAddress: homeAddress

      getName() as pure
        <- rtn as String: managerName

      getAddress() as pure
        <- rtn as Address: homeAddress

      default operator ?

    <?-
      Department type that holds a manager.
      Method chains from Department cross 3 type boundaries maximum.
    -?>
    Department
      deptName <- String()
      deptManager <- Manager()

      Department()
        ->
          deptName as String
          deptManager as Manager
        this.deptName: deptName
        this.deptManager: deptManager

      getName() as pure
        <- rtn as String: deptName

      getManager() as pure
        <- rtn as Manager: deptManager

      default operator ?

  defines program

    TypeTraversalBoundaryDemo()
      stdout <- Stdout()

      addressRegion <- Region("Springfield")
      homeAddress <- Address(cityLabel: "Springfield", addressRegion: addressRegion)
      deptManager <- Manager(managerName: "Alice", homeAddress: homeAddress)
      department <- Department(deptName: "Engineering", deptManager: deptManager)

      //3 type transitions: Department -> Manager -> Address -> String
      //This is exactly at the boundary
      managerCity <- department.getManager().getAddress().getCity()
      stdout.println(managerCity)

      //Same-type chains: 0 transitions (all String)
      processed <- managerCity.trim().upperCase()
      stdout.println(processed)

      //2 transitions: Department -> Manager -> String (within limit)
      managerLabel <- department.getManager().getName()
      stdout.println(managerLabel)

Common mistakes

E11054 — Adding an extra type transition (Address to Region to String instead of Address to String) pushes the chain from 3 to 4 type transitions, exceeding the limit. Encapsulate the traversal in a convenience method. See ek9 -h E11054 for details.

Incorrect:

      managerCity <- department.getManager().getAddress().getRegion().getCity()

Correct:

      managerCity <- department.getManager().getAddress().getCity()
Other ways to ask this
  • What triggers E11054 excessive type traversal?
  • How many type transitions are allowed in a method chain?
  • What is the Law of Demeter in EK9?
  • Why does EK9 limit dot-chain depth?

Coming from another language?

Java: no type traversal limit (Demeter checked by optional tools). Python: no limit. C++: no limit. Rust: method chains common, no cross-type limit. Go: no limit (short method chains by convention). EK9: 3-transition hard limit enforced at compile time.

Keywords: type, limit, transition, traversal, Demeter, E11054, coupling, dot, clean-code, quality, encapsulation, boundary, chain