Chapter 13: DSL construction
Introduces Kotlin DSL design and identifies lambdas with receivers as a way to give DSL code blocks their own functions, properties, and behavior.
After this chapter you will be able to
- Receiver-based DSL structure. Explain how lambdas with receivers help create structure in a Kotlin DSL by making functions, properties, and behavior specific to a code block.
- Clean APIs and maintainability. Explain why API design matters for everyday application classes and what makes an API clean.
- Structure beyond individual calls. Distinguish what Kotlin DSLs add to clean APIs built from individual method calls.
- Domain-specific language focus. Distinguish a domain-specific language from a general-purpose language using SQL or regular expressions as an example.
- Concise declarative operations. Explain how a DSL’s restricted syntax can express a domain operation concisely, and contrast declarative requests with imperative steps.
- Internal and external DSL distinction. Distinguish an external DSL’s independent syntax from an internal DSL’s use of the syntax of its host language.
- Kotlin database query integration. Explain how the Exposed query illustrates an internal DSL and what integration it offers compared with writing SQL directly.
- DSL structure versus API calls. Distinguish a typical command-query API from a DSL by describing how their calls are organized.
- DSL grammar and type checking. Explain why the section calls an internal DSL a language and how its calls make up a meaningful operation.
- Type-safe HTML structure. Explain how the Kotlin HTML DSL produces HTML while checking tag placement, using the example that td can appear only inside tr.
- Dynamic HTML generation. Explain why writing HTML with the Kotlin DSL enables dynamic content rather than only a fixed markup fragment.
- Structured APIs through receiver lambdas. Explain that lambdas with receivers let Kotlin APIs have structure, a key trait that distinguishes DSLs from regular APIs.
- Implicit receiver in lambdas. Explain how making StringBuilder the receiver of a buildString lambda lets its body call append() without repeatedly writing it.append().
- Extension function type invocation. Describe how buildString changes its parameter type from (StringBuilder) -> Unit to StringBuilder.() -> Unit, and how that changes invocation from builderAction(sb) to sb.builderAction().
- Receiver-based HTML structure. Explain how lambdas with receivers make an HTML builder look like nested HTML while remaining regular, type-safe Kotlin code.
- DSL marker receiver restrictions. Explain how a DSL marker prevents accidental use of an outer tag’s implicit receiver inside a nested tag block.
- Tag hierarchy construction and rendering. Trace how the simplified builder creates, links, and renders a hierarchy of tags.
- Reusable HTML builder abstractions. Explain how an internal Kotlin DSL lets you extract repeated HTML-building code into named functions such as listWithToc and item.
- Table of contents builder flow. Trace how listWithToc uses a LISTWITHTOC receiver to collect item entries and then produces the table of contents and corresponding content inside the HTML body.
- Callable Custom Objects. Explain that the invoke convention lets objects of custom types use function-call syntax, and why the section cautions against using it indiscriminately.
- Callable objects through invoke. Explain how defining an operator invoke method lets an object be called with parentheses, and trace that syntax to a call to invoke. State that invoke can have different parameter lists, return types, and overloads.
- Dependency Handler Invoke Mechanism. Trace how a nested dependencies block works: calling dependencies as a function invokes its invoke method, which runs a lambda with DependencyHandler as its receiver.
- Infix assertion syntax. Explain how declaring `should` with the `infix` modifier lets a DSL express assertions with less punctuation, so a test reads almost like English.
- Matchers and typed assertions. Describe how `should` and a `Matcher` work together in the example, and explain why invalid combinations are caught at compile time.
- Primitive type duration extensions. Explain how a date-manipulation DSL can use extension properties on Int for units such as days and hours, including on numeric constants. Describe what those properties return and how the approach can support a custom measurement.
- Scoped column property extensions. Explain why Exposed declares column operations such as autoIncrement as member extensions of Table, and how the receiver type restricts their use.
- Query condition extension scope. Trace how a select condition can use eq without explicitly naming SqlExpressionBuilder.
What it builds on
- Lambdas with receivers: with, apply, and also (chapter 5) · Introduces lambdas with receivers.
- Adding methods to other people’s classes: Extension functions and properties (chapter 3) · Extension functions are one of the previously introduced features used to build clean APIs.
- Working with pairs: Infix calls and destructuring declarations (chapter 3) · Infix calls are one of the syntax features the section identifies as helping clean APIs.
- Function types specify the parameter types and return values of a lambda (chapter 10) · Function types describe a lambda’s parameters and return value.
- Meta-annotations: Controlling how an annotation is processed (chapter 12) · Introduces meta-annotations.
- Accessing elements by index: The get and set conventions (chapter 9) · Uses the earlier get convention as an analogy for concise operator syntax.
- Extension properties (chapter 3) · Extension properties provide the underlying language feature.
- Creating types with type arguments: Generic type parameters (chapter 11) · Reading generic types such as Column<Int>.
Section by section
Introduction
Introduces Kotlin DSL design and identifies lambdas with receivers as a way to give DSL code blocks their own functions, properties, and behavior.
- Receiver-based DSL structure: Explain how lambdas with receivers help create structure in a Kotlin DSL by making functions, properties, and behavior specific to a code block.
Why it matters: This is the basic connection between lambdas with receivers and structured DSL code.
Key terms: DSL structure, lambdas with receivers, code-block-specific functions
13.1 From APIs to DSLs: Creating expressive custom code structures
This section explains why readable, maintainable code depends on clean APIs and introduces Kotlin DSLs as a way to make statically typed code more expressive.
- Clean APIs and maintainability: Explain why API design matters for everyday application classes and what makes an API clean.
Why it matters: It gives you a way to judge whether code is easy for other developers to use and read. - Structure beyond individual calls: Distinguish what Kotlin DSLs add to clean APIs built from individual method calls.
Why it matters: It identifies the added capability that motivates using a DSL rather than a collection of clean calls. - Static typing in DSLs: State which static-typing benefits remain available when using Kotlin DSL patterns.
Why it matters: Readable DSL-style code can retain the checks and tooling benefits of static typing.
Key terms: API, clean API, syntactic noise, DSLs, structure, method calls, statically typed, compile-time error detection, IDE support
13.1.1 Domain-specific languages
This section defines domain-specific languages, explains why their focused and declarative syntax is useful, and identifies the difficulty of combining them with general-purpose applications.
- Domain-specific language focus: Distinguish a domain-specific language from a general-purpose language using SQL or regular expressions as an example.
Why it matters: That narrow focus explains both what a DSL is and why its syntax can be specialized. - Concise declarative operations: Explain how a DSL’s restricted syntax can express a domain operation concisely, and contrast declarative requests with imperative steps.
Why it matters: It explains the practical appeal of a DSL beyond simply having fewer features. - Host-language integration costs: Describe why a DSL with its own syntax can be difficult to use inside an application written in a general-purpose language.
Why it matters: These costs motivate the subsequent discussion of internal DSLs.
Key terms: domain-specific language, DSL, general-purpose programming language, SQL, regular expressions, declarative, imperative, execution engine, host application, string literal
13.1.2 Internal DSLs are seamlessly integrated into the rest of your program
This section contrasts external SQL with a Kotlin internal DSL, showing how a task-specific query can remain ordinary Kotlin code.
- Internal and external DSL distinction: Distinguish an external DSL’s independent syntax from an internal DSL’s use of the syntax of its host language.
Why it matters: This distinction explains why DSL-style code can fit directly into a Kotlin program. - Kotlin database query integration: Explain how the Exposed query illustrates an internal DSL and what integration it offers compared with writing SQL directly.
Why it matters: The example shows the practical value of expressing a database task within Kotlin instead of placing SQL in a string.
Key terms: external DSL, internal DSL, general-purpose language, Exposed, SQL, selectAll, groupBy, orderBy, native Kotlin objects
13.1.3 The structure of DSLs
This section distinguishes DSLs from ordinary APIs by the structure of their calls and explains how that structure can make code more meaningful and readable.
- DSL structure versus API calls: Distinguish a typical command-query API from a DSL by describing how their calls are organized.
Why it matters: Structure is the main distinction this section offers for recognizing a DSL without treating the boundary as absolute. - DSL grammar and type checking: Explain why the section calls an internal DSL a language and how its calls make up a meaningful operation.
Why it matters: The grammar analogy explains why a DSL is more than a collection of conveniently named methods. - Readability through structured calls: Describe how shared context and chained calls can reduce repetition or improve readability in the examples discussed.
Why it matters: These are practical reasons for choosing structured DSL calls over a noisier sequence of ordinary API calls.
Key terms: DSL, command-query API, nesting of lambdas, chained method calls, grammar, internal DSL, type checker, context, infix call syntax
13.1.4 Building HTML with an internal DSL
Shows why an internal Kotlin DSL is useful for building HTML: it enforces valid tag nesting while allowing HTML to be generated with ordinary code.
- Type-safe HTML structure: Explain how the Kotlin HTML DSL produces HTML while checking tag placement, using the example that td can appear only inside tr.
Why it matters: The DSL helps catch structural HTML mistakes before the page is generated. - Dynamic HTML generation: Explain why writing HTML with the Kotlin DSL enables dynamic content rather than only a fixed markup fragment.
Why it matters: The same table definition can incorporate data without writing a separate static HTML fragment.
Key terms: type safe, td, tr, HTML fragment, regular code, language construct, dynamically, map
13.2 Building structured APIs: Lambdas with receivers in DSLs
Introduces lambdas with receivers as a Kotlin feature for building structured DSL APIs.
- Structured APIs through receiver lambdas: Explain that lambdas with receivers let Kotlin APIs have structure, a key trait that distinguishes DSLs from regular APIs.
Why it matters: This connects a Kotlin feature to the structural quality that makes an API feel like a DSL.
Key terms: lambdas with receivers, DSLs, structure
13.2.1 Lambdas with receivers and extension function types
This section uses buildString to explain how lambdas with receivers are typed and called, then relates them to apply and with.
- Implicit receiver in lambdas: Explain how making StringBuilder the receiver of a buildString lambda lets its body call append() without repeatedly writing it.append().
Why it matters: This explains why builder code can read like operations on the object being built. - Extension function type invocation: Describe how buildString changes its parameter type from (StringBuilder) -> Unit to StringBuilder.() -> Unit, and how that changes invocation from builderAction(sb) to sb.builderAction().
Why it matters: The type and call syntax show how the implicit receiver is supplied to the lambda. - Receiver type at call sites: Explain why a lambda’s source-code appearance alone does not show whether it has a receiver, and describe how the receiving function’s signature reveals the receiver type.
Why it matters: Checking the expected type tells you which unqualified member calls are available inside a lambda. - Apply and with behavior: Describe how apply and with invoke an extension-function-type argument on a receiver, and distinguish what each returns.
Why it matters: Their different return values explain which function fits when you need the configured object versus the lambda’s result.
Key terms: lambda with a receiver, receiver, StringBuilder, buildString, extension function type, receiver type, builderAction, StringBuilder.() -> Unit, function signature, apply
13.2.2 Using lambdas with receivers in HTML builders
This section explains how lambdas with receivers give an HTML builder its structured syntax, how DSL markers limit receiver confusion, and how a simple builder constructs and renders tags.
- Receiver-based HTML structure: Explain how lambdas with receivers make an HTML builder look like nested HTML while remaining regular, type-safe Kotlin code.
Why it matters: Receiver types make the builder’s structure readable and constrain which tags can be created in each block. - Implicit and nested receivers: Describe how implicit receivers simplify builder syntax and what happens to outer receivers in nested blocks.
Why it matters: Implicit receivers explain both the builder’s concise appearance and a source of ambiguity in deeply nested code. - DSL marker receiver restrictions: Explain how a DSL marker prevents accidental use of an outer tag’s implicit receiver inside a nested tag block.
Why it matters: The marker keeps nested builder code from silently modifying a surrounding tag. - Tag hierarchy construction and rendering: Trace how the simplified builder creates, links, and renders a hierarchy of tags.
Why it matters: The visible nesting becomes output because each tag function adds a child to a hierarchy that can be rendered recursively.
Key terms: HTML builder, type-safe builders, lambda with a receiver, TABLE, TR, TD, implicit receiver, this@table, this@tr, this@td
13.2.3 Kotlin builders: Enabling abstraction and reuse
Shows how a Kotlin HTML builder can turn repeated page structure into reusable functions that generate both a table of contents and its content.
- Reusable HTML builder abstractions: Explain how an internal Kotlin DSL lets you extract repeated HTML-building code into named functions such as listWithToc and item.
Why it matters: Named builder functions hide repetitive HTML details and make the page-building code easier to understand. - Table of contents builder flow: Trace how listWithToc uses a LISTWITHTOC receiver to collect item entries and then produces the table of contents and corresponding content inside the HTML body.
Why it matters: Collecting each item once keeps the table of contents and the displayed summaries tied to the same entries.
Key terms: internal DSLs, kotlinx.html, listWithToc, item, BODY, entries, @HtmlTagMarker
13.3 More flexible block nesting with the invoke convention
Introduces the invoke convention as a way to call custom objects like functions, while noting its selective usefulness in DSLs.
- Callable Custom Objects: Explain that the invoke convention lets objects of custom types use function-call syntax, and why the section cautions against using it indiscriminately.
Why it matters: This gives you the basic idea needed to recognize why a DSL might make an object look callable.
Key terms: invoke convention, function types, DSLs
13.3.1 The invoke convention: Objects callable as functions
This section explains how Kotlin’s invoke convention lets objects be called with function-call syntax and how the same convention applies to lambdas.
- Callable objects through invoke: Explain how defining an operator invoke method lets an object be called with parentheses, and trace that syntax to a call to invoke. State that invoke can have different parameter lists, return types, and overloads.
Why it matters: It explains why an instance can look like a function call without being a function. - Lambda calls as invoke calls: Explain why calling a lambda with parentheses is also an application of the invoke convention.
Why it matters: It connects the new convention to familiar lambda-call syntax.
Key terms: invoke convention, operator modifier, invoke method, lambda, FunctionN interfaces
13.3.2 The invoke convention in DSLs: Declaring dependencies in Gradle
This section shows how Gradle-style dependency DSLs use the invoke convention to offer both flat calls and nested blocks.
- Dependency Handler Invoke Mechanism: Trace how a nested dependencies block works: calling dependencies as a function invokes its invoke method, which runs a lambda with DependencyHandler as its receiver.
Why it matters: This explains why a dependencies object can accept a block that calls its methods without repeating the object’s name. - Flat And Nested DSL Forms: Explain why the dependency DSL supports both direct calls and nested blocks, and distinguish when each form is useful.
Why it matters: Supporting both forms lets DSL users choose a layout that fits the number of items they are configuring.
Key terms: invoke method, DependencyHandler, lambda with a receiver, implicit receiver, nested block structure, flat call structure, dependencies, implementation
13.4 Kotlin DSLs in practice
Introduces upcoming practical Kotlin DSL examples involving testing, rich date literals, and database queries.
13.4.1 Chaining infix calls: The should function in test frameworks
Shows how infix calls give a statically typed testing DSL a clean, English-like assertion syntax.
- Infix assertion syntax: Explain how declaring `should` with the `infix` modifier lets a DSL express assertions with less punctuation, so a test reads almost like English.
Why it matters: It explains how an ordinary Kotlin call can become readable DSL syntax. - Matchers and typed assertions: Describe how `should` and a `Matcher` work together in the example, and explain why invalid combinations are caught at compile time.
Why it matters: The sentence-like assertion still has checked Kotlin types behind it.
Key terms: internal DSL, infix modifier, should function, Matcher, startWith, statically typed
13.4.2 Defining extensions on primitive types: Handling dates
This section explains how extension properties on Int can form a small DSL for expressing durations.
- Primitive type duration extensions: Explain how a date-manipulation DSL can use extension properties on Int for units such as days and hours, including on numeric constants. Describe what those properties return and how the approach can support a custom measurement.
Why it matters: It shows how ordinary numeric values can read like time measurements without introducing a separate syntax.
Key terms: extension properties, Int, Duration, days, hours, fortnight
13.4.3 Member extension functions: Internal DSL for SQL
This section uses Exposed’s SQL DSL to show how member extensions make column operations available only in appropriate table and query contexts.
- Table declarations and columns: Describe how an Exposed table declaration represents database structure and how its column declarations become typed properties.
Why it matters: The table declaration supplies the context in which the section’s column extensions are used. - Scoped column property extensions: Explain why Exposed declares column operations such as autoIncrement as member extensions of Table, and how the receiver type restricts their use.
Why it matters: Member extensions let the DSL express which operations make sense for a column and where they make sense. - Query condition extension scope: Trace how a select condition can use eq without explicitly naming SqlExpressionBuilder.
Why it matters: This shows how a SQL-like condition can offer comparison operations only in the context where they belong. - Member extension extensibility tradeoff: Explain the extensibility cost of making DSL operations member extensions rather than ordinary extensions.
Why it matters: The same design choice that controls DSL scope can make outside additions harder.
Key terms: Exposed, Table, Column, SchemaUtils.create, member extensions, Column<Int>, autoIncrement, receiver type, select, eq
13.5 Summary
Concepts explained in this chapter
- lambda with receiverA lambda with receiver is a lambda whose function type designates one parameter as an implicit receiver, allowing the lambda body to access that receiver's members without qualification.
- internal DSLAn internal DSL is a domain-specific language embedded in a host programming language and expressed using the host language's syntax and features.
- domain-specific languageA domain-specific language is a language designed to express operations and configurations within a particular problem domain.
- type-safe builderA type-safe builder is a builder API whose structure and allowed operations are checked by the programming language's type system.
- @DslMarker@DslMarker is a Kotlin meta-annotation for restricting which receivers of the same DSL marker are available implicitly in nested scopes.
- html builderAn HTML builder is a type-safe builder API that constructs an HTML document or fragment through nested declarations.
- method invocationThe invoke method is a Kotlin function named invoke that enables instances of its containing type to be called with function-call syntax.
- extension function typeAn extension function type is a Kotlin function type that specifies a receiver type before the parameter types, allowing values of that type to invoke the callable using extension-function syntax.
- member extension functionA member extension function is a function declared as a member of one class while extending another receiver type.
- context receiverA context receiver is a Kotlin language feature that lets a function declare an additional required receiver type.
- receiverA receiver is an entity that accepts a message or signal, or an object on which a method or command operates.
- receiver typeA receiver type is the class or interface type whose values can invoke an extension function or serve as the receiver for an extension function type.
- infix functionAn infix call is Kotlin call syntax that places a one-parameter function name between its receiver and argument without parentheses or other separators.
- extension propertyAn extension property is a Kotlin property that provides property-like access to an existing type without storing state in that type.
- implicit receiverAn implicit receiver is an object whose members are directly accessible within a callable body without an explicit object qualifier.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
