Installation & first steps #
Installing Tangu #
Install the tangu CLI, Language Server (tangu-lsp), formatter (tangu-format), and standard libraries with the official installer script:
curl -fsSL https://tangu.dev/install.sh | sh
Verify your installation and inspect the active toolchain paths:
tangu --version
tangu doctor
Hello, Tangu #
Create hello.tan and run it with tangu run hello.tan (or tangu r hello.tan):
(let language "Tangu")
(say "Hello from ${language}!")
sayprints values followed by a newline (echoprints without a trailing newline).- Strings support inline
${...}interpolation for any expression implementingTo-String. - Line comments start with
;(or;;for module headers).
Primitives, literals & sized integers #
Tangu provides rich literal syntax for numbers, exact decimals, strings, characters, booleans, and symbols:
(let count 42) ; Int (64-bit signed integer)
(let hex-mask 0xff_00) ; Hexadecimal Int literal (0b... binary, 0o... octal)
(let ratio 3.14159) ; Float (64-bit IEEE 754)
(let price 19.99d) ; Dec (128-bit exact fixed-point decimal for finance)
(let is-active true) ; Bool (true or false)
(let letter (Char "A")) ; Char (Unicode scalar value)
(let status :ready) ; Key-Symbol (self-evaluating interned keyword)
(let empty ()) ; None / unit value
Explicit sized integers #
When interoperating with binary protocols, SIMD, or databases, use sized integer literal suffixes or explicit constructors:
- Signed:
I8,I16,I32,I64(Int),I128,ISize - Unsigned:
U8(Byte),U16,U32,U64,U128,USize
(let port 8005u16)
(let byte-val 255u8)
(let idx 1000usize)
(let offset (I32 -128))
Bindings, scope & mutability #
def vs let #
- Use
deffor top-level and mutually recursive function, struct, or constant declarations (hoisted within their scope). - Use
letfor sequential local bindings inside functions and scripts.
; Always stack annotations (#pub, #inline, #test) vertically above (def ...)
#pub
#inline
(def square (Func x (* x x)))
(let result (square 12))
Rebindable variables (#var) vs interior mutation (#mut) #
Bindings are immutable by default. Tangu distinguishes between rebinding a variable (#var) and mutating a collection in place (#mut):
; #var allows reassigning the variable binding with <- or +<-
(let #var total 0)
(<- total 10)
(+<- total 5) ; total is now 15
; #mut allows mutating the contents of a collection or struct
(let #mut items [1 2 3])
(push items 4) ; items is now [1 2 3 4]
Collections, key-paths & negative indexing #
Arrays, lists, maps, sets & ranges #
(let nums [10 20 30 40]) ; Array (contiguous growable vector)
(let quoted '(a b c)) ; List (s-expression linked list)
(let tags (Set ["web" "db"])) ; Set (unique hashed values)
(let r 0..10) ; Half-open Range (0 through 9)
(let evens 0..=10|2) ; Inclusive Range with step 2 (0, 2, 4, 6, 8, 10)
Maps ({...}) accept :key symbols or string keys without commas, and support ... map spreading and {_ var} punning:
(let host "127.0.0.1")
(let port 8005)
(let base-config {_ host _ port}) ; Punned keys {:host host :port port}
(let dev-config {...base-config :debug true}) ; Spread base-config and override/add keys
Universal sequence operations #
All sequence types (Array, List, String, Set, Map, Range, Slab) share a single vocabulary in the prelude:
(count-of nums) ; => 4
(is-empty? nums) ; => false
(contains? nums 20) ; => true
(first-of nums) ; => 10
(last-of nums) ; => 40
(nth-of nums 2) ; => 30
(nums -1) ; => 40 (negative index from the end)
(slice nums 1 -1) ; => [20 30]
(reverse nums) ; => [40 30 20 10]
Required (:) vs optional (?) key-path navigation #
Navigate nested maps and structs concisely using : for required fields and ? for nil-safe optional traversal:
(let payload {:user {:profile {:handle "gmosx"}}})
(let handle payload:user:profile:handle) ; => "gmosx" (errors if missing)
(let bio (some-or payload?user?profile?bio "No bio yet"))
Control flow & pattern matching #
Strict boolean conditions (if, unless, cond) #
Conditions in Tangu require a strict Bool (true or false)—never rely on truthy/falsy coercion:
; Multi-statement if with explicit (else ...)
(if (not (is-empty? items))
(say "Processing items")
(first-of items)
(else
(say "Queue is empty")
()
))
; Flat multi-branch cond with _ default arm
(let label (cond
(== status :ok) "Healthy"
(== status :degraded) "Warning"
_ "Offline"
))
Structural pattern matching (when) #
Use when to match literals, runtime types, array destructuring ([head ...tail]), map keys ({:role r}), and Error values:
(def handle-response (Func resp
(when resp
{:status 200 :body b} "OK: ${b}"
{:status 404} "Not found"
[first ...rest] "List starting with ${first} (${(count-of rest)} more)"
{:variant v :message msg} "Failed (${v}): ${msg}"
_ "Unhandled response"
)
))
Loops (for, for->seq, while) #
Use for for side effects and for->seq to build an Array comprehension (using (continue) to skip elements):
; Iterate over values in a sequence
(for [name ["alpha" "beta"]]
(say name)
)
; Comprehension producing [4 16 36]
(let even-squares (for->seq [n 1..=6]
(if (!= (% n 2) 0)
(continue)
)
(* n n)
))
Functions, implicit closures & pipelines #
Function parameter forms (Func) #
Func uses three canonical parameter shapes:
- Zero parameters:
(Func () body) - Single parameter:
(Func x (* x 2)) - Multiple or typed parameters:
(Func [#Int a #Int b] (+ a b))(with optional signature annotation#(Func [Int Int] Int)stacked abovedef)
; Rest parameters (...rest) and map destructuring
(def format-tag (Func [{name tone} ...children]
"${name}:${tone} (${(count-of children)})"
))
Implicit closures (%0, %1) & pipelines (|>) #
For concise higher-order transforms, %0 (or %), %1, %2 create an implicit closure automatically, and |> threads a value through expression chains via %0:
(let doubled (map [1 2 3 4] (* %0 2))) ; => [2 4 6 8]
(let big (filter doubled (> %0 4))) ; => [6 8]
; Pipeline threading
(let summary (|> " hello tangu world "
(trim %0)
(split %0 " ")
(count-of %0)
)) ; => 3
Structs, validation constraints & traits #
Defining structs & field constraints #
Define nominal product types with Struct, optional #(Or T) fields, default values, and declarative validation constraints (#{...}):
#pub
(def Account (Struct
#Int #{:min 1} id
#String #{:format :email} email
#String #{:min-len 2 :max-len 32} handle
#(Or String) bio
(#String status "active")
))
Validate dynamic maps before constructing a Struct using /validation:
(use {validate} /validation)
(let raw {:id 1 :email "dev@tangu.dev" :handle "gmosx"})
(let checked (validate raw Account))
Pure copy-update vs in-place mutation #
(let acc (Account {:id 1 :email "dev@tangu.dev" :handle "gmosx"}))
; Pure copy-update (returns a new Account with updated :bio)
(let updated (Account acc {:bio "Systems programmer"}))
; In-place mutation on a #mut binding
(let #mut mutable-acc (clone acc))
(<- mutable-acc:bio "Updated in place")
Deriving standard traits (derive) #
Prefer (derive ...) over manual boilerplate for standard traits:
(def Point (Struct #Int x #Int y))
(derive Point Eq Hash Ord Clone To-String Default To-JSON From-JSON)
Custom traits & implementations (Trait & impl) #
(def Renderable (Trait
#(Func This String)
(render (Func this))
))
(impl Renderable Point
(let render (Func self "(${self:x}, ${self:y})"))
)
Error handling (Error, try, ok-or, some-or) #
Tangu represents recoverable failures as first-class Error values rather than unchecked exceptions:
#(Func String (Or Int Error))
(def parse-port (Func raw
(let n (try (Int raw) (else
(Error :invalid-port "Port must be an integer: ${raw}")
)))
(if (or (< n 1) (> n 65_535))
(return (Error :out-of-range "Port ${n} is outside 1..65_535"))
)
n
))
; Local fallback for Error values with ok-or
(let port (ok-or (parse-port "8005") 8000))
; Local fallback for None () values with some-or
(let host (some-or config?host "127.0.0.1"))
Modules, testing & tooling #
Directory-as-module & use imports #
In Tangu, a directory is a module: all .tan files in a directory merge into a single module namespace. Export public symbols with #pub stacked above (def ...) or (let ...):
; Sort imports alphabetically by module path:
(use /fs)
(use {strip-prefix strip-suffix} /string)
(use {<card>} @acme/ui/components/cards)
(use {helper} ./utils)
Writing #test suites (*.test.tan) #
Place tests in *.test.tan files alongside your code or under tests/ and run tangu test:
#test
(def point-equality-and-update-works (Func ()
(let p1 (Point 10 20))
(let p2 (Point p1 {:x 99}))
(assert-eq! p1:x 10)
(assert-eq! p2:x 99)
(assert! (!= p1 p2))
))