Language Comparison · U vs the World
Ten languages, ten design philosophies, one consistent set of categories. Where each wins, where U wins, and why the tradeoffs exist.
The reigning language of ML, scripting, and science. U is designed to be Python's natural successor.
Python uses reference counting with a cycle collector. Everything is a heap object. Stack allocation doesn't exist as a concept. The GIL (Global Interpreter Lock) prevents true multi-core parallelism in CPython. Memory overhead per object is substantial — a simple integer is 28 bytes.
# Every value heap-allocated # GIL prevents true parallelism x = 42 # 28 bytes on heap arr = [1,2,3] # list of heap objects # No stack/heap distinction
// Stack by default (-R) x: I = 42 // 8 bytes, stack arr: [I] = [1,2,3] // contiguous, stack heap: I+R = 42 // explicit heap opt-in // No GIL — cactus stack + biased ARC
Python's None can appear anywhere. No type system prevents AttributeError: 'NoneType' object has no attribute 'x'. Type hints help but aren't enforced at runtime. Optional[T] is just a hint.
user = users.get(id) # None if missing — no warning print(user.name) # 💥 AttributeError
user: User+N = users[id] // +N forces you to handle absence user ?? r => none // must handle print(user.name) // safe — non-null
The coloring problem: async def functions can only be awaited from other async def functions. Making any function async requires rewriting every caller up the entire call chain. Python has three largely-incompatible concurrency models: asyncio, threading, multiprocessing. When await suspends, the entire call stack is serialised into a heap-allocated coroutine object — local variables that survive the suspension are captured in that closure. Stack frames are lost across suspensions.
# foo() needs to be async... async def foo(config, threshold): ... # ...so bar() must become async (infected) async def bar(): config = load_config() # local threshold = 42 # local # awaiting serialises config+threshold # into a heap closure before yielding result = await foo(config, threshold) # ...so main() must become async (infected) async def main(): await bar() # infected asyncio.run(main()) # ceremony at every entrypoint
// foo() is async a f foo(config: Config, threshold: I) -> S: ... // bar(): NOT async — not infected f bar() config = load_config() // on stack segment threshold = 42 // on stack segment // foo suspends → new cactus BRANCH // bar's segment stays alive // config+threshold reachable via parent ptr result = a foo(config, threshold) // main(): NOT async — not infected f main() bar() // plain call — just works
How U eliminates coloring — the cactus stack. When a foo() suspends inside a plain function, U does not unwind the call stack. It creates a new branch of the cactus tree. The parent frame — its local variables, loop state, everything — stays alive in its stack segment, kept by a reference count on the parent pointer held by the child fiber. When the child completes, the count drops and the parent resumes. No heap closure. No serialised continuation. No refactoring cascade.
// config and threshold live on parent stack segment config = load_config() // -R, on stack threshold = 42 // -R, on stack // a foo() — if foo is sync: zero cost, same stack // if foo suspends: new branch created // parent segment stays alive via parent pointer ARC result = a foo(config, threshold) // Three concurrent branches — same parent segment r1: S+A = a foo(config, threshold) // branch 1 r2: S+A = a foo(config, threshold) // branch 2 r3: S+A = a foo(config, threshold) // branch 3 // config and threshold NOT copied — referenced via parent pointer // No @escaping. No [weak self]. No Arc<Mutex<>>. No Sendable.
main() segment │ config = ..., threshold = 42 │ ├── foo() branch 1 ← waiting on network ├── foo() branch 2 ← waiting on network └── foo() branch 3 ← waiting on network Parent segment refcount = 3 (one per branch) When branch completes: refcount-- When refcount == 0: parent resumes No copies. No closures. O(1) per branch.
Python's type hints (PEP 484) are annotations only — ignored at runtime unless mypy or pyright is run separately. Duck typing means any object can be passed anywhere. No modifier algebra. No ownership. No compile-time guarantees of any kind.
def process(items: list[Item]) -> None:
items[0].count += 1 # mutates silently
# caller: mutates? async? throws? Unknown.f process(items: [Item]+R+M) -> none // +M: mutates no +A: sync no !: no throw // Everything in the signature
Vectorization, GPU, exact arithmetic, autodiff. Python needs NumPy for SIMD vectorization, PyTorch or JAX for GPU tensors, the decimal or fractions module for exact arithmetic, and an autograd library for differentiation. In U these are built into the type and modifier system — no imports, no dtype systems, no separate compilation targets.
import numpy as np
import torch
# NumPy: separate dtype system
a = np.array([1.0, 2.0, 3.0], dtype=np.float32)
c = a + a # element-wise — works
# PyTorch GPU: separate placement API
t = torch.tensor([1.0, 2.0, 3.0]).cuda()
w = torch.randn(512, 512, device="cuda")
# Exact: decimal module, manual everywhere
from decimal import Decimal
price = Decimal("1") / Decimal("3") # 1/3
# Autodiff: requires grad tracking
x = torch.tensor(2.0, requires_grad=True)
y = x ** 3
y.backward()
x.grad # 12.0 — but only for torch ops# +V: SIMD — compiler picks ISA (SSE/AVX/NEON) a: [N32]+V = [1.0, 2.0, 3.0] c = a + a // SIMD dispatch # +R(GPU): GPU placement — same API, different loc t: [N32]+R(GPU) = [1.0, 2.0, 3.0] // copy to GPU w: [[N32]]+R(GPU) = load_weights() // 2D on GPU c_gpu = t + t // GPU kernel dispatched # Q: exact rational — standard type, no import price: Q = Q(1, 3) // exactly 1/3 total = price * 99 // exactly 33 — symbolic # Autodiff: standard library, any expression x: N = 2.0 y = x ^ 3 dy_dx = Diff.grad(y, x) // 12.0 — works on all N
[[N32]] on CPU, [[N32]]+V for SIMD, [[N32]]+R(GPU) on GPU. The operations (+, *, @, ^) are identical — the modifier routes dispatch. No .cuda(), no device=, no dtype conversion. The Q type carries exact rationals symbolically until =! forces materialisation — intermediate calculations never accumulate float error.Python uses exceptions — any code can throw anything, nothing is declared. try/except is a runtime scope. Happy path and error path are interleaved. No exhaustiveness checking.
def run(url):
try:
data = fetch(url) # buried
return process(data) # buried
except NetworkError: retry()
except: pass # swallows everythingf run(url: S) -> Result+A
e.x([retry(3), log])
data = fetch(url) // clean happy path
r => process(data)Python decorators (@cache, @retry) apply at runtime — call overhead on every invocation, no type safety. __getattr__ and metaclasses are powerful but untyped. exec and eval exist. No compile-time execution equivalent to c f.
@functools.lru_cache(maxsize=128) # runtime
@retry(stop=stop_after_attempt(3)) # runtime
def fetch(url: str) -> str:
return requests.get(url).text
# call overhead every invocationfetch = c memo(c retry(3,
http`GET {url: S}`
))
// c f: compile-time — zero runtime overhead
// cache key = function hash + typed inputsPython f-strings are untyped string interpolation. SQL injection requires ORMs or discipline. HTML injection requires escaping libraries. None of this is enforced by the type system. cursor.execute(f"SELECT * FROM users WHERE id = {id}") is a valid, dangerous pattern.
id = req.args["id"]
cursor.execute(f"SELECT * FROM u WHERE id={id}")
# id="0 OR 1=1" leaks entire tableid: I = req.params["id"]
sql`SELECT * FROM u WHERE id = {id}`()
// I slot: parameterised — never string-interpolatedPython's syntax is genuinely elegant — clean indentation, readable English keywords, minimal ceremony. U's syntax is heavily inspired by Python and intentionally reads similarly. The -> return type annotation is borrowed directly from Python. One key difference: Python accumulated many ways to do things (format strings, async models, class decorators); U keeps one obvious way.
"x=%s" % x / "x={}".format(x) / f"x={x}"
# Three concurrent models: asyncio / threading / multiprocessing
# Type hints not enforced at runtime"x={x}" // one style, always typed
// One concurrent model: +A + cactus
// Types enforced at compile time, alwaysJavaScript with a type layer. The dominant language of the web. U is what TypeScript would be if designed from scratch for the agentic era.
GC via V8's generational garbage collector. No stack allocation. Objects, closures, and async continuations all heap-allocate. Every await serializes the call stack to a heap-allocated Promise closure.
async function bar() {
const cfg = loadConfig() // must survive await
const r = await foo(cfg) // cfg → heap Promise closure
}f bar() // NOT async
cfg = load_config() // on stack segment
result = a foo(cfg) // cactus branch
// cfg: parent ptr, no heap closureTypeScript has strictNullChecks — disabled by default, must be opted in. Even then, any escapes all checks. JavaScript's undefined and null are distinct but often conflated. Prototype property access returns undefined silently.
// strictNullChecks must be enabled // and can be bypassed with 'any' const user: User | undefined = getUser(id) user.name // error (with strict) (user as any).name // bypasses — compiles
// -N is the default — non-null // No 'any' escape hatch user: User+N = getUser(id) user.name // must check first user ?? r => none user.name // ✓ safe
Full coloring problem — async infects every caller. Two queues (macrotask / microtask) with real starvation risk: a long chain of awaits can starve I/O callbacks indefinitely. Every await serialises local variables into a heap Promise closure before yielding. No true threads — Web Workers require explicit message passing, no shared memory beyond SharedArrayBuffer.
// foo() goes async... async function foo(cfg: Config, n: number) {} // ...bar must become async (infected) async function bar() { const cfg = loadConfig() // must survive await const n = 42 // cfg and n serialised into heap Promise closure const r = await foo(cfg, n) } // main must become async (infected) async function main() { await bar() }
a f foo(cfg: Config, n: I) -> S: ... f bar() // NOT async cfg = load_config() // on bar's stack segment n = 42 // on bar's stack segment // foo suspends → cactus branch // bar's segment stays: cfg, n reachable r = a foo(cfg, n) // no closure needed f main() bar() // plain — not infected
Why U has no coloring — the cactus stack. When a foo() suspends, U creates a new branch on the cactus tree. The calling function's stack segment stays alive — its local variables reachable via a parent pointer held by the child fiber. When the child completes, the parent resumes. No heap closure, no continuation serialisation, no refactoring cascade. If foo is synchronous, a foo() is a zero-cost no-op — same call stack, zero overhead. This means you can always write a foo() defensively; the compiler handles both cases.
// Calling function is NOT async — no annotation needed f bar() config = load_config() // lives on bar's stack segment threshold = 42 // lives on bar's stack segment result = a foo(config, threshold) // foo suspends: // ┌─ bar's segment ─────────────────┐ ← alive, refcount = 1 // │ config, threshold │ // └──────────────────────────────────┘ // └── foo() branch ← waiting, holds parent ptr // When foo() completes: refcount-- → bar resumes // config and threshold: NOT copied, NOT closed over
Structural typing — a strength for web APIs but means two unrelated types with the same shape are compatible. No ownership. No modifier algebra. readonly is shallow — nested objects are still mutable. Union types are powerful but complex; conditional types can become arcane.
interface User { name: string; age: number }
interface Product { name: string; age: number }
const p: Product = { name:'x', age:1 }
greet(p) // TypeScript: ✓ (structural match)d User { name: S; age: I }
d Product { name: S; age: I }
p: Product = Product(name:'x', age:1)
greet(p) // ✗ type error — nominalUntyped exceptions — anything can be thrown, nothing is declared. Caught values are typed unknown (since TS 4.0) and must be narrowed. No exhaustiveness on catch. Async errors require .catch() or try/await — separate patterns for same concept.
try {
const r = await fetch(url)
} catch (e: unknown) {
if (e instanceof NetworkError) retry()
else throw e // rethrow unknown
}f run(url: S) -> Data+A
e.x([retry(3), log])
data = fetch(url) // happy path
r => process(data)TypeScript's type-level programming (conditional types, mapped types, template literal types) is powerful but operates only on types — no values. Decorators are stage-3 and primarily runtime. No compile-time execution. Code generation requires separate tooling.
// Type-level: no values type NonNull= T extends null ? never : T // Runtime decorator: @Cache({ ttl: 60 }) fetch(url: string) {} // call overhead
// c f: compile-time values AND types
fetch = c memo(c timeout(5000,
http`GET {url: S}`
)) // zero runtime overheadTagged template literals exist but are rarely typed safely. SQL injection is a discipline problem — no type system enforcement. XSS requires sanitisation libraries. `SELECT * FROM users WHERE id = ${id}` compiles fine and is dangerous.
// Compiles — dangerous db.query(`SELECT * FROM users WHERE id = ${userId}`) // userId = "0 OR 1=1" → leak
// Type error — S ≠ Sql sql`SELECT * FROM users WHERE id = {userId: I}`() // userId bound as parameter, not string
TypeScript uses : T for type annotations — U now uses -> for function return types (aligned with Python/Rust/Swift). TS's structural typing makes APIs flexible; U's nominal-ish system with modifier algebra is more explicit. TypeScript's tooling — VSCode integration, Language Server — is best-in-class.
const [a,b,c] = await Promise.all([ fetchA(), fetchB(), fetchC() ]) // null | undefined confusion everywhere
a: S+A = a fetch_a() b: S+A = a fetch_b() c: S+A = a fetch_c() // +N for nullable. -N default. No undefined.
any escape hatchThe closest spiritual relative. Same safety goals, fundamentally different philosophy: Rust proves non-escape; U declares it.
Rust's borrow checker statically proves no aliased mutation exists. Ownership is tracked via lifetimes. Zero GC. The key difference from U: Rust proves non-escape via whole-program analysis; U requires you to declare it with -R. Rust's analysis catches mistakes; U's annotation eliminates the analysis entirely. Both produce safe code — different ergonomic costs.
// Borrow checker proves non-escape // Shared mutable requires Arc<Mutex<T>> let state = Arc::new(Mutex::new(Config::new())); let state2 = Arc::clone(&state); thread::spawn(move || { let mut s = state2.lock().unwrap(); s.update(delta); });
// Declare the fact — compiler reads it // MVCC allows multiple concurrent writers state: Config+R+M(MVCC) = Config() snap = state.c() // snapshot state.update(delta) // concurrent safe
Rust's Option<T> is excellent — enforced, exhaustive pattern matching. U's T+N achieves the same result with lighter syntax and makes non-null the default (no annotation needed for the common case). Rust requires wrapping everything in Some(); U just uses values directly.
let user: Option= db.find(id); user.unwrap().name // panics if None // Safe: if let Some(u) = user { u.name }
user: User+N = db.find(id) user.name // ✗ compile error user ?? r => none // handle user.name // ✓ safe
Rust async is stackless: the compiler transforms async fn into a state machine storing only variables that cross .await points. Lean — but with three costs. First, coloring: async fn infects every caller. Second, you cannot .await inside a non-async closure, so iterating a list and awaiting per item requires collecting futures then joining — the natural pattern is blocked. Third, async trait methods need Pin<Box<dyn Future>>.
async fn foo(cfg: &Config, n: u32) -> String { ... }
async fn bar() {
let cfg = load_config();
let n = 42u32;
// CANNOT await inside non-async closure:
// items.iter().for_each(|i| foo(&cfg,n).await) ✗
// MUST collect futures then join:
let futs: Vec<_> = items.iter()
.map(|_| foo(&cfg, n)).collect();
futures::join_all(futs).await;
}
// Async trait: requires Pin<Box<dyn Future+Send>>a f foo(cfg: Config, n: I) -> S: ...
f bar() // NOT async
cfg = load_config() // on bar's segment
n = 42
// await INSIDE iterator — just works
items.x(item =>
result = a foo(cfg, n) // cactus branch
store(result))
// No join_all. No collect. No Pin.Pin<Box<dyn Future>> exists because Rust's async state machine contains self-references and must not be moved in memory after polling begins. The cactus branch is a stack segment linked by a parent pointer — it doesn't move, needs no pinning, contains no self-references.
The cactus stack. When a foo() suspends, U creates a new branch. The caller's stack segment stays alive — its local variables reachable via a parent pointer held by the child fiber. When the child completes, the parent resumes. No heap closure. No serialisation. No infection. Writing a foo() where foo is synchronous is a zero-cost no-op.
// bar() is NOT marked async f bar() cfg = load_config() // on bar's stack segment n = 42 result = a foo(cfg, n) // foo suspends: // ┌ bar segment alive ──────────┐ refcount = 1 // │ cfg, n │ // └─────────────────────────────┘ // └── foo branch ← waiting, holds parent ptr // foo completes → refcount-- → bar resumes // cfg and n were NEVER copied or closed over
Rust's type system is powerful: traits, generics with bounds, associated types, const generics. Lifetime parameters add a third dimension beyond types. The annotation burden can be substantial: Arc<Mutex<HashMap<String, Vec<Box<dyn Trait + Send + Sync>>>>> is real code. U's modifier algebra expresses ownership, mutability, and async in 8 short symbols.
// Real production type: HashMap>>>> // Lifetime on every borrowed reference
// 8 modifiers cover all of this:
{S: [Trait+R+M]+R}
// No Box, no dyn, no Send+Sync, no 'lifetimeRust's Result<T, E> with the ? operator is excellent — typed, exhaustive, propagating. U's ! ErrorType in the function signature with e.x() handlers achieves the same goals with zero happy-path noise: handlers are declared once as policy arrays, never interleaved with logic.
fn run(url: &str) -> Result {
let raw = fetch(url)?; // ? per call
let data = parse(raw)?; // ? per call
Ok(data)
}f run(url: S) -> Data ! NetworkError
raw = fetch(url) // happy path
data = parse(raw) // happy path
r => dataRust has two macro systems: declarative (macro_rules!) and procedural (#[proc_macro]). Both are powerful but separate from the type system — proc macros operate on token streams, not typed ASTs. Compilation time suffers. U's c f is the same language in a different execution domain — typed, composable, fast.
// Declarative:
macro_rules! vec { ($($x:expr),*) => ... }
// Proc macro: separate crate, TokenStream API
#[proc_macro] pub fn make(input: TokenStream) -> ...// c f: same language, typed, generic:
c f retry[T](func: F(S)->T, n: I) -> F(S)->T
r => url => [1..n].find(i => (
result = func(url)
result != none ? result : none))
// Types checked. Author writes error messages.Rust has no built-in template safety for SQL or HTML. Libraries like sqlx verify queries at compile time against a live database (requires a DB connection at compile time). U's sql\`\` tag validates against the schema loaded at compile time — same goal, cleaner mechanism, no live DB required.
// sqlx: connects to DB during build
query!("SELECT * FROM users WHERE id=$1", id)
// No live DB available → build fails
// CI must provision a databasesql`SELECT * FROM users WHERE id={id: I}`()
// __schema() loads schema file — no live DB
// CI: schema.sql in repo is enoughRust's learning curve is famously steep — lifetimes, the borrow checker, and Pin require significant mental model investment. The reward is well-earned. U's approach — declare facts instead of proving them — has a shallower curve for the same safety. The cactus stack alone eliminates most lifetime concerns.
#[async_trait]
trait Handler: Send + Sync {
async fn handle(&self) -> Result<(), Err>;
} // => Pin> under hood d Handler
a f handle() -> none ! Error
// +A inferred. No Pin. No Box. No Send+Sync.Pin, no Arc<Mutex<>>Closest to U in concurrency ergonomics. Goroutines solved coloring; U does the same and adds safety and frame minimisation.
GC with periodic pauses. Goroutines save entire stacks on growth — no equivalent of +A to minimise what's preserved. Stack copying adds latency spikes under load. No per-value ownership or annotation.
func make() *Result {
r := &Result{val: 42} // compiler: escapes to heap
return r // GC tracks it
} // goroutine stacks copied on growthf make() -> Result // stack copy returned f make() -> Result+R // explicit heap // Cactus segments: never copied, fixed size
nil pointers cause runtime panics. No type system enforcement. Interface values can be nil. The zero value is a well-designed convention but still allows nil pointer dereference without compile-time warning.
user := getUser(0) // returns nil fmt.Println(user.Name) // nil panic // Interface nil trap: var err *MyError = nil var e error = err // e != nil!
user: User+N = get_user(0) user.name // ✗ compile error user ?? r => none // must handle user.name // ✓ safe
Goroutines solve coloring — any function can block. But goroutine stacks are copied on growth (not segmented), adding latency. No typed +A to tell the runtime "save only these values." WaitGroup ceremony for fan-out/fan-in. Channels are elegant; select statements are powerful.
var wg sync.WaitGroup results := make([]Config, len(keys)) for i, key := range keys { wg.Add(1) go func(i int, k string) { defer wg.Done() results[i] = lookup(k) }(i, key) } wg.Wait()
jobs: [Config+A] = keys.map(k => a lookup(k)) cfgs: [Config] = jobs // auto-resolve // No WaitGroup. No closure index bug. // Frame = only +A values (not full stack)
Go's type system is intentionally simple — structural interfaces, no generics until 1.18. Generics arrived but remain conservative. No union types, no pattern matching, no modifier algebra. The simplicity is a feature for many; a limitation for others.
// No union types: must use interface{}
func reduce[T, U any](s []T, f func(U,T)U, init U) U
// Any struct with matching fields satisfies interface// Union type:
d Shape = Circle | Square | Triangle
// Generics on classes (v1); functions use interfaces:
d Reducible!
f fold(acc: Reducible, item: Reducible) -> Reducible(T, error) return pairs are typed but silently ignorable — _ discards them with no warning by default. if err != nil is everywhere. No exhaustiveness, no policy arrays, no happy-path separation.
data, err := fetch(url)
if err != nil { return err }
result, err := process(data)
if err != nil { return err }f run(url: S) -> Data+A
e.x([retry(3), log])
data = fetch(url) // clean
result = process(data) // clean
r => resultNo macros. Code generation via go generate runs external tools — separate from the compiler. Reflection at runtime. No compile-time execution. The deliberate simplicity means the language is easy to tool but limited in expressiveness.
//go:generate stringer -type=Direction // Runs external binary — not the compiler // runtime reflection: reflect.TypeOf(x)
// c f __load: adds to_string to a type
c f __load()
Direction.to_string = v => v.name
// compile-time, same language, no external toolGo's html/template package auto-escapes HTML — a good runtime defense. SQL injection requires parameterised queries by discipline. No compile-time enforcement of either.
// html/template: runtime auto-escape
// SQL: discipline only
db.Query("SELECT * FROM u WHERE id=" + id)
// injection if id comes from user inputsql`SELECT * FROM u WHERE id = {id: I}`()
// I cannot carry string — structurally impossible
// Compile-time schema: column types checkedGo's ergonomics are deliberately minimal — small keyword set, fast compile, simple toolchain. gofmt standardises formatting. The simplicity cuts both ways: approachable on day one, occasionally limiting later.
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
ch <- process(i)
}(item)
}
wg.Wait(); close(ch)results = items.map(i => a process(i)) // No WaitGroup. No channel. No defer.
+AModern, safe, expressive. Every Swift annotation is a workaround for a model not built around a unified algebra. U has the algebra.
ARC — same mechanism as U, but U's is biased (non-atomic on the owning thread, atomic only on cross-thread access). Swift's ARC is always atomic for class reference counts, adding overhead on every retain/release even in single-threaded code. Swift structs are value types and avoid ARC; U's -R stack values are the equivalent.
class Node { var next: Node? } // ARC: always atomic
// Every retain/release: atomic op — even on one thread
struct Point { var x: Double } // value type: no ARC// -R: stack — zero ARC pt: Point = Point(x: 1.0) // +R: biased ARC — non-atomic on owning thread cfg: Config+R = Config() // atomic only when stolen across fibers
Swift's Optional<T> is excellent and enforced. Syntax sugar (T?, ??, if let, guard let) makes it ergonomic. U's T+N is slightly more compact; both make non-null the type-safe path.
guard let user = getUser(id) else { return }
user.name // unwrapped
// or: if let u = getUser(id) { ... }user: User+N = get_user(id) user ?? r => none // must handle user.name // ✓ narrowed
Swift structured concurrency (async/await, actor, withTaskGroup) is well-designed but built from separate features. Each is a workaround for a problem the cactus stack dissolves: @escaping (closures that outlive scope), [weak self] (retain cycles), Sendable (values crossing actor boundaries), withTaskGroup (fan-out lifecycle). U's cactus handles all of these structurally.
Swift has protocols, generics, associated types, and existentials. some Protocol and any Protocol are distinct. Opaque types (some View) enable SwiftUI's DSL. No modifier algebra — mutability is a declaration-level keyword (var/let), not a composable modifier.
func make() -> some View { Text('hi') } // opaque
var views: [any View] = [...] // existential
// Distinct semantics, two keywords for same conceptd View!
f draw(ctx: Context) // interface — any impl
f make() -> View // concrete — static dispatch
f make() -> View! // interface — dynamic dispatch
// ! suffix = interface type. One axis, not two keywords.Swift's typed throws (SE-0413) finally arrived in Swift 6 — functions declare which error types they throw. Until then, throws was untyped. try at every call site. U's ! ErrorType in the signature with policy arrays achieves the same with zero happy-path noise.
// Swift 6: typed throws func fetch() throws(NetworkError) -> Data // try adds noise at every call site: let d = try fetch() let p = try parse(d)
f run() -> Data ! NetworkError
e.x([retry(3), log]) // declare once
data = fetch() // no try noise
result = parse(data)
r => resultSwift Macros (SE-0382) arrived in Swift 5.9 — compile-time code generation via typed AST transformation. More principled than Rust proc-macros. Still a separate system from the type system. U's c f is the same language in a different domain, not a separate AST manipulation API.
@attached(member)
macro AddID() = #externalMacro(
module: 'MyMacros', type: 'AddIDMacro')
// Separate crate, Syntax library API// c f __load: adds id field to User
c f __load()
User.generate_id = () => UUID.new()
// same language — no external moduleNo built-in SQL or HTML injection safety. String interpolation is typed but unguarded for injection contexts. Libraries provide safety by convention.
let q = "SELECT * FROM u WHERE id=\(id)" db.execute(q) // injection if id is tainted // No compiler warning — compiles and ships
sql`SELECT * FROM u WHERE id = {id: I}`()
// I slot: parameterised at compile time
// Injection structurally impossibleSwift's syntax is genuinely pleasant — trailing closures, property wrappers, result builders (SwiftUI). The accumulation of features (6 ways to handle optionals, 3 ways to declare protocols) adds cognitive load. U deliberately keeps one obvious way.
func load(@escaping done: (Data)->Void) {}
btn.action = { [weak self] in self?.tap() }
Task { @MainActor in await refresh() }
struct Cfg: Sendable {}f load(done: F(Data)->none) {}
btn.action = () => tap()
refresh() // cactus handles isolation
// -R: Sendable by construction@escaping, [weak self], SendableJava done right — null safety, coroutines, concise syntax. Closest in return-type annotation style (uses : T, which U moved away from).
JVM GC — well-tuned but pauses exist. No stack allocation as a concept. JVM startup cost. Native targets (Kotlin/Native) have their own memory model with ARC, but different semantics per target platform.
val x = 42 // boxed Int on heap (JVM) val list = listOf(1,2,3) // ArrayList of boxed ints // Kotlin/Native: ARC, different per target
x: I = 42 // 8 bytes, stack list: [I] = [1,2,3] // contiguous, stack, unboxed x_heap: I+R = 42 // explicit heap opt-in
Kotlin's nullable types (T?) are excellent and enforced. The Elvis operator (?:), safe call (?.), and !! for asserting non-null are clean. U's T+N and ?? are the same design with slightly different symbols.
// Java interop: platform type String! is nullable val name: String = javaLib.getName() // unsafe! name!!.length // KotlinNullPointerException
name: S = java_lib.get_name() ?? "" // +N must be handled — no !! escape // Compiler enforces at every use site
Kotlin coroutines with structured concurrency are the most principled async model before U. Scopes prevent goroutine leaks; launch/async manage fan-out. But suspend is a color: a suspend fun can only be called from another suspend fun or a coroutine builder. Making any function async requires rewriting every caller. Dispatchers (Dispatchers.IO vs Dispatchers.Main) must be explicitly chosen and threaded.
suspend fun foo(cfg: Config, n: Int): String { ... }
// bar must become suspend (infected)
suspend fun bar() {
val cfg = loadConfig() // saved in continuation
val n = 42
val r = foo(cfg, n) // suspend point
}
// caller needs coroutine builder + dispatcher
runBlocking(Dispatchers.IO) { bar() }a f foo(cfg: Config, n: I) -> S: ...
f bar() // NOT suspend — not infected
cfg = load_config() // on bar's stack segment
n = 42
r = a foo(cfg, n) // foo suspends → cactus branch
// bar's segment alive — cfg, n reachable via ptr
f main()
bar() // plain call — works anywhereThe cactus stack. When a foo() suspends, U creates a new branch. The caller's stack segment stays alive — its local variables reachable via a parent pointer held by the child fiber. When the child completes, the parent resumes. No heap closure. No serialisation. No infection. Writing a foo() where foo is synchronous is a zero-cost no-op.
// bar() is NOT marked async f bar() cfg = load_config() // on bar's stack segment n = 42 result = a foo(cfg, n) // foo suspends: // ┌ bar segment alive ──────────┐ refcount = 1 // │ cfg, n │ // └─────────────────────────────┘ // └── foo branch ← waiting, holds parent ptr // foo completes → refcount-- → bar resumes // cfg and n were NEVER copied or closed over
Kotlin has sealed classes, data classes, value classes, type aliases, extension functions. No modifier algebra — val/var is declaration-level. Generics with variance annotations (in/out) are powerful. Interop with Java means nullable escapes via platform types.
val x = 42 // immutable binding only var y = 42 // mutable — not in the type // Concurrent mutation: requires external sync
x: I = 42 // -M: immutable (default) y: I+M = 42 // +M: mutable — IN the type state: Config+R+M(MVCC) = Config() // mutation policy composable with type
Kotlin inherited Java's unchecked exception model — anything can throw, nothing is declared. runCatching and Result<T> are library-level alternatives. No built-in typed error handling like U's ! ErrorType.
// No checked exceptions — anything can throw
try {
val r = fetchData(url)
} catch (e: IOException) {
retry()
} // exhaustiveness? no.f run(url: S) -> Data ! NetworkError
e.x([retry(3), log])
data = fetch(url) // compiler: covered
r => dataKotlin Symbol Processing (KSP) for annotation processing. No compile-time execution in the language itself. Inline functions with reified type parameters preserve generics at runtime. @Serializable via KSP generates code externally.
@Serializable data class User(val name: String) // KSP generates serialiser externally // Separate phase, different API (ksymbol)
// c f: same language — no separate KSP phase
c f __load()
User.to_json = c build_serialiser(User, "json")
User.from_json = c build_deserialiser(User, "json")
// integrated: same types, same error messagesString templates ("Hello $name") are convenient but unguarded for injection. SQL injection requires discipline or ORMs. No compile-time injection safety.
"Hello $name" // readable, but:
db.query("SELECT * FROM u WHERE id=$id")
// SQL injection: compiles, shipssql`SELECT * FROM u WHERE id = {id: I}`()
// typed slot: never string-interpolated
// compile-time schema check includedKotlin's ergonomics are genuinely excellent — concise, readable, pragmatic. Extension functions and lambdas make DSLs natural. The JVM heritage means familiar tooling. U's : T for return types (old style) vs Kotlin's : T is the same; U switched to -> T to align with Python/Rust.
GlobalScope.launch(Dispatchers.IO) {
val data = withContext(Dispatchers.Default) {
fetchData()
}
process(data)
}f run()
data = a fetch_data() // cactus handles
process(data) // no dispatcherThe enterprise standard. Verbose where U is terse; runtime where U is compile-time. The checked exception idea was right — the execution was wrong.
JVM GC is mature and well-tuned (G1, ZGC, Shenandoah) but pauses exist. Project Valhalla (value types) moves toward stack allocation for primitives — arriving decades after the need was clear. No ownership model.
Result r = new Result(); // heap always Listlist = new ArrayList<>(); // boxed // Stack: primitives only. Project Valhalla: someday.
r: Result = Result() // stack by default r_heap: Result+R = Result() // explicit heap list: [I] = [1,2,3] // unboxed, contiguous
NullPointerException is Java's most famous bug. Optional<T> was added in Java 8 but adoption is inconsistent. @Nullable and @NonNull annotations exist but are tool-dependent. No language-level enforcement.
User user = getUser(id); // null if missing System.out.println(user.getName()); // NPE // Optional: Java 8, inconsistent adoption
user: User+N = get_user(id) user.name // ✗ compile error user ?? r => none // handle user.name // ✓ safe
Project Loom (Java 21) finally brings goroutine-style ergonomics — virtual threads can block freely, no coloring, any method can block without annotation. The right answer, 15 years after Go. Virtual threads save the entire call stack per suspension — no frame minimisation equivalent to U's cactus branches. Earlier Java async approaches (CompletableFuture, reactive streams like RxJava, Spring WebFlux) introduced significant complexity and did have coloring.
// Pre-Loom: coloring + callback hell
CompletableFuture.supplyAsync(() -> loadConfig())
.thenApplyAsync(cfg ->
foo(cfg, 42)) // infected chain
.thenAcceptAsync(result ->
store(result)) // buried happy path
.exceptionally(e -> {
log(e); return null; // error interleaved
});f bar()
cfg = load_config() // on bar's segment
result = a foo(cfg, 42) // foo suspends:
// bar's segment: alive via refcount
// cfg: parent ptr from foo branch
// NOT a full 2MB thread stack
store(result)The cactus stack. When a foo() suspends, U creates a new branch. The caller's stack segment stays alive — its locals reachable via a parent pointer held by the child fiber. When the child completes, the parent resumes. No heap closure. No serialisation. No infection. a foo() where foo is sync is a zero-cost no-op.
f bar() cfg = load_config() // on bar's stack segment n = 42 result = a foo(cfg, n) // foo suspends → cactus branch // bar segment: alive, refcount=1 // cfg, n: NOT copied — parent ptr from foo branch // foo completes → refcount-- → bar resumes
Java's type system is improving (records, sealed classes, pattern matching in switch) but carries 30 years of legacy. Generics are erased at runtime. No union types without sealed hierarchies. Verbosity is the historical complaint — Map<String, List<Integer>> on both sides of an assignment.
Map>> cache; // Erased: no List .class at runtime // instanceof check required for narrowing
{S: [User+N]} // map of nullable-user lists
// Types not erased — full runtime info
// Pattern matching: native to type systemJava's checked exceptions were the right idea — declared, typed, exhaustive. The execution failed: listing every subtype at every frame led to throws Exception everywhere. U inherits the right idea, fixes the execution: ! NetworkError covers all subtypes, policy arrays replace try/catch at every call site.
void fetch() throws NetworkException,
TimeoutException, SSLException {
// add one → rewrite every caller
}
// Result: 'throws Exception' everywheref run(url: S) -> Data ! NetworkError
e.x([retry(3), log]) // covers all subtypes
data = fetch(url)
r => dataJava annotation processing (APT) generates code from annotations. Spring and Lombok use it extensively. Reflection is powerful but runtime and slow. No compile-time execution in the language. Records and sealed classes reduce the need for boilerplate but don't eliminate it.
@Getter @Setter // Lombok: bytecode manipulation @Entity // Hibernate: runtime classpath scan @Component // Spring: runtime proxy // All runtime — startup cost, no compile check
// c f: compile-time, same language
c f __load()
User.get_name = () => t.name // add getter
User.get_age = () => t.age // add getter
User.to_json = c build_serialiser(User)
// zero startup cost — no classpath scanSQL injection has plagued Java applications for decades. PreparedStatement is the correct tool but discipline-dependent. No language-level enforcement. JEP 430 (String Templates, preview in Java 21) begins addressing this — inspired by the same insight as U's template tags.
String q = "SELECT * FROM u WHERE id=" + id; stmt.execute(q); // id from user → leak // JEP 430 (Java 21 preview) finally addresses this
sql`SELECT * FROM u WHERE id = {id: I}`()
// Parameterised by construction — not concatenatedJava has become significantly more ergonomic since Java 8 (lambdas, streams, var, records, switch expressions). Still verbose by modern standards. Boilerplate is the word most associated with Java. U's single-letter keywords (f, d, r) are an opposite extreme.
public static void main(String[] args) {
record User(String name, int age) {}
sealed interface Shape permits Circle, Square {}
}d User { name: S; age: I }
d Shape = Circle | Square
f main()
// no boilerplateThe most expressive. Features accumulated, not compressed. Every U mechanism replaces a C++ mechanism that accreted over decades.
Manual memory with RAII destructors. unique_ptr, shared_ptr, weak_ptr add safety. shared_ptr is always atomic reference counting — no biased variant. Stack allocation is standard. The danger: use-after-free, double-free, buffer overflows are all undefined behaviour and compile fine. U's modifier algebra expresses the same ownership decisions with compile-time enforcement.
auto s = std::make_shared(); // atomic ARC auto s2 = s; // atomic increment — even single-thread // UB: use-after-free compiles without warning
cfg: Config+R = Config() // biased ARC cfg2 = cfg // non-atomic on owning thread // atomic only when stolen across fibers // No use-after-free possible
nullptr can be assigned to any pointer. Dereferencing it is undefined behaviour — sometimes a crash, sometimes silent corruption. std::optional<T> (C++17) provides a safe nullable type but adoption is inconsistent. No language-level enforcement.
Config* cfg = nullptr;
cfg->update(); // UB — compiler optimises assuming this
// doesn't happen → security holecfg: Config+N+R = load() cfg ?? r => none // must handle cfg.update() // ✓ safe — narrowed
C++20 coroutines are stackless and low-level — you implement the promise type yourself. std::async is higher-level but limited. No structured concurrency. std::thread for true parallelism. The primitives are powerful but composing them is manual.
C++ has the most expressive type system of any mainstream language — templates, concepts (C++20), constexpr, SFINAE, variadic templates. The power is real; the complexity is legendary. Thousand-line error messages from template failures. U's c f replaces C++ TMP with the same language in a different domain — typed, author-error-messages, no SFINAE.
template>> void process(T v); // Wrong type → 300-line instantiation trace // Author cannot control the error message
d Numeric!
f scale(factor: N) -> Numeric
// Generic functions use interface polymorphism:
f process(v: Numeric) -> Numeric
r => v.scale(2.0)
// wrong type → 'S does not implement Numeric'SIMD, GPU, exact arithmetic, autodiff. C++ needs compiler-specific intrinsics (or #pragma omp simd) for vectorization, CUDA for GPU (a separate language and compilation pipeline), and external libraries like Eigen for linear algebra. There is no standard exact arithmetic type and no standard autodiff. In U the modifier system handles all three, and autodiff ships in the standard library.
// SIMD: manual intrinsics or pragma
#include <immintrin.h>
__m256 a = _mm256_load_ps(data_a); // AVX2 — x86 only
__m256 c = _mm256_add_ps(a, a);
// GPU: CUDA — separate .cu file, separate compiler
__global__ void add(float* a, float* c, int n) {
int i = blockIdx.x*blockDim.x + threadIdx.x;
if (i < n) c[i] = a[i] + a[i];
}
// cudaMalloc / cudaMemcpy / kernel<<<>>> / cudaFree...
// Exact arithmetic: no standard type
// __int128, boost::multiprecision, or GMP
// Autodiff: no standard — hand-roll or use library// +V: SIMD — compiler picks ISA, portable a: [N32]+V = load_data() c = a + a // SIMD, ISA-agnostic // +R(GPU): same expression, GPU dispatch a_gpu: [N32]+R(GPU) = a.c(+R(GPU)) // copy to GPU c_gpu = a_gpu + a_gpu // GPU kernel — no .cu file // 2D matrix on GPU: same API as CPU w: [[N32]]+R(GPU) = load_weights() // matrix, on GPU out = w @ x_gpu // matmul dispatched to GPU // Q: exact rational — standard type cost: Q = Q(355, 113) // π approximation, exact error = cost - Math.PI // exact symbolic difference // Autodiff: standard library loss = model(x) - target Diff.grad(loss, model.weights) // exact gradient
add<<<blocks, threads>>>(a, c, n); U programmers write c = a + a on a +R(GPU) array. The modifier IS the target selection.C++ exceptions are untyped and have non-zero overhead even when not thrown (DWARF tables). noexcept helps but is advisory. Error codes as alternatives have no ergonomic composability. No declared error surface, no exhaustiveness.
void fetch(string url); // can throw anything // noexcept: advisory — not enforced // DWARF tables: overhead even on happy path
f fetch(url: S) -> Data ! NetworkError // declared, inherited, exhaustive // zero overhead on non-error path
C++ template metaprogramming is Turing-complete but accidental — discovered via substitution failure rules, not designed. Concepts (C++20) improve the experience. consteval and constexpr add compile-time execution. Still a separate system from the runtime language. U's c f is the same language.
// SFINAE: substitution failure, not an error template>> void f(T v); // Concepts (C++20) improve, don't fix
// c f wraps any function at compile time:
c f double_it(func: F(N)->N) -> F(N)->N
r => x => func(x) * 2.0
process_double = c double_it(process)
// author controls wrapping + error messageNo injection safety. String formatting is untyped (printf) or type-safe but cumbersome (std::format). SQL, HTML, shell injection are all discipline problems. No type system enforcement of any kind.
printf("%s scored %d", score, name);
// args swapped: UB, security hole
cursor.exec("SELECT * FROM u WHERE id=" + id);sql`SELECT * FROM u WHERE id = {id: I}`()
print("{score} scored {name: S}")
// Typed slots — wrong type = compile errorC++ is infamous for its complexity — multiple ways to initialise, declare, cast, manage memory. Modern C++ (C++20, C++23) has improved significantly. The language accreted features rather than compressing them. U's design philosophy is the opposite: compress, don't accumulate.
int x{42}; int x=42; int x(42); auto x=42;
// 4 ways — different semantics in edge cases
// UB: signed overflow, strict aliasing, morex: I = 42 // one way, always // No UB by construction // Modifiers declare intent explicitly
c f vs C++ TMP — same languageClosest in philosophy — simple mechanisms, no hidden control flow. Zig makes allocation explicit; U encodes it in the modifier. Both reject magic.
Zig makes allocation explicit via allocator parameters — every function that allocates takes an Allocator argument. U encodes the same information in the modifier: -R = stack (no allocator), +R = heap (runtime allocator). Same visibility, different mechanism. Zig's allocator chains propagate through call stacks; U's +R annotation propagates through types.
// Allocator chains everywhere fn process(ally: Allocator, data: []T) ![]T { const result = try ally.alloc(T, data.len); defer ally.free(result); // allocator must reach every site }
// Modifier encodes allocation intent f process(data: [T]) -> [T] // -R: stack f process(data: [T]) -> [T]+R // +R: heap // No allocator param to thread through
Zig's ?T optional is enforced. orelse and if (opt) |val| handle absence. Similar to U's T+N and ??. Both make non-null values explicit at the type level.
const user: ?User = getUser(id); const name = user.?.name; // panics if null // or: if (user) |u| u.name else "default"
user: User+N = get_user(id) user ?? r => none // must handle user.name // ✓ safe
Zig's async system has been under significant redesign — the original stackless coroutine model was deprecated in 2023. Coloring applies: async fn returns a frame type distinct from regular functions. No structured concurrency. No cactus stack equivalent — parent frames are not retained when a child fiber suspends; captured variables must be explicitly managed. This means U's cactus-based "plain function calling async function" ergonomic is not achievable in Zig's model.
// async fn has different type than fn
fn foo(cfg: *Config, n: u32) void { ... }
// Zig async: different story
// Frame must be kept alive by caller
// No cactus — parent not retained automaticallya f foo(cfg: Config, n: I) -> S: ...
f bar() // NOT async
cfg = load_config()
n = 42
result = a foo(cfg, n)
// foo suspends → cactus branch
// bar's segment alive automatically
// cfg, n: parent pointer — not copiedZig's comptime is powerful — types are first-class values, generic functions are just functions with comptime parameters. No trait system or algebraic types. Error unions (!T) are built-in. The simplicity is genuine — one consistent model for compile-time and runtime computation.
fn Vec(comptime T: type, n: comptime_int) type {
return struct { data: [n]T };
}
const V3f32 = Vec(f32, 3); // works!// Generic class — ElemType: multi-letter rule
d Vec[ElemType]
data: [ElemType]
// Fixed-size: -M(n) on the array, not a type parameter
d Vec3f32
data: [N32]-M(3) // 3-element, stack, compile-time fixed
V3 = Vec3f32(data: [1.0, 2.0, 3.0])Zig's error unions (!T or MyError!T) with try for propagation are clean and typed. Errors are inferred from the function body — no declaration required. U requires declaring ! ErrorType in the signature; Zig infers it. Both achieve typed exhaustive error handling.
fn fetch(url: []const u8) !Data {
const conn = try connect(url);
return try conn.read(); // set inferred
}f fetch(url: S) -> Data ! NetworkError // declared explicitly: contract is clear e.x([retry(3)]) data = connect(url).read()
Zig's comptime is the closest existing analogue to U's c f — same language at compile time, types as values, compile-time execution is normal code. The key difference: Zig's comptime has no capability system, no modifier algebra, no template tag hooks. It is a general-purpose compile-time computation mechanism without the adapter pattern.
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
// No capability gating. No template tag hooks.// Generic function [T] — same as make_getter[T] pattern:
f max[T](a: T, b: T) -> T
r => a > b ? a : b
// c f adds capability system on top:
// __check, __validate hooks for template tags
// adapters — not part of Zig comptimeNo injection safety. String formatting is manual. SQL injection requires discipline. No template tag system.
const q = try std.fmt.allocPrint(
ally,
"SELECT * FROM u WHERE id={d}", .{id}
);
// No compile-time schema checksql`SELECT * FROM u WHERE id = {id: I}`()
// __schema() at compile time: columns checked
// Type mismatch = build errorZig's philosophy — no hidden control flow, explicit everything — is shared with U. Both are opinionated about what the language won't do for you. Zig is more minimal in syntax; U has more built-in (modifier algebra, template tags, cactus stack). Both reject magic.
// No hidden control flow — by design defer ally.free(buf); // explicit cleanup // comptime: same language — elegant // No OOP, no exceptions, no GC, no async magic
// Same philosophy: explicit, no hidden flow // c f: same language — same elegance // -R -M -N: explicit defaults, zero annotation // Cactus: explicit model, no hidden GC/async
c fMicrosoft's answer to Java — more expressive, more modern. LINQ shows the right instinct for typed DSLs. U generalises it.
.NET GC with Span<T>, ref structs, and stackalloc for stack allocation. C# 7+ made stack allocation ergonomic for hot paths. No ownership model — refs can escape freely. Span<T> enforces stack confinement at the type level — the closest C# comes to -R.
var r = new Result(); // heap, GC // Span: stack-only (C# 7.2+, opt-in) Span s = stackalloc int[8]; // Span enforced not to escape by type system
r: Result = Result() // stack (default, -R) r_heap: Result+R = Result() // explicit heap // Modifier is the declaration — no stackalloc
Nullable reference types (C# 8+) add string? vs string — opt-in per project. Non-null is the default with the pragma enabled. Better than Java, opt-in like TypeScript's strict mode. U's -N default is always-on.
// Requires #nullable enable string? name = GetName(); // nullable name!.Length // ✓ compiles — unsafe bypass // Can always suppress the check
name: S+N = get_name() name.length // ✗ compile error name ?? r => none // must handle name.length // ✓ safe — no ! bypass
C# invented async/await in 2012 — every other language copied it, coloring problem included. async infects every caller. ConfigureAwait(false) is required in library code to avoid deadlocks on synchronisation contexts — forgetting it is a famous cause of production ASP.NET deadlocks. Task vs ValueTask tradeoffs add further cognitive load.
async Task<string> Foo(Config cfg, int n) { ... }
// bar must become async (infected)
async Task Bar() {
var cfg = LoadConfig(); // must survive await
var n = 42;
// cfg, n serialised into heap Task state machine
var r = await Foo(cfg, n).ConfigureAwait(false);
// ↑ forget this in library code → deadlock
}
// main must be async (infected)
async Task Main() { await Bar(); }a f foo(cfg: Config, n: I) -> S: ...
f bar() // NOT async
cfg = load_config() // on bar's stack segment
n = 42
r = a foo(cfg, n) // foo suspends → cactus branch
// bar's segment: alive — cfg, n reachable
// No ConfigureAwait. No deadlock scenario.
f main()
bar() // plain — not infectedThe cactus stack. When a foo() suspends, U creates a new branch. The caller's stack segment stays alive — its locals reachable via a parent pointer held by the child fiber. When the child completes, the parent resumes. No heap closure. No serialisation. No infection. a foo() where foo is sync is a zero-cost no-op.
f bar() cfg = load_config() // on bar's stack segment n = 42 result = a foo(cfg, n) // foo suspends → cactus branch // bar segment: alive, refcount=1 // cfg, n: NOT copied — parent ptr from foo branch // foo completes → refcount-- → bar resumes
C# has records, pattern matching, discriminated unions (via OneOf library), generics, extension methods. LINQ demonstrates typed query composition — the same insight as U's sql\`\` but as a library, not a language feature. No modifier algebra.
readonly record struct Pt(double X, double Y); // readonly: can't reassign fields // But: nested mutable objects still mutable // No ownership, no mutation policy
pt: Point = Point(x:1.0, y:2.0) // -M: immutable pt_m: Point+M = Point(x:1.0, y:2.0) // +M: mutable // Mutation policy is a composable modifier
Untyped exceptions inherited from .NET. Result<T, E> patterns via libraries. No language-level typed error surfaces. The try/catch ceremony is everywhere in production C# code.
try {
var data = await FetchAsync(url);
Process(data);
} catch (HttpRequestException e) {
Retry();
} catch (Exception) { throw; }a f run(url: S) -> none
e.x([retry(3), log])
data = a fetch(url) // happy path
process(data)C# Source Generators (Roslyn) are powerful — typed AST transformation at compile time. More principled than Java APT. Roslyn's API is rich. Still a separate system from the type system. C# attributes are runtime by default.
[Generator]
public class MyGen : IIncrementalGenerator {
public void Initialize(
IncrementalGeneratorInitializationContext ctx) {...}
} // separate project, Roslyn syntax API// c f: same language — no separate Roslyn project
c f __load()
User.to_json = c build_serialiser(User, "json")
User.from_json = c build_deserialiser(User, "json")
// full User type available — not a token streamLINQ to SQL was the right instinct — typed queries that compile against a schema. Entity Framework extends it. Interpolated strings ($"...") are type-safe for formatting but not for injection contexts. No compile-time injection safety equivalent to U's template tags.
// LINQ: typed ✓
var names=users.Where(u=>u.Age>18).Select(u=>u.Name);
// Raw SQL: still user discipline:
db.Execute($"SELECT * FROM u WHERE id={id}");// Template tags: typed for every target
sql`SELECT name FROM u WHERE age > {min: I}`()
graphql`query { users(minAge:{min:I}) { name } }`C# has improved dramatically — records, top-level statements, pattern matching, primary constructors. LINQ and extension methods make it expressive. The ecosystem (NuGet, .NET) is excellent. Still carries C-style ceremony in places where U uses single-letter keywords.
async TaskFetchAsync(string url){...} async Task RunAsync() { // Forget ConfigureAwait(false) in lib → deadlock var r=await FetchAsync(url).ConfigureAwait(false); }
a f fetch(url: S) -> S // async — no keyword infection
f run()
result = a fetch(url) // no ConfigureAwait ever
// one scheduler — deadlock impossible