Skip to content

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.

A @paradigm directive at the top of a file declares a paradigm that file uses:

@paradigm functional
@paradigm procedural

The 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.

DirectiveUnlocks
@paradigm proceduralfor, while, do while, mut variables
@paradigm functionalmap, filter, reduce, fold, foreach, do notation, the monad keyword, pure function enforcement
@paradigm oopinterface, 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 code uses loops and mutable variables. This is the everyday imperative style:

default.dusk
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.

@paradigm functional unlocks the five collection builtins, which take lambdas that capture outer variables by immutable copy:

pipeline.dusk
@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.

@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:

speak.dusk
@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.

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.

Directives stack, and the set of available builtins and syntax is the union of all declared paradigms:

mixed.dusk
@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.

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 through impl in any file. Only the interface declaration 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 builtins spawn, join, and submit work 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.

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 functional cannot call map directly, but it can call a user-defined function that internally uses map.
  • 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:

list_utils.dusk
@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.

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.