# Migrating from classical to modern If you are maintaining a `bless`-based codebase and wondering what the equivalent [`class`](../../p5/core/perlfunc/class) block looks like, this chapter is the recipe. The translation is mostly mechanical, but the places where it is not mechanical are where real design decisions surface — and where the migration pays for itself. Before starting, read the [modern class chapter](classes) for the syntax you are translating *into*, and skim the [classical OO chapter](classical) for any constructs the existing code uses that you may not remember in detail. ## When to migrate Migration is not free. Do it when you get real value: - The class is under **active development**. You are about to add fields, add methods, or change the constructor contract. Doing that work against the `class` feature is less error-prone than doing it against hand-rolled hash slots. - The class has **unclear encapsulation**. Callers reach into `$obj->{fieldname}` directly, and you want to stop that without rewriting every call site at once. The `class` feature makes the violation a compile-time error the moment you switch. - The class sits on top of **Moose or Moo**, and you are paying the load-time cost without using the dependency-heavy features. The modern `class` is a straightforward replacement for most Moose/Moo classes that only use `has` and `with`. Do **not** migrate a class that is working and frozen. A stable classical class that will not see new code for another five years is exactly as good as its modern equivalent, and the migration itself will introduce bugs that the stable class does not have. ## The mechanical translation Take this classical class: ```perl package Counter; sub new { my ($class, %args) = @_; my $self = { value => $args{value} // 0, step => $args{step} // 1, }; die "step must be positive" if $self->{step} <= 0; return bless $self, $class; } sub value { my $self = shift; $self->{value} = shift if @_; return $self->{value}; } sub step { $_[0]{step} } sub tick { my $self = shift; $self->{value} += $self->{step}; } 1; ``` The modern form: ```perl use v5.38; use feature 'class'; no warnings 'experimental::class'; class Counter { field $value :param :reader :writer = 0; field $step :param :reader = 1; ADJUST { die "step must be positive" if $step <= 0; } method tick { $value += $step } } 1; ``` The mapping, piece by piece: | Classical construct | Modern construct | |----------------------------------------------|---------------------------------------| | `package Counter;` | `class Counter { ... }` | | `sub new { ... bless ... }` | *(deleted — generated)* | | Constructor argument defaulting with `//` | `= DEFAULT` or `:param ... = DEFAULT` | | `$args{foo}` stored in `$self->{foo}` | `field $foo :param` | | Read-only accessor | `:reader` | | Read/write accessor | `:reader :writer` | | Validation at the end of `new` | `ADJUST` block | | `$self->{foo}` inside a method | `$foo` (the field name) | | `my $self = shift` | *(deleted — implicit)* | | `use parent 'Base';` / `our @ISA = ('Base')` | `class Sub :isa(Base) { ... }` | | `$self->SUPER::foo(@args)` | unchanged — still works | ## The non-mechanical parts Everything above is rote translation. The places where you need to think are all variants of "the classical code was doing something the `class` feature does not do the same way." ### Multiple inheritance `class` does not support `:isa(A, B)`. If the classical class has `@ISA = ('Parent1', 'Parent2')`, you have three options: 1. **Collapse to single inheritance.** Usually the second parent is a mixin or role in disguise. Migrate it into a [role](roles-and-delegation) and consume it with `with`. 2. **Collapse to composition.** If the second parent is a collaborator rather than an ancestor, migrate to delegation instead of inheritance. 3. **Do not migrate the class.** If the multiple inheritance is genuinely load-bearing and neither of the above fits, the class is a poor fit for the modern feature. Leave it classical until the core role feature lands. In practice option 1 or 2 applies to about 95% of the multiple- inheritance cases in real code. ### Direct hash access from callers If external callers do `$counter->{value}` rather than `$counter->value`, migrating the class will break them. The compiler error is clean and local — the error message names the site — but the migration is no longer a drop-in. Mitigation strategy: 1. **Audit first.** Grep for `$obj_name->{` patterns across the codebase. The count is usually small. 2. **Add an accessor** to the classical class that covers every accessed field. 3. **Replace** `$obj->{fieldname}` call sites with `$obj->fieldname()` in a dedicated commit. 4. **Then migrate** the class to modern form. Splitting the refactor into two phases keeps each commit small and bisectable. ### `AUTOLOAD` `AUTOLOAD` in a classical class has no modern equivalent. The `class` feature does not let you catch missing method calls. If the `AUTOLOAD` is doing one of the common patterns, translate it: - **Lazy accessor synthesis** — declare the accessors explicitly as fields with `:reader` or `:writer`. The `class` feature's code generation replaces what `AUTOLOAD` was doing. - **Delegation to a contained object** — replace with explicit forwarder methods (see [roles and delegation](roles-and-delegation)) or with a Moose-style `handles` accessor if the codebase can depend on Moose. - **A generic proxy** — keep the class classical. The `class` feature is not the right tool. ### `DESTROY` `class` does not introduce a first-class destructor form. If the classical class has a `DESTROY`, write it as a plain method inside the `class` block — it is invoked through the same refcount hook: ```perl class Logger { field $fh; ADJUST { ... } sub DESTROY { my $self = shift; close $self->{fh} if $self->{fh}; } } ``` Note the wart: inside `DESTROY` you cannot name the field by its declared name, because `sub` bodies do not see field bindings. Either use a regular method that `DESTROY` delegates to, or accept the slight asymmetry. A cleaner pattern when the lifetime is under your control is to add an explicit `close` method and call it deterministically, leaving `DESTROY` as a belt-and-braces backup. ### Non-hash instances If the classical class does `bless \@self, $class` or `bless \$self, $class` — an array-backed or scalar-backed instance — the modern feature has no equivalent. Hash-like instance layout is baked in. Keep these classes classical. ### Tied hashes / overloading `use overload` and `tie` integrations work unchanged in a `class` block. Nothing special to do. ## Migrating Moose / Moo classes The translation from Moose to `class` is cleaner than from classical because Moose is already declarative. The rough map: | Moose / Moo | Modern `class` | |---------------------------------------------|-----------------------------------| | `use Moose;` / `use Moo;` | `use feature 'class';` plus block | | `has foo => (is => 'ro', ...)` | `field $foo :param :reader` | | `has foo => (is => 'rw', ...)` | `field $foo :param :reader :writer` | | `has foo => (default => sub { ... })` | `field $foo = EXPR` (or `ADJUST`) | | `extends 'Parent'` | `class Sub :isa(Parent) { ... }` | | `BUILD { ... }` | `ADJUST { ... }` | | `with 'Role::Name'` | *(wait for core roles)* | | `has foo => (isa => 'Int', ...)` | *(no direct equivalent)* | | `method_modifiers` (before/after/around) | *(no direct equivalent)* | The gap items — type constraints, method modifiers, roles — are what keeps some Moose codebases on Moose for now. If your Moose use is limited to `has`, `extends`, and `BUILD`, the migration is straightforward. If it depends heavily on Moose's type system or method modifiers, the migration is a project, not a commit. ## Testing the migration A class migration that changes no external behaviour should pass the existing test suite unchanged. Steps: 1. Ensure the class has tests covering its public API. If it does not, write them **before** migrating — the tests are your migration safety net. 2. Migrate the class. 3. Run the tests. A clean pass is the completion signal. 4. Grep the codebase for `$obj->{...}` reaches into the migrated class's instances. Fix any hits. 5. Remove any now-dead `use parent`, `use base`, explicit `sub new`, or accessor boilerplate the old form needed. If step 3 fails, the failures usually fall into a short list: - **Direct hash access** in a method that the old form tolerated and the new form rejects. Replace with field references. - **A constructor argument that was silently accepted.** The generated constructor is strict about `:param` names. Either declare the parameter or change the call site. - **A field initialiser that referenced `$self`.** Field initialisers run before `$self` is bound. Move the dependent logic into an `ADJUST` block. ## What you get out of it The classical-to-modern migration is unusual in that the result is both shorter and more correct: - **Shorter.** The `Counter` example above drops from ~20 lines to ~10, and the removed lines are all boilerplate. - **Private fields.** External code can no longer reach in. The encapsulation you used to defend in review is now defended by the compiler. - **No forgotten `bless`.** The generated constructor is always inheritance-safe; you cannot ship a constructor that accidentally blesses into the wrong class. - **No forgotten `$self = shift`.** The implicit bind cannot go missing. - **A named feature flag.** `no warnings 'experimental::class'` documents at the top of the file what the class depends on. ## Further reading - [Modern classes](classes) — reference for the syntax you are migrating into. - [Classical OO](classical) — reference for the syntax you are migrating out of. - [Inheritance and method resolution](inheritance) — the mixed- hierarchy chapter, useful while the codebase is half-migrated. - [Roles and delegation](roles-and-delegation) — where multiple inheritance ends up in the migrated code. - [`class`](../../p5/core/perlfunc/class), [`field`](../../p5/core/perlfunc/field), [`method`](../../p5/core/perlfunc/method) — the reference pages for the three keywords at the heart of the migration. - [`bless`](../../p5/core/perlfunc/bless), [`ref`](../../p5/core/perlfunc/ref), [`isa`](../../p5/core/perlfunc/isa) — the primitives that underpin both old and new forms and that keep working across mixed hierarchies.