Early preview: syntax, standard library APIs, and tooling may change.

FoundationsPure Functions

Pure Functions

pure func marks deterministic code and makes programs easy to reason about.

`pure func`

A pure func always returns the same value for the same input. It cannot perform I/O, call impure functions, mutate global state, or accept impure callbacks. Restricted debug diagnostics are the deliberate exception below.

pure func add_one(n: Int) -> Int:
	n + 1

Pragmatic Local Mutation

Local var bindings are allowed when the mutation cannot be observed outside the function.

pure func total(xs: List[Int]) -> Int:
	var sum: Int = 0

	for x in xs:
		sum += x

	sum

Keeping I/O at Program Boundaries

It's often useful to keep impure code limited to the "shell" of a program -- things like reading files, network calls, and printing. The core of your program can focus on pure data processing.

import:
	system: read_file


pure func word_count(text: String) -> Int:
	text
		.words()
		.length()


func main(args: List[String]) -> Int:
	match read_file("notes.txt"):
		Ok(text):
			print(word_count(text))
			0
		Err(msg):
			print_error(msg)
			1

Larger Boundary Example

Rejected Effects

The same file read is rejected inside pure func; the boundary has to stay visible in the caller.

import:
	system: read_file


pure func load_notes() -> Result[String, String]:
	read_file("notes.txt") -- rejected

Pure Callbacks

Collection and parallel APIs can accept pure callbacks when they need deterministic work.

items.parallel(
    -- `pure` can be omitted in closures where pure functions are required
    func(chunk): chunk.map(func(x): x * 2)
)

Debug Diagnostics

A debug: block may use approved debug diagnostics, even inside a pure function. In normal builds the block is erased; debug and test builds retain it and can write to stderr without changing the return value. This is not permission to call arbitrary impure functions, read files, or use the network.

import:
	debug


pure func score(value: Int) -> Int:
	debug:
		debug.log("score: ${value * 2}") -- stderr: score: 42 (debug/test only)
	value * 2


func main(args: List[String]) -> Void:
	print(score(21).to_string()) -- stdout: 42 (normal and debug builds)

Example

analyze-notes.brp
import:
	system: read_file


record TextStats {lines: Int, words: Int, todos: Int}


pure func count_note_lines(text: String) -> Int:
	var total: Int = 0
	for line in text.lines():
		if line.trim() != "":
			total += 1
	total


pure func count_todos(text: String) -> Int:
	var total: Int = 0
	for line in text.lines():
		if line.contains("TODO"):
			total += 1
	total


pure func analyze(text: String) -> TextStats:
	{
		lines = count_note_lines(text),
		words = text
			.words()
			.length(),
		todos = count_todos(text)
	}


pure func format_stats(stats: TextStats) -> String:
	"lines: " + stats.lines.to_string() + ", words: " + stats.words.to_string() + ", todos: " + stats.todos.to_string()


func main(args: List[String]) -> Int:
	path: String = "notes.txt"
	match read_file(path):
		Ok(text):
			print(format_stats(analyze(text))) -- prints: lines: 3, words: 11, todos: 2
			0
		Err(msg):
			print_error(msg)
			1

Try it

terminal
blorp run analyze-notes.brp