Blog › ICP guides

Dylan developer on retainer: method dispatch specialization, module system, condition system, macros, and Open Dylan engineering on monthly retainer

October 25, 2026 · ~18 min read

A Dylan application computing product prices was returning wrong results for 4 premium product instances per day. The system used a Dylan generic function calculate-price with a define method specializing on <base-product> that applied the standard pricing formula. When the <premium-product> subclass was introduced, the developer added a new subclass with extra attributes but did not add a corresponding define method for calculate-price specializing on <premium-product>. Dylan’s generic function dispatch selects the most specific applicable method: for a <premium-product> instance, the most specific applicable method was the one specializing on <base-product>, because <premium-product> is a subclass of <base-product> and there was no more specific method. The Dylan developer on retainer diagnosed the root cause: the base-class method was being silently applied to all premium products, computing the standard price without the premium surcharge. The fix added define method calculate-price (p :: <premium-product>) => (price :: <integer>) with the correct premium formula. Dylan’s method dispatch then selected this more specific method for all <premium-product> instances, with the base-class method continuing to apply for <base-product> instances. Wrong calculations: 4 per day → 0.

The work log entry read “fixed pricing bug for premium products, 16h.” It names the symptom and the duration. It cannot explain to a client why the fix required understanding Dylan’s method dispatch algorithm, why Dylan’s generic functions select the most specific applicable method (not the most recently defined), why the absence of a subclass method is not a compile-time error in Dylan (only a dispatch failure at runtime if no applicable method exists at all), why the => (price :: <integer>) return type declaration matters for the Open Dylan compiler’s type inference, or what the difference is between a define method on a subclass (specialization) and a define method on the base class with a conditional inside it (which would be the wrong pattern). The 16 hours of class hierarchy audit (examining all define class declarations that extend <base-product> to find other subclasses that might also lack price overrides), dispatch trace analysis (using Open Dylan’s compiler method report to list applicable methods for a <premium-product> argument and verify dispatch order), method specialization addition (writing the correct define method with the premium formula), and regression testing (verifying that the base-class method still applied correctly for <base-product> instances after the subclass method was added) are not visible in the diff beyond the new define method definition.

Dylan’s generic function dispatch: specializers, specificity, and next-method

Dylan’s object system is built on generic functions and methods. A generic function is declared with define generic: define generic calculate-price (product) => (price :: <integer>); declares that calculate-price is a generic function that takes one argument and returns an integer. Methods are added to a generic function with define method: each define method declaration specifies a specializer for each parameter, and the method is applicable only when the argument matches the specializer. The most common specializer form is a class name: define method calculate-price (p :: <premium-product>) => (price :: <integer>) is applicable whenever p is an instance of <premium-product> or any subclass. The dispatch algorithm: when calculate-price is called with an argument, Dylan finds all applicable methods (those whose specializer is a superclass of the argument’s class) and sorts them by specificity. A method specializing on <premium-product> is more specific than a method specializing on <base-product> because <premium-product> is a proper subclass of <base-product>. Dylan calls the most specific applicable method. If no applicable method exists, Dylan signals a <no-applicable-method-error> condition at runtime.

Dylan supports multiple specializer forms. Class specializers (:: <ClassName>) are the most common. Singleton specializers (singleton(value)) dispatch on a specific value: define method handle-command (cmd == #"quit") => () uses a singleton specializer on the keyword symbol #"quit", so this method is applicable only when cmd is exactly the symbol #"quit". This is Dylan’s idiomatic replacement for switch/case dispatch — multiple singleton-specialized methods on a generic function provide cleaner dispatch than a large conditional. Dylan also supports the subclass() specializer for dispatch on class objects themselves (rather than instances): define method describe-class (c :: subclass(<shape>)) => () is applicable when c is <shape> itself or any subclass object, not an instance of <shape>. This is used for class-level generic functions that compute on the class rather than instances.

The next-method mechanism allows cooperative method composition. Inside a define method body, calling next-method() invokes the next most specific applicable method with the same arguments. This is Dylan’s equivalent of CLOS’s call-next-method. Pattern: a subclass method handles the subclass-specific part and calls next-method() to invoke the base-class behavior. For calculate-price: the <premium-product> method could compute let base = next-method(); (invoking the <base-product> method) and then return base + premium-surcharge(p). Alternatively, it can compute the full price independently without calling next-method(). Dylan also supports define method with #rest and #key argument lists for keyword argument dispatch and variadic methods. The define sealed generic and define sealed method declarations tell the Open Dylan compiler that no additional methods can be added at runtime, enabling the compiler to inline or devirtualize the dispatch in many call sites, eliminating the generic function dispatch overhead entirely.

Dylan’s class system: define class <MyClass> (<superclass1>, <superclass2>) slot-definitions end; defines a class with multiple superclasses (Dylan supports multiple inheritance). Slot declarations: slot my-slot :: <integer> = 0, init-keyword: my-slot: defines a slot with type <integer>, default value 0, and keyword argument my-slot: for make. constant slot creates a read-only slot. virtual slot creates a slot with custom getter and setter methods defined separately. Instance creation: make(<premium-product>, base-price: 100, premium-tier: #"gold"). The initialize generic function is called by make after instance allocation; customizing it: define method initialize (p :: <premium-product>, #key premium-tier) => () next-method(); p.computed-surcharge := surcharge-for(premium-tier) end method. Slot access: p.slot-name (getter); p.slot-name := value (setter, requires setter: slot-name-setter in the slot declaration or the setter is defined by default for non-constant slots). The is-a? predicate tests class membership: is-a?(p, <premium-product>). The object-class function returns the direct class of an object.

Dylan’s module system, condition system, limited collections, and macros

Dylan’s module system is explicitly declared using define module and define library. A define module declaration specifies which names the module exports and which modules it imports using use: define module my-module use common-dylan; use format-io; export calculate-price, <premium-product>; end module;. The use clause imports all exported names from the named module into the current module’s namespace. Name conflicts (when two imported modules export the same name) must be resolved explicitly: use module-a, rename: { shared-name => module-a-name }; renames the imported name; use module-b, prefix: "b-"; adds a prefix to all imports from module-b. Modules can also use import: { name1, name2 } to import only a subset of a module’s exports. A define library declaration groups modules into a deployable unit: define library my-library use common-dylan; export my-module; end library;. The Open Dylan project system uses .lid (Library Interchange Description) files to specify the source files, libraries, and modules that constitute a project, and dylan-compiler to build them into executables or shared libraries.

Dylan’s condition system is modeled after Common Lisp’s condition system. Condition classes are defined with define class extending <condition>, <serious-condition>, <error>, or <warning>. Condition classes can have slots for error context: define class <file-not-found-error> (<error>) constant slot error-filename :: <string>, required-init-keyword: filename:; end class;. Signaling: signal(condition) for recoverable conditions; error(condition) for non-recoverable conditions; error(format-string, args) for inline error creation using the <simple-error> class. Handler establishment uses handler-bind: handler-bind ((<file-not-found-error>, handler-function)) body end establishes a dynamic handler for <file-not-found-error> that invokes handler-function when such a condition is signaled within body. The handler-bind mechanism is dynamic (not lexical) — it establishes a handler that is in effect for all code called during the dynamic extent of the body, not just the lexical body. The block (...) body exception (<error>) recovery-expr end block form is syntactic sugar that catches conditions and returns a recovery value; it is easier to write but less flexible than handler-bind because it cannot return to the signaling point (restarts are not available). Restarts use restart-query for interactive recovery strategies.

Dylan’s limited collections allow the type system to track element types and sizes at compile time. limited(<vector>, of: <integer>) creates a vector type whose elements are restricted to <integer>, enabling the Open Dylan compiler to use unboxed storage and direct integer operations without dynamic type checks on element access. limited(<array>, of: <single-float>, dimensions: #(3, 3)) creates a 3×3 single-precision float array type whose dimensions are known at compile time. Limited collection instances are created with make using the limited type: make(limited(<vector>, of: <integer>), size: 10, fill: 0). The element accessor for limited collections is element(v, i) or the shorthand v[i]; assignment is v[i] := value (which for limited(<vector>, of: <integer>) performs no dynamic type check because the element type is statically known). The Open Dylan compiler generates significantly more efficient code for limited collections than for their unrestricted counterparts; using limited collections in performance-critical inner loops is one of the primary Dylan optimization techniques.

Dylan’s macro system uses pattern-matching rewriting rules defined with define macro. A macro definition has a name and a set of main rule patterns and templates: define macro with-logging ?:name body:body end => (begin do-log(?name); ?body end) end macro;. Pattern variables are introduced with ?name (matches a name token), ?expr or ?:expression (matches an expression), ?:body (matches a sequence of Dylan statements), and ?:name (matches a module-level name). Macro hygiene: Dylan macros are not fully hygienic in the Scheme sense, but the with-hygienic-name ?:name (generated-temp) body end form generates unique names to avoid capture. Auxiliary rule sets allow macros to recognize multiple syntactic patterns: define macro my-macro ... aux-rule: ?pattern => ?template ... end macro. Common macro patterns: generating define class with standard slots; generating define method boilerplate for common dispatch patterns; implementing DSL syntax for configuration, state machine definition, or command routing. Dylan’s macro system is hygiene-aware and allows full syntactic abstraction, making it suitable for building embedded domain-specific languages within Dylan programs.

How HourTab tracks Dylan developer retainer hours

Dylan retainer work shares the invisible-work problem with all multiple-dispatch language retainers, with the additional challenge that Dylan’s most common retainer tasks — specializer hierarchy completion, module boundary design, condition handler chain restructuring, define macro authorship — produce diffs whose surface area is small relative to the analytical work required. Adding a define method calculate-price on <premium-product> is a diff with ten lines; the value is correct dispatch for all premium product instances in perpetuity, enforced by the Open Dylan compiler’s method report for all future subclasses. Changing a module export to remove a concrete subclass type and export only the abstract superclass is a diff with one modified export line; the value is an enforced abstraction boundary that prevents all downstream code from bypassing the factory function invariants, now and in all future versions of the library. Replacing a block ... exception (<error>) catch-all with a structured handler-bind for three specific condition classes is a diff with twenty lines; the value is correct recovery semantics for each error type, elimination of silent swallowing of unexpected conditions, and the ability for the signaling code to offer restarts that the handler-bind can accept. Adding a define macro that generates the six-slot class definition boilerplate for a new entity type is a diff with twenty lines of macro; the value is elimination of 120 lines of repetitive class definitions across the codebase and a single point of change when the entity schema changes.

HourTab gives Dylan developers a public retainer-hours URL they send to clients — typically teams maintaining Open Dylan applications in embedded systems firmware, research groups using Dylan for programming language implementation work (Dylan compilers are themselves written in Dylan), and organizations with legacy Dylan codebases built during the language’s 1990s adoption period at Apple and CMU — at the start of an engagement. For Dylan retainers, each work log entry should name the mechanism (define method specializer hierarchy audit for all subclasses of each generic function; singleton specializer addition for value-dispatch; next-method call chain design for cooperative method composition; dispatch ambiguity diagnosis and resolution; define sealed method annotation for compiler optimization; define module export boundary redesign with rename:/prefix: collision resolution; define library module combination design; condition class hierarchy design with <serious-condition>/<warning> superclasses; handler-bind dynamic handler establishment; restart-query interactive recovery design; block ... exception handler restructuring; define macro pattern/template pair authorship; limited(<collection>, of: <type>) optimization; Open Dylan C FFI define C-function binding), the specific generic function name and the dispatch problem, and the before/after observable metric. Dylan retainers are often compared to Standard ML developer retainers for statically-typed functional language work, to Racket developer retainers for Lisp-family language platform engineering, and to Smalltalk developer retainers for object-oriented language platform work. The distinction from Smalltalk is the module system: Dylan enforces name visibility at the module boundary with explicit export declarations, while Smalltalk uses a global image namespace. HourTab’s work log makes the specializer hierarchy repair and module boundary redesign visible to clients who would otherwise see only the symptom — wrong pricing calculations or invariant violations — and not understand why the fix required understanding Dylan’s generic function dispatch algorithm and module export mechanics.

Track Dylan developer retainer hours without the status emails

HourTab gives Dylan developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.

See HourTab pricing →

FAQ: Dylan developer retainers

What does a Dylan developer on retainer typically do?

A Dylan developer on monthly retainer covers four principal service areas: method dispatch and generic function design (define generic function signature design; define method specializer hierarchy audit for all subclasses; singleton specializer addition for value-dispatch on singleton(value); next-method call chain design; dispatch ambiguity diagnosis; define sealed method annotation for performance-critical paths); module system design (define module export and use clause design; define library module combination; rename:/prefix: collision resolution; module hierarchy for information hiding; cyclic import resolution); condition and restart system design (define class condition hierarchy with <serious-condition>/<warning>; signal/error/cerror signaling; handler-bind dynamic handler establishment; restart-query recovery; block ... exception handler restructuring); and macro system and platform design (define macro pattern/template pair authorship; limited(<collection>, of: <type>) performance optimization; Open Dylan C FFI define C-function binding; .lid project file management; DUIM GUI framework components).

What Dylan work is most commonly underlogged in a retainer?

Method specializer hierarchy audit (define method on <base-product> invoked for <premium-product> instances missing a subclass override; wrong calculation: 4/day → 0 after adding define method on <premium-product>; 12–22 hrs invisible in class hierarchy audit and dispatch trace analysis), module export boundary redesign (concrete subclass exported through module boundary bypassing factory invariants; 3 invariant violations/week → 0 after restricting export to abstract superclass; 10–18 hrs invisible in export analysis and downstream refactoring), and condition handler-bind restructuring (block ... exception (<error>) catch-all silently swallowing disk-full and network errors requiring user notification; restructured with handler-bind for separate condition class handlers; 8 silent error masking events/month → 0; 8–16 hrs invisible in condition class hierarchy design and handler precedence analysis).

What are typical Dylan developer retainer rates?

Entry-level Dylan developers (1–2 years, basic define class/make/initialize, simple define method specializers, define module import/export, let bindings, basic collection usage) bill at $65–$115/hr. Mid-level Dylan engineers (2–4 years, define method specializer hierarchy design across multi-level class hierarchies, next-method call chain composition, define module export boundary design with rename:/prefix:, handler-bind condition handler chains, define macro authorship, Open Dylan C FFI binding) bill at $110–$195/hr. Senior Dylan architects (4–8 years, full Dylan application architecture with define library module hierarchies, Open Dylan compiler optimization with dfmc introspection and sealed method annotations, DUIM GUI framework application design, complex define macro systems for DSL construction, limited collection performance engineering, Dylan concurrent programming) bill at $160–$290/hr. Monthly retainer ranges: $2,200–$5,000/mo advisory (15–25 hrs), $7,000–$18,000/mo for full Open Dylan platform engagements.

What should a Dylan developer retainer agreement include?

A Dylan developer retainer agreement should specify: method dispatch scope (define generic function signature design; define method specializer hierarchy audit; singleton specializer addition; next-method call chain composition; dispatch ambiguity resolution; define sealed method annotation); module system scope (define module export and use clause design; define library module combination; rename:/prefix: collision resolution; cyclic import resolution); condition and restart scope (define class condition hierarchy design; signal/error/cerror signaling; handler-bind dynamic handler establishment; restart-query interactive recovery; block ... exception handler restructuring); macro and platform scope (define macro pattern/template pair authorship; limited(<collection>, of: <type>) optimization; Open Dylan C FFI define C-function binding; .lid project file management; DUIM GUI components); and hour logging format (generic function name; dispatch error type; specializer hierarchy depth before/after; Open Dylan version).

How should Dylan developer retainer hours be logged?

Log each Dylan retainer session with: advisory category (define method specializer hierarchy audit; singleton specializer addition; next-method call chain design; dispatch ambiguity resolution; define sealed method annotation; define module export boundary redesign with rename:/prefix:; define library module combination design; condition class hierarchy design with <serious-condition>/<warning>; handler-bind dynamic handler establishment; restart-query recovery design; block ... exception handler restructuring; define macro pattern/template authorship; limited(<collection>, of: <type>) optimization; Open Dylan C FFI define C-function binding), the specific generic function name and the dispatch problem (define method specializing on <base-product> invoked for <premium-product> instances because subclass method was missing; wrong calculation: 4/day → 0 after adding define method on <premium-product>), and the before/after metric (wrong dispatch per day: 4 → 0; module invariant violations per week: 3 → 0; silent error masking per month: 8 → 0). Include Open Dylan version, platform, and whether the fix required specializer hierarchy addition, module export change, condition handler redesign, or macro pattern addition.