Tutorial - Pure Business Functions
Collect every validation finding
This program retains every blocking finding in its original order:
module examples.review_list
type Finding
label is Text
blocking is Boolean
function Report
takes findings as List of Finding
takes additional as List of Finding
gives Text
do
bind all as List of Finding = concat(findings, additional)
bind labels as List of Text = [item.label for item in all if item.blocking]
give join(labels, "; ")
touba compile review-list.tb --out review-list.air.json
touba run review-list.air.json --entry function:examples.review_list/Report --inputs-json '{"findings":[{"label":"Missing owner","blocking":true},{"label":"Style","blocking":false}],"additional":[{"label":"Expired policy","blocking":true}]}' --output json
Result: Missing owner; Expired policy. A list without blocking findings
produces empty text. Filtering happens before transformation and input lists
remain unchanged. Expressions stay pure: external services are called through tools.
To share a display rule, export this function from a package.
Currency and locale conventions remain package-level business choices;
join combines already formatted text.
Goal: show useful computation in Touba without a host handler.
Partition and search
Both functions preserve input order and leave the input list unchanged.
module examples.collections
type Groups
negative is List of Int
nonnegative is List of Int
function Partition
takes values as List of Int
gives Groups
do
bind negative as List of Int = [value for value in values if value < 0]
bind nonnegative as List of Int = [value for value in values if value >= 0]
give record Groups { negative as negative, nonnegative as nonnegative }
function FirstPositive
takes values as List of Int
gives Optional of Int
do
for value in values
if value > 0
give some(value)
give none(Int)
For [-2, 0, 4, -1], Partition returns negative: [-2, -1] and
nonnegative: [0, 4]; FirstPositive returns some(4). Empty input
returns two empty lists and none(Int). Each traversal uses the host's
iteration limit, including filtered elements.
Use a package tool with an effect contract of kind: none for general sums,
sorting or dynamic grouping in V1. Keep business calculations in a tested
JavaScript/Python handler and return a typed result. The current expression
builtins include concat, join, text, date, some and none.
Handle presence with if present; there is no native sum or sort.
Syntax Used
Module constant:
define Name as Type = literal
Immutable local value:
bind name as Type = expression
Function return:
give expression
Source
module examples.review_functions
define RiskThreshold as Float = 0.7
define FileThreshold as Int = 12
define ReviewPrefix as Text = "review: "
define FilesPrefix as Text = "files: "
type ChangeSignal
title is Text
high_impact is Boolean
risk is Float
files is Int
type ReviewDecision
approved is Boolean
label is Text
risk is Float
function CountRemainingReviews
takes requested as Int
takes completed as Int
gives Int
do
give requested - completed
function ComputeAdjustedRisk
takes signal as ChangeSignal
takes mitigation as Float
gives Float
do
bind adjusted as Float = signal.risk - mitigation
give adjusted
function BuildReviewLabel
takes signal as ChangeSignal
gives Text
do
bind label as Text = ReviewPrefix ++ signal.title ++ " / " ++ FilesPrefix ++ text(signal.files)
give label
function ShouldEscalateChange
takes signal as ChangeSignal
gives Boolean
do
bind risky as Boolean = signal.risk >= RiskThreshold
bind many_files as Boolean = signal.files > FileThreshold
give signal.high_impact or risky or many_files
function BuildReviewDecision
takes signal as ChangeSignal
gives ReviewDecision
do
bind label as Text = ReviewPrefix ++ signal.title
bind approved as Boolean = not signal.high_impact
give record ReviewDecision { risk as signal.risk, approved as approved, label as label }
strategy RunReview
takes signal as ChangeSignal
gives ReviewDecision
uses CountRemainingReviews
uses ComputeAdjustedRisk
uses BuildReviewLabel
uses ShouldEscalateChange
uses BuildReviewDecision
do
CountRemainingReviews
with requested as 5
with completed as 2
into remaining
ComputeAdjustedRisk
with signal as signal
with mitigation as 0.1
into adjusted
BuildReviewLabel
with signal as signal
into label
ShouldEscalateChange
with signal as signal
into escalation
BuildReviewDecision
with signal as signal
into decision
show
with message as label ++ " / remaining: " ++ text(remaining) ++ " / adjusted: " ++ text(adjusted) ++ " / escalate: " ++ text(escalation)
give decision
What the Example Shows
definefor non-sensitive thresholds;bindto name intermediate calculations;Intwithrequested - completed;Floatwithsignal.risk - mitigation;TextwithReviewPrefix ++ signal.title;- explicit text rendering with
text(signal.files); Booleanwithor,>=,>;- field access with
signal.risk,signal.files,signal.title.
Compile
touba compile review-functions.tb --out review-functions.air.json
touba run review-functions.air.json --entry strategy:examples.review_functions/RunReview --inputs-json '{"signal":{"title":"Public report","high_impact":true,"risk":0.8,"files":4}}' --output json --trace-out trace.json
The result is AIR JSON that the runtime and inspection tools can read.
The strategy explicitly calls all five functions and returns a typed decision.
The JSON result contains approved: false, label: "review: Public report"
and risk: 0.8. messages contains the displayed summary; the trace does not
copy its business text. Function-local bindings do not overwrite caller bindings.
No SDK, manifest, secret or network service is needed.
Takeaway
Pure functions keep business logic simple in Touba. External effects belong in tools.
define does not replace secrets. An API key or sensitive environment variable
must remain declared as a secret handle and explicitly resolved by the runtime.