Paradigms
dusk is a multi-paradigm language with enforced discipline. Every source file declares which paradigms it uses, and the compiler rejects paradigm-specific builtins and syntax in files that have not opted in. Nothing is hidden: reading the top of a file tells you exactly which kinds of constructs can appear below.
This guide covers the three directives, what each one unlocks, how they stack, and how gating interacts with code in other files. The normative rules live in the paradigm system reference.
Declaring a paradigm
Section titled “Declaring a paradigm”A @paradigm directive at the top of a file declares a paradigm that file uses:
@paradigm functional@paradigm proceduralThe directive can be repeated to stack paradigms. Directives unlock the builtins, syntax, and keywords associated with each paradigm within the current file. They do not affect which functions from other files can be called. Only builtins and syntax are gated, and only inside the file that uses them.
What each paradigm unlocks
Section titled “What each paradigm unlocks”| Directive | Unlocks |
|---|---|
@paradigm procedural | for, while, do while, mut variables |
@paradigm functional | map, filter, reduce, fold, foreach, do notation, the monad keyword, pure function enforcement |
@paradigm oop | interface, composition syntax |
A note on the functional row: the spec reserves pure function enforcement under @paradigm functional. In 0.3.3 the checker gates the five builtins, do notation, and monad blocks.
Procedural
Section titled “Procedural”Procedural code uses loops and mutable variables. This is the everyday imperative style:
func main() -> int32 { xs: int64[3] = [10, 20, 30] mut sum: int64 = 0 for x in xs { sum = sum + x } println(sum) return 0}Note that this file has no directive at all. See the default is procedural below.
Functional
Section titled “Functional”@paradigm functional unlocks the five collection builtins, which take lambdas that capture outer variables by immutable copy:
@paradigm functional
func main() -> int32 { nums: int64[] = [1, 2, 3, 4, 5]
evens := filter(nums, lambda (n: int64) -> bool { return n % 2 == 0 }) doubled := map(evens, lambda (n: int64) -> int64 { return n * 2 }) sum := fold(doubled, 0, lambda (acc: int64, n: int64) -> int64 { return acc + n })
println(sum) return 0}The same directive gates the monad keyword and do notation, which the standard library’s Maybe, Either, and friends build on. See the functional reference and std.functional.
Object oriented
Section titled “Object oriented”@paradigm oop unlocks interface declarations. There are no classes and no inheritance; a struct satisfies an interface through an explicit impl, and composition is the only way to combine behaviors:
@paradigm oop
struct Dog { sound: int64,}
interface Animal { speak() -> int64}
impl Animal for Dog { func speak() -> int64 { return self.sound }}
func describe(a: Animal) -> int64 { return a.speak()}
func main() -> int32 { d := Dog { sound: 70 } println(describe(d)) return 0}See the OOP reference for the full rules.
The default is procedural
Section titled “The default is procedural”A file with no @paradigm directive defaults to procedural. That is why default.dusk above compiles: mut and for are available without any directive.
The default is replaced, not extended, the moment you declare a paradigm explicitly. A file that declares only @paradigm functional is no longer procedural, so mut in that file is an error:
@paradigm functional
func main() -> int32 { mut x: int64 = 0 // error: the 'mut' keyword requires the // procedural paradigm; add '@paradigm procedural' return 0}If you want both styles, declare both. That is what stacking is for.
Stacking paradigms
Section titled “Stacking paradigms”Directives stack, and the set of available builtins and syntax is the union of all declared paradigms:
@paradigm functional@paradigm procedural
func main() -> int32 { nums: int64[] = [1, 2, 3, 4]
// Procedural: a loop with mutable state. mut sum: int64 = 0 for n in nums { sum = sum + n } println(sum)
// Functional: the same numbers through map and fold. squares := map(nums, lambda (n: int64) -> int64 { return n * n }) total := fold(squares, 0, lambda (acc: int64, n: int64) -> int64 { return acc + n }) println(total)
return 0}Stacking is common in the repository’s own examples: examples/display.dusk stacks oop and procedural, and examples/m8c.dusk stacks procedural and oop to define an Allocator interface alongside plain procedural code.
What is never gated
Section titled “What is never gated”Only paradigm-specific builtins and syntax are gated. The rest of the language is paradigm agnostic:
- User-defined names. Functions, structs, and enums you define are usable from any file regardless of paradigm directives.
- Structs and
impl. Structs are plain data containers available across all paradigms, not gated by@paradigm oop. Methods can be associated with structs throughimplin any file. Only theinterfacedeclaration itself is gated. - Enums. Tagged unions are a first-class, paradigm-agnostic data type, like structs.
- Always-available builtins.
alloc,free,print,println,printerr,sizeof, and the concurrency builtinsspawn,join, andsubmitwork in every file. See builtins. - Imports and exports. Imports are independent of paradigm directives; importing a module does not grant any paradigm, and there is no paradigm restriction on exports.
Cross-file rules
Section titled “Cross-file rules”Gating is per file and covers builtins and syntax, nothing else:
- Functions and types defined in any file are paradigm agnostic. They can be called or used from any other file regardless of either file’s directives.
- A file without
@paradigm functionalcannot callmapdirectly, but it can call a user-defined function that internally usesmap. - The check is intra-file and runs during semantic analysis; nothing is checked at link time, because calls through user-defined functions are never gated. The paradigm system reference states these rules normatively.
In practice this means a module can encapsulate a paradigm. A functional file can export a helper:
@paradigm functional
export func double_all(xs: int64[]) -> int64[] { return map(xs, lambda (n: int64) -> int64 { return n * 2 })}and a plain procedural file can import and call it without declaring functional:
// app.dusk, no functional directive needed@import list_utils
func main() -> int32 { doubled := list_utils.double_all([1, 2, 3]) println(doubled[0]) return 0}The caller never touches map directly, so no gate applies.
The errors you will see
Section titled “The errors you will see”Using a gated construct without its directive is a compile error during semantic analysis, and the message names the fix:
error: the 'mut' keyword requires the procedural paradigm; add '@paradigm procedural'Functional builtins, do notation, and monad blocks report requires the functional paradigm, and an interface declaration reports requires the oop paradigm. A directive naming anything other than the three paradigms unlocks nothing: the prescanner flags it as unknown paradigm '<name>' (visible via dusk scan), but in 0.3.3 dusk check and dusk build ignore the bad directive and the file keeps whatever paradigms its remaining directives declare, or the procedural default if there are none.
Where to go next
Section titled “Where to go next”- Language tour for the core syntax shared by all paradigms.
- Paradigm system reference for the normative rules.
- Functional reference for monads and do notation in depth.
- OOP reference for interfaces and
impl. - Examples for complete programs in each style.