Skip to main content
Version: 1.0.0

Tutorial - Pure Business Functions

Collect every validation finding​

This program retains every blocking finding in its original order:

review-list.tb
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.

Both functions preserve input order and leave the input list unchanged.

collections.tb
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:

source.tb
define Name as Type = literal

Immutable local value:

source.tb
bind name as Type = expression

Function return:

source.tb
give expression

Source​

review-functions.tb
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​

  • define for non-sensitive thresholds;
  • bind to name intermediate calculations;
  • Int with requested - completed;
  • Float with signal.risk - mitigation;
  • Text with ReviewPrefix ++ signal.title;
  • explicit text rendering with text(signal.files);
  • Boolean with or, >=, >;
  • field access with signal.risk, signal.files, signal.title.

Compile​

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