Tanguv0.32.0
s-exprtypesstructstraitsmodules

Language tutorial

Progressive step-by-step guide from installation and s-expressions to structs, traits, pattern matching, and modules.

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}!")
  • say prints values followed by a newline (echo prints without a trailing newline).
  • Strings support inline ${...} interpolation for any expression implementing To-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 def for top-level and mutually recursive function, struct, or constant declarations (hoisted within their scope).
  • Use let for 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:

  1. Zero parameters: (Func () body)
  2. Single parameter: (Func x (* x 2))
  3. Multiple or typed parameters: (Func [#Int a #Int b] (+ a b)) (with optional signature annotation #(Func [Int Int] Int) stacked above def)
; 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))
))