Features

How each language does it

A near-exhaustive matrix. Toggle languages to focus; each cell explains how the feature works there.

Columns: GujiGoOCamlHaskellPerlRakuRustPython

Concurrency & Parallelism

FeatureGujiGoOCamlHaskellPerlRakuRustPython
OS threadsSpawning kernel-scheduled threads that the OS maps 1:1 to hardware and can run in parallel across cores. NoGuji exposes no 1:1 OS-thread API. Concurrency is expressed only with hatch tasks over an M:N scheduler that grows its own thread pool internally (spec §17.6); raw OS threads are not a language primitive. PartialGo never exposes raw OS threads directly; goroutines are multiplexed M:N onto a pool of OS threads sized by GOMAXPROCS. You can pin work with runtime.LockOSThread, but there is no 1:1 thread-spawn API. YesSince OCaml 5.0 the Domain module spawns domains, each backed by its own OS thread and running truly in parallel. The legacy Thread module also gives concurrent (now parallel) systhreads. PartialHaskell programs almost never use raw 1:1 OS threads; GHC's forkIO creates lightweight green threads multiplexed M:N over a small pool of OS threads (capabilities, set by -N/+RTS). forkOS does create a dedicated bound OS thread, mainly needed for thread-local foreign (C) state. Partialuse threads provides interpreter (ithreads) backed by real OS threads, but each thread clones the entire interpreter, so they are heavy and have no shared memory by default. Their use is widely discouraged for new code. PartialRaku runs on a real OS-thread pool (the ThreadPoolScheduler) but discourages manual Thread.start; idiomatic code uses high-level Promise/Supply which the scheduler maps onto threads. Yesstd::thread::spawn creates a 1:1 native OS thread; the closure must be Send and 'static. Joining via the returned JoinHandle propagates the result or panic. Yesthreading.Thread creates real OS threads, but in the default build the GIL serializes bytecode so only one runs Python at a time (still useful for I/O). The free-threaded build (3.13 experimental, 3.14 supported) can run them in parallel.
Green threads / lightweight tasksUser-space tasks multiplexed M:N onto a few OS threads, cheap to create in large numbers. Yeshatch { ... } starts a lightweight task that runs the block concurrently and returns immediately; an M:N scheduler multiplexes many tasks onto a few OS threads, so spawning is cheap. This is verified working in both the interpreter and the native compiler. YesGoroutines are the canonical green threads: go f() starts one with a tiny growable stack, and the runtime schedules thousands of them M:N over OS threads. This is the headline feature of the language. Via libraryOCaml 5 has no built-in green threads, but effect handlers let libraries build them in direct style. Eio and Domainslib's task pool provide cooperative fibers scheduled cooperatively within and across domains. YesGreen threads are the default model: forkIO spawns an extremely cheap user-space thread (initial stack a few hundred bytes), and GHC's runtime schedules millions of them M:N over OS capabilities. This is a headline strength of GHC concurrency. Via libraryCore Perl has no green threads. Coro provides real cooperative coroutines that share data in one interpreter, and event frameworks like IO::Async give cooperative concurrency on a single thread. PartialRaku does not expose explicit green threads; start/Promise schedule code onto a managed thread pool rather than spawning a thread per task, giving similar lightweight-task economics at a higher level. Via libraryThe standard library only has 1:1 OS threads. Async runtimes such as tokio and async-std provide green-thread-like tasks (tokio::spawn) multiplexed M:N over a worker-thread pool. Partialasyncio tasks are cooperative green-thread-like coroutines on one thread, but they are not preemptive and do not parallelize CPU work. True M:N preemptive lightweight threads are not part of CPython.
async / awaitLanguage-level syntax for writing non-blocking, suspendable code in a sequential style. NoGuji has no async/await keywords and no future type; it follows Go's model where ordinary blocking code runs inside hatch tasks and the scheduler suspends at send/recv/select points (spec §17). 'One obvious way' favors tasks-and-channels over coloured functions. NoGo has no async/await by design; goroutines plus channels make every function effectively non-blocking from the caller's view, so functions are not 'coloured'. Blocking calls simply yield the goroutine to the scheduler. Via libraryOCaml 5 has no async/await keywords; instead effect handlers let Eio and Lwt/Async offer direct-style or monadic concurrency. Domainslib.Task exposes async/await as ordinary functions for parallel task pools. NoHaskell has no async/await keywords and no function coloring: GHC's lightweight threads make ordinary blocking IO cheap, so you just forkIO and let the runtime suspend a thread at a blocking call. The async library packages this into Async handles, but that is a library, not syntax. Via libraryCore Perl has no async syntax. The Future::AsyncAwait module adds real async/await keywords layered on IO::Async/Future, giving non-blocking code in a sequential style on a single thread. Yesawait is a built-in that blocks the current scheduler slot until a Promise (or list of them) completes, and start produces a Promise. Together they form Raku's first-class async model. Yesasync fn/async {} produce Futures and .await suspends them; futures are lazy and do nothing until polled by an executor. Rust ships the syntax and Future trait but no runtime, so you add tokio or async-std. Yesasync def defines coroutines and await suspends them, driven by an event loop (asyncio in the stdlib). It is cooperative and single-threaded, ideal for high-concurrency I/O.
Channels / CSPTyped conduits that pass values between concurrent tasks, the basis of Communicating Sequential Processes. YesChannels are real and verified: channel() builds an unbuffered Chan[T] (and channel(n) a buffered one), $ch.send($v) and $ch.recv(): Option[T] move immutable values between tasks, $ch.close() closes it, and for $x in $ch drains until closed. Immutable-only payloads mean no data races. YesChannels are a core type: make(chan T) (unbuffered) or make(chan T, n) (buffered), with ch <- v to send and <-ch to receive. A closed channel yields the zero value with a false ok flag, and range drains it. Via libraryNo channels in the stdlib, but Domainslib.Chan gives multiple-producer/multiple-consumer bounded and unbounded channels across domains, and Eio.Stream offers fiber-level channels. YesThe Control.Concurrent.Chan type is an unbounded thread-safe FIFO with writeChan/readChan, and Control.Concurrent.STM.TChan/TBQueue give composable (optionally bounded) channels under STM. These are library types built on GHC's concurrency primitives rather than a language keyword. Via libraryThread::Queue provides a thread-safe FIFO queue that acts as a channel between ithreads (enqueue/dequeue). Outside ithreads, Coro::Channel serves coroutines. YesChannel is a built-in thread-safe queue with .send and .receive; closing it lets a for loop or react/whenever drain it. Supply adds a pub/sub streaming complement. Yesstd::sync::mpsc gives multi-producer/single-consumer channels (Sender/Receiver); tokio::sync::mpsc and crates like crossbeam add async and MPMC variants. Send moves ownership, enforcing data-race freedom. Yesqueue.Queue is a thread-safe channel for threads, multiprocessing.Queue/Pipe for processes, and asyncio.Queue for coroutines. These are library types rather than a CSP language primitive.
Selecting over multiple channelsWaiting on several channel operations at once and proceeding with whichever becomes ready first. Yesselect { ... } waits on several channel ops and runs the first ready arm, choosing arbitrarily among ready ones; an optional else makes it a non-blocking poll. A recv arm binds the Option[T] it yields. Verified against the compiler. Yesselect blocks until one of its channel cases can proceed, picking randomly among ready cases; a default clause makes it non-blocking. This is the primitive Guji's select is modeled on. Via libraryNo built-in select, but Eio exposes Fiber.first/any to race fibers, and Reagents composes channel operations with choice. Domainslib channels lack a native multi-way select. YesSoftware Transactional Memory makes multi-way waiting first-class: retry blocks a transaction until any read variable changes, and orElse composes alternatives, so racing several channels is just readTChan a orElse readTChan b. The async library's waitEither/waitAny cover racing tasks. Via libraryNo channel-select construct. Event loops (IO::Async, AnyEvent) let you await multiple async sources, and Future->wait_any/needs_any resolves on the first ready future. Yesreact { whenever $chan { ... } } reacts to whichever of many channels/supplies/promises emits first, the idiomatic multi-way wait. Promise.anyof also resolves on the first to complete. Via libraryThe standard library has no select. The tokio::select! macro awaits multiple async branches and runs the first ready one, and crossbeam's select! does the same for sync channels. Via libraryNo channel select for threads; asyncio.wait(..., return_when=FIRST_COMPLETED) or asyncio.as_completed selects the first ready awaitable, and selectors/select handle file descriptors.
Shared-memory synchronizationCoordinating concurrent access to mutable memory with locks, atomics, and other primitives. NoGuji has no mutexes, atomics, or shared mutable state across tasks: every value crossing a channel is immutable and hatch may capture only immutable bindings, so there is nothing to lock (spec §17). Tasks share data solely by communicating. YesAlthough channels are preferred, sync.Mutex/RWMutex, sync.WaitGroup, sync.Once, and the sync/atomic package guard shared memory. Goroutines share one address space, so this is fully available. YesDomains share the heap, so OCaml 5 provides Mutex, Condition, Semaphore, and the Atomic module for lock-free counters. The memory model gives well-defined behavior for data-race-free programs. YesShared mutable state uses MVar (a synchronizing box that is also a lock), IORef/atomicModifyIORef' for atomic updates, and especially Software Transactional Memory (TVar + atomically) for composable lock-free coordination. STM is a distinctive Haskell strength. PartialWith ithreads, variables are thread-local unless marked :shared via threads::shared, and lock/cond_wait coordinate them. Without ithreads there is no shared mutable memory between Perl threads. YesRaku threads share memory and offer Lock, Lock::Async, Semaphore, and atomic ops (atomic-inc/ operators), though high-level Promise/Supply are preferred over manual locking. YesShared state uses Arc<Mutex<T>> or RwLock, plus std::sync::atomic types; the Send/Sync traits and borrow checker make misuse a compile error. This is the safe alternative to message passing. Yesthreading provides Lock, RLock, Semaphore, Event, and Condition. Under the GIL these mainly prevent logical races; the free-threaded build makes them load-bearing for memory safety too.
Data-race safetyWhether the language statically or structurally prevents two tasks writing/reading the same memory unsynchronized. YesData races are structurally impossible: values are immutable by default, every value sent over a channel is immutable, and hatch closures capture only immutable bindings, so no task can observe another mutating shared data. There is no shared mutable state to race over. NoGo does not prevent data races: goroutines can freely share mutable memory and a missing lock is a silent bug. The go run -race detector finds many races at runtime, but the compiler gives no guarantee. PartialOCaml 5's memory model guarantees that data-race-free programs are sequentially consistent and that even racy programs stay memory-safe (no crashes/tearing of immediates), but it does not statically forbid races; correctness still needs discipline. PartialPurity and immutability mean the vast majority of code is data-race-free by construction, and STM gives composable atomicity without locks. But the type system does not statically forbid races on shared IORef/MVar in IO, and lazy thunks can even race on evaluation, so the guarantee is structural, not enforced like Rust's. Partialithreads avoid races by making data thread-local by default (no sharing unless explicitly :shared), so accidental cross-thread races are rare, but :shared data still needs manual locking. There is no compile-time race check. NoRaku shares mutable memory across threads and does not prevent races at compile time; correctness relies on using high-level Promise/Supply/Channel or explicit locks. Low-level shared mutation can race. YesRust statically prevents data races in safe code: the borrow checker plus the Send/Sync marker traits make any unsynchronized shared mutation a compile error. This is the core of 'fearless concurrency'. PartialIn the default GIL build, only one thread runs bytecode at a time so low-level memory corruption is avoided, but high-level logical races (e.g. read-modify-write) still occur. The free-threaded build removes the GIL, making true data races possible.
Actors & message passingIsolated units of state that communicate only by asynchronous messages, never by shared memory. PartialGuji has no actor type, but its tasks-plus-immutable-channels model is pure message passing: a hatch task owning a Chan[T] and looping for $msg in $ch is an actor in all but name, with state never shared. The motto is communicate, don't share. PartialGo has no actor type but encourages the actor-like pattern: a goroutine owning some state and serving requests over a channel. 'Share memory by communicating' is the canonical idiom. Via libraryNo built-in actors; libraries provide them. You can build an actor from a Domainslib.Chan mailbox plus a domain, and effect-based libraries compose message-passing fibers. Via libraryNo actor type in the core, but the model is easy to build: a forkIO thread looping over its own Chan/TQueue mailbox is an actor. Distributed-actor frameworks exist in distributed-process (Cloud Haskell), inspired by Erlang. Via libraryNo core actor model. CPAN offers actor frameworks and message-passing layers, and Thread::Queue between ithreads gives mailbox-style communication. PartialRaku has no actor keyword, but OO::Actor-style patterns are easy, and Supply/Channel plus react/whenever express message-driven isolated workers idiomatically. Via libraryNo actors in std, but actix, ractor, and kameo are mature actor frameworks; the common channel-per-task pattern (a task owning state behind an mpsc receiver) is also widely used. Via libraryNo built-in actors. Frameworks like Pykka, Thespian, Ray, and Dramatiq provide actor or task-message models, and multiprocessing plus queues gives isolated processes communicating by messages.

Functional programming

FeatureGujiGoOCamlHaskellPerlRakuRustPython
First-class functionsFunctions are ordinary values: they can be bound to names, passed as arguments, and returned from other functions. YesA sub is a value (§7.1): it can be passed, returned, and stored in a binding, and the uniform data-first call rule means any function is callable directly or method-style. Verified. YesFunctions are first-class values with a func type; they can be assigned, passed, and returned. Closures are supported via function literals. YesFunctions are first-class and curried by default; passing and returning them is the everyday idiom in OCaml. YesFunctions are ordinary first-class values - bound to names, passed, returned, and stored in data structures - and are curried by default. Higher-order functions like map, foldr, and (.) are the everyday idiom. YesSubroutines are first-class via code references (\&name or anonymous sub { ... }), called with $ref->(...). YesRoutine/Block objects are first-class values; subs and blocks are passed and returned freely, and &name references a named sub. YesFunctions can be passed as fn pointers or via the Fn/FnMut/FnOnce closure traits, typically behind generics or Box<dyn Fn>. YesFunctions are objects: they can be assigned, passed, returned, and stored in data structures with no special syntax.
ClosuresAnonymous functions that capture and retain bindings from their enclosing lexical scope. YesLambdas are anonymous subs (or { ... } topic blocks, §7.4) that capture surrounding bindings. Because captures are immutable by default, a closure observes a stable snapshot; capturing a mut binding is an error. YesFunction literals close over surrounding variables by reference, so captured variables stay live and can be mutated. YesEvery anonymous function (fun) closes over its lexical environment; captured immutable bindings are the norm. YesLambdas (\x -> ...) and local let/where definitions close over their lexical environment. Because captured bindings are immutable, a closure observes a stable snapshot of its environment. YesAnonymous sub { ... } blocks capture lexical my variables, forming proper closures that persist after the enclosing scope returns. YesBlocks and pointy blocks (-> $x { ... }) close over lexical variables, the basis of much of Raku's functional style. YesClosures capture environment by reference or by value (move); the compiler infers the least-restrictive Fn trait that fits. YesNested functions and lambda close over enclosing locals; rebinding a captured name requires nonlocal.
Immutability by defaultBindings (and ideally values) are immutable unless mutation is explicitly opted into. YesBindings are immutable by default (§4); reassignment is an error and mut is required to opt in. Methods that 'change' an object return a new instance, and every channel-crossed value is immutable. Verified: reassigning a non-mut binding is rejected. Partialconst exists only for compile-time scalar/string constants; variables, structs, slices, and maps are mutable, so immutability is not the default. Yeslet bindings and most data are immutable; mutation is explicit via ref, mutable record fields, or arrays. YesImmutability is total and the default: all bindings and data are immutable, and there is no assignment operator - let/where introduce names, never reassign them. Mutable cells (IORef, STRef, MVar, MArray) are explicit, effectful opt-ins. PartialVariables are mutable by default; immutability is opt-in via core use constant (scalars) or the Readonly/Const::Fast modules. PartialVariables are mutable, but value types (Int, Str, etc.) are immutable, and constant/sigilless bindings give immutable names; Map/List are immutable by default while Hash/Array are mutable. Yeslet bindings are immutable by default; mutation requires let mut, and the borrow checker enforces aliasing-vs-mutation at compile time. PartialNames are rebindable and most containers are mutable; immutability is available per-type (tuple, frozenset, str) or via @dataclass(frozen=True), not as a default.
Pattern matchingDestructuring and dispatching on the shape of values, binding sub-parts in the process. Yesmatch (§6.2, §10) is an expression with literal, binding, wildcard, variant, and nested patterns plus if guards. The compiler checks exhaustiveness and reports missing cases. Verified working on enum variants. PartialThere is no structural pattern matching; switch (including type switches) dispatches on values or dynamic types but cannot destructure or bind nested fields. Yesmatch ... with is a core construct over variants, tuples, records, and lists, with nested patterns, guards (when), and compiler-checked exhaustiveness. YesPattern matching is pervasive: function clauses, case ... of, and let/where destructure constructors, tuples, lists, and records, with nested patterns, | guards, and as-patterns. GHC checks exhaustiveness with warnings. PartialNo native structural matching; the experimental given/when is discouraged. Idiomatic dispatch uses if/regex or modules; Perl 5.38+ adds experimental class/field but not match. Yesgiven/when smart-matches against types, ranges, regexes, and values, and signature destructuring (-> (:$x, :$y)) plus where clauses cover structural cases. Yesmatch over enums, tuples, structs, and slices with nested patterns, bindings, ranges, and guards; the compiler enforces exhaustiveness. YesStructural pattern matching (match/case) landed in Python 3.10 (PEP 634) with sequence, mapping, class, and capture patterns plus guards; it is not exhaustiveness-checked.
Recursion and tail-call optimizationSupport for recursive functions and whether tail calls run in constant stack space (TCO). NoRecursion works, but there is no guaranteed tail-call optimization: each call consumes stack, so very deep tail recursion can overflow. Idiomatic iteration uses collection methods (§6.3); a guaranteed-tail-call form is not specified. NoRecursion is supported but the Go compiler does not perform tail-call optimization; deep recursion grows the (growable) goroutine stack. Loops are idiomatic. PartialTail calls in tail position run in constant stack as an implementation property; it is not a formal language guarantee, and non-tail recursion (e.g. naive list build) needs the opt-in [@tail_mod_cons] to stay tail-recursive. YesRecursion is the primary looping construct. GHC performs proper tail-call optimization, and laziness plus accumulator patterns let recursion run in constant stack; lazy thunk buildup is the usual pitfall, addressed with strict accumulators (foldl', BangPatterns). PartialOrdinary recursion grows the call stack with no automatic TCO; an explicit tail call can be written with goto &sub, and Sub::Recursive style helpers exist. PartialRecursion is supported; Rakudo does not guarantee general TCO, so deep recursion can overflow. Lazy sequences and iteration are preferred for unbounded work. NoRecursion is supported but tail-call elimination is not guaranteed by the language; LLVM may optimize some cases, so deep recursion can overflow. Loops/iterators are idiomatic. NoRecursion works but there is no TCO, and a recursion-depth limit (~1000) raises RecursionError; iteration is the idiomatic alternative.
Currying and partial applicationBuilding a new function by fixing some arguments of an existing one (partial application) or treating multi-arg functions as nested single-arg ones (currying). PartialNo dedicated currying or partial-application syntax ('one obvious way'). Partial application is expressed by returning a closure that captures the fixed arguments. PartialNo built-in currying or partial application; you write a closure that captures the bound arguments explicitly. YesAll functions are curried by default, so partial application is just supplying fewer arguments - no helper needed. YesEvery function is curried by default, so partial application is just supplying fewer arguments - no helper needed. Operator sections ((+ 1), (2 *)) are partial application of infix operators. PartialNo native currying; partial application is done with a closure, or via CPAN helpers such as List::Util/Sub::Curry. YesThe built-in .assuming method primes a routine with fixed arguments, and the * whatever-star (* + 2) also produces partially applied callables. PartialNo currying syntax; partial application is a closure capturing the fixed arguments (often move). Yesfunctools.partial (standard library) fixes leading/keyword arguments; closures or lambda also work for ad-hoc cases.
Lazy evaluationDeferring computation until a value is needed, enabling infinite sequences and avoiding unused work. NoEvaluation is strict: collection methods like map/filter materialize eagerly, and lazy or infinite sequences are not part of the language (deferred, §21). There is no lazy-by-default semantics. PartialNo language-level laziness, but you can defer work with goroutines plus channels, or with closures (thunks) computed on demand. YesThe Lazy module with the lazy/Lazy.force keywords gives explicit memoized thunks, and Seq provides lazy, potentially infinite sequences. YesHaskell is lazy (non-strict) by default: expressions are evaluated only when demanded, which makes infinite data structures like [1..] and repeat natural and separates generation from consumption. Strictness is the opt-in (seq, !, BangPatterns, Data.List.foldl'). Via libraryNo core laziness; CPAN modules such as List::Gen, Scalar::Defer, or tied/iterator patterns provide lazy lists and on-demand evaluation. YesLists are lazy by default where possible; the lazy/gather/take machinery and infinite ranges (1..∞) support lazy and infinite sequences natively. YesIterators are lazy adapters that do no work until consumed, and std's LazyCell/LazyLock (and OnceCell) provide lazy, memoized initialization. YesGenerators and generator expressions evaluate lazily, and itertools builds lazy (even infinite) pipelines pulled element by element.
Pipelines and compositionComposing data transformations so values flow left-to-right through a series of operations. YesThere is no separate pipeline operator; the data-first uniform call convention (§7.2) makes . the single composition mechanism, so transformations chain left-to-right (§7.3). Verified: .filter().map().sum() works. PartialNo pipeline operator or method-chaining stdlib for collections; you compose with nested calls, explicit loops, or channel-based pipeline stages between goroutines. YesThe pipe operator |> threads a value through functions left-to-right, and @@ / Fun.compose express ordinary composition. YesComposition is first-class: (.) composes functions right-to-left and the & operator (Data.Function) threads a value left-to-right through stages, while $ removes parentheses. Point-free pipelines are idiomatic. PartialNo pipeline operator; transformations chain by nesting map/grep (right-to-left) or via method chains on objects; CPAN modules add pipe-like syntax. YesThe feed operators ==> (and <==) pass a list through stages left-to-right, and method chaining plus o/ composition are first-class. PartialNo pipe operator, but iterator adapters chain fluently left-to-right via .; general function composition has no dedicated syntax. PartialNo pipe operator; composition is nested calls or comprehensions, and method chaining is common in fluent libraries (e.g. pandas) rather than a language feature.

Memory & Runtime

FeatureGujiGoOCamlHaskellPerlRakuRustPython
Automatic garbage collectionUnreachable memory is reclaimed automatically by a runtime collector rather than freed by hand. YesGuji is memory-managed: values are immutable by default and the runtime reclaims unreachable data automatically, so a program never frees memory by hand. Both the reference interpreter and the native AOT binary ship a managed runtime, so allocation and reclamation are automatic in compiled programs too. YesGo has a non-generational, concurrent tri-color mark-and-sweep collector with write barriers; it runs mostly alongside your goroutines with only two brief stop-the-world pauses. It is tuned via GOGC/GOMEMLIMIT and runtime.GC() forces a cycle, but you essentially never manage memory manually. YesOCaml uses a generational collector: a small minor heap collected by copying, and a major heap collected by an incremental mark-and-sweep. Since OCaml 5 the runtime is parallel (a per-domain minor heap over a shared, mostly-concurrent major heap); the Gc module exposes tuning and stats. YesGHC's runtime has an automatic generational, copying garbage collector (a small fast nursery plus older generations), parallel and incrementally low-pause in recent versions. Allocation is cheap (bump-pointer) since pure functional code allocates heavily, and you never free by hand. PartialPerl reclaims memory by reference counting: a value is freed the instant its refcount hits zero, giving prompt destruction. There is no cycle collector, so a circular reference leaks until interpreter shutdown unless you break it (see manual memory below). YesRaku (on MoarVM) uses a real tracing, generational, parallel garbage collector rather than reference counting - a deliberate choice for a multi-threaded core. Collection is not prompt, so there is no timely destruction; cycles are reclaimed correctly without weakening. NoRust has no garbage collector. Memory is freed deterministically when a value's owner goes out of scope (RAII / Drop), decided entirely at compile time by ownership. Shared-ownership opt-ins (Rc/Arc) use reference counting, but there is no background collector. YesCPython combines reference counting (prompt) with a cyclic generational collector (the gc module) that reclaims reference cycles refcounting alone cannot. Since 3.13 a second, free-threaded GC implementation pauses other threads instead of relying on the GIL.
Ownership & borrowingA compile-time discipline that tracks who owns each value and lends temporary references, preventing use-after-free and data races without a GC. NoGuji has no ownership or borrow checker. It reaches memory and thread safety a different way - a managed runtime plus immutability by default, where every value crossing a channel is immutable so data races are not even expressible. There are no lifetimes, references, or &/&mut distinctions. NoGo has pointers but no ownership system or borrow checker; the GC makes lifetime tracking unnecessary. Aliasing and concurrent mutation are possible, so data races are caught only at run time by the -race detector, not prevented by the type system. NoOCaml relies on its GC for memory safety and has no ownership/borrowing. Immutability is the default and mutation is explicit (ref, mutable fields, arrays), but lifetimes are not tracked; aliasing is unrestricted and safe because nothing is freed manually. NoHaskell has no ownership or borrow checker; the garbage collector owns all lifetimes and values may be aliased freely - which is safe precisely because they are immutable. Linear types (a GHC extension, LinearTypes) add an opt-in way to track single use, but this is not Rust-style ownership. NoPerl has no ownership model; lifetime is governed entirely by reference counting at run time. Any number of references may alias the same value, and the last one to be dropped frees it - there is no compile-time borrow analysis. NoRaku has no ownership or borrowing; the tracing GC owns all lifetimes. Containers and bindings (my $x) alias freely, and is rw/is copy control mutability of parameters, but none of this models ownership transfer or borrow lifetimes. YesOwnership and borrowing are Rust's defining feature: every value has exactly one owner, the borrow checker enforces either one mutable (&mut) or any number of shared (&) references at a time, and lifetimes ensure no reference outlives its referent. This guarantees memory and thread safety at compile time with no GC. NoPython has no ownership or borrowing; names are bindings to shared, refcounted objects and any number of names may alias the same object. Lifetime is a run-time property of the reference count plus cyclic GC, not a compile-time discipline.
Manual / explicit memory managementThe programmer can or must explicitly allocate, free, or otherwise direct the lifetime of memory. NoGuji exposes no allocation or free primitives - there is no malloc/free, no pointers, and no unsafe escape hatch. Memory is wholly the runtime's concern; the programmer only creates immutable values and lets them go out of scope. PartialDay-to-day Go is fully managed, but the unsafe package and unsafe.Pointer allow raw pointer arithmetic and manual layout control, and arena-style or off-heap allocation is possible via cgo/syscalls. These are escape hatches, not the normal path. PartialAllocation is automatic, but OCaml offers Bigarray and Stdlib.Bytes for off-heap/contiguous buffers, and the C FFI plus the Gc and Obj modules let you manage external memory and tune or trigger collection. Manual free only applies to memory the GC does not own (e.g. C-allocated). PartialOrdinary memory is wholly GC-managed with no free. For interop and performance you can allocate off-heap explicitly via Foreign.Marshal.Alloc (malloc/free, alloca), and the Gc-tuning RTS flags plus performGC influence the collector - but these are FFI/escape-hatch tools, not the normal path. PartialThere is no free, but because reclamation is reference-counted you manage lifetime by managing references - and crucially you must break reference cycles manually, typically with Scalar::Util::weaken, or they leak. undef-ing the last reference frees promptly. NoRaku does not expose manual memory management; the tracing GC handles everything, including cycles, so there is no weaken-for-correctness need as in Perl. The trade-off is no guaranteed, timely destruction - you cannot force when an object is reclaimed. YesFreeing is automatic and deterministic via Drop, but Rust gives full manual control when needed: Box/Vec own heap allocations, std::mem::drop frees early, custom allocators implement the GlobalAlloc trait, and unsafe with raw pointers (*mut T) plus alloc/dealloc allow C-style management. PartialMemory is automatic, but you can influence it: del drops a reference, gc.disable()/gc.collect() control the cyclic collector, and weakref avoids keeping objects alive. True manual allocation lives in C extensions and the ctypes/buffer-protocol layer.
Compiled vs. interpreted executionWhether programs are compiled ahead-of-time to native code or executed by an interpreter / virtual machine. YesGuji is both compiled and interpreted: it ships a reference tree-walking interpreter and a native AOT compiler (guji build emits a real native binary), kept byte-identical by a differential test gate. Native codegen is verified working - it is no longer 'planned'. YesGo is compiled ahead-of-time to native machine code by the gc toolchain (go build). There is no interpreter or bytecode VM in the standard distribution; the compiled binary embeds the runtime and scheduler. YesOCaml offers both: ocamlopt compiles to optimized native code, while ocamlc produces portable bytecode run by a VM, and a top-level REPL interprets interactively. Native is the default for production performance. YesHaskell offers both: GHC compiles ahead-of-time to optimized native code (via its own backend or LLVM), while GHCi interprets interactively and runghc/runhaskell run a script without a separate build. Native compilation is the default for production. NoPerl is interpreted: perl compiles source to an internal opcode tree at startup and then walks it; there is no standard ahead-of-time native compiler. The compile-then-run happens every invocation. PartialRaku source is compiled to bytecode for a virtual machine (primarily MoarVM), which then executes it with a profiling JIT for hot paths. It is not interpreted line-by-line like Perl, but neither does the standard toolchain emit a standalone native executable. YesRust is compiled ahead-of-time to native code through LLVM (rustc/cargo build), with no interpreter or runtime VM. Monomorphization and optimization happen entirely at build time. PartialCPython compiles source to bytecode (.pyc) and then interprets it on a stack VM - so it is compiled-to-bytecode but not to native code. 3.13 added an experimental JIT; alternative implementations (PyPy) JIT to native.
Single self-contained binaryWhether the toolchain can produce one standalone executable with no external runtime or interpreter to install on the target. Yesguji build -o app main.guji emits a single self-contained native executable with the runtime (memory management and task scheduler) linked in, needing no external interpreter on the target. Verified: a built binary runs standalone. YesGo is famous for this: go build produces a single statically-linked binary that embeds the runtime and GC, and with CGO_ENABLED=0 it has no libc dependency, so the binary runs on a bare image. This drives Go's popularity for containers and CLIs. Yesocamlopt links the runtime into the executable, producing a native binary that needs no OCaml installation to run. It dynamically links libc by default, but is otherwise self-contained (fully-static links are achievable with extra flags/musl). YesGHC links the runtime into the executable, so ghc/cabal build produces a native binary needing no Haskell installation on the target. It dynamically links system libc by default, but fully static linking (-static/-optl-static, or a musl toolchain) yields a dependency-free binary. Via libraryStock Perl needs the perl interpreter present, but packagers like PAR::Packer (pp) or staticperl bundle the interpreter and modules into one self-contained executable. It is a third-party tool, not the core workflow. NoRaku programs require the Rakudo runtime and MoarVM to be installed; the standard toolchain does not emit a standalone native binary. Distribution means shipping the compiled bytecode plus a Raku runtime (or a container image). Yescargo build yields a single native binary with the small Rust runtime statically linked. Targeting *-musl produces a fully-static, dependency-free executable; by default it dynamically links the system libc only. Via libraryCPython itself needs an interpreter on the target, but PyInstaller, Nuitka, or cx_Freeze package the interpreter, your code, and dependencies into one self-contained executable. These are third-party, and the bundle is large.
Startup & runtime modelWhat must exist and execute when a program starts: a managed runtime/VM, a scheduler, an entry point, and the cost of getting going. YesExecution begins at sub main(), which returns Unit (no trailing 0) or, as sub main(): Int, an Int exit code. A managed runtime (memory management plus the M:N task scheduler) is embedded in the native binary, so there is no separate runtime to install and startup is fast. YesA compiled Go binary boots its embedded runtime (GC, goroutine scheduler) and then calls func main() in package main; startup is fast and self-contained. The M:N scheduler multiplexes goroutines onto OS threads from the first instruction. YesA native OCaml executable links in a small runtime (GC, exception machinery) and starts at the top-level module bindings, by convention let () = .... Startup is fast; OCaml 5 adds a domain scheduler for parallelism but a single-domain program pays little for it. YesA compiled Haskell program embeds the GHC RTS (garbage collector and the M:N green-thread scheduler) and begins at main :: IO () in module Main. Startup is fast; RTS behavior (heap, capabilities) is tunable at launch via +RTS flags. PartialEach run starts the perl interpreter, which parses and compiles the whole program to opcodes before executing top-to-bottom (no main is required). Startup cost is the interpreter launch plus compiling the script and its used modules. PartialStarting Raku spins up MoarVM and the Rakudo runtime, then runs the program (top-level statements, or a sub MAIN for CLI argument handling). Historically startup was heavy; precompilation of modules has reduced but not eliminated the runtime-launch cost. YesRust has a minimal runtime - essentially just std setup before calling fn main() - with no GC and no scheduler, so startup is very fast and predictable. async work needs a runtime library (e.g. Tokio) you opt into, not a built-in one. PartialLaunching python initializes the interpreter, then imports and runs the module top-to-bottom; the if __name__ == "__main__": guard is the conventional entry point. Interpreter and import startup is the dominant cost for short scripts.
Value vs. reference semanticsWhether assigning or passing a value copies it or shares (aliases) the same underlying object. YesBecause bindings and values are immutable by default, the value-vs-reference distinction is mostly invisible: nothing can observe sharing when nothing mutates, and methods that 'modify' return a new instance (§8.3). Verified: depositing into an Account returns a new value and leaves the original unchanged. YesGo assigns and passes by value - structs and arrays are copied - but slices, maps, and channels are small headers that reference shared backing storage, and you take pointers explicitly with &. So copy-by-default with explicit sharing. YesOCaml binds names to immutable values by default; assignment binds, it does not mutate. Mutable cells are explicit (ref, mutable record fields, arrays) and are shared by reference, while ordinary data is conceptually copied because it never changes. YesBecause values are immutable, the value-vs-reference distinction is observationally invisible: passing or binding never copies semantically and sharing cannot be detected since nothing mutates, so the runtime shares structurally for free. Explicit mutable cells (IORef, STRef, MVar) are the only reference-semantics constructs. PartialPlain scalars/arrays/hashes are copied on assignment (value semantics), while references (\$x, [...], {...}) are explicit aliases to shared storage. @_ aliases the caller's arguments, a notable reference-like exception. PartialRaku distinguishes containers from values: $x is a mutable container, but binding (:=) shares while assignment (=) copies into the container. Sigil-bearing structures and objects are reference-like, and is copy/is rw control argument sharing. YesRust assigns by move by default (ownership transfers, the source becomes invalid), copies only for Copy types, and shares only through explicit borrows (&) or smart pointers (Rc/Arc). The semantics are made explicit by the type and the borrow checker. NoPython uses call-by-object-reference: names always bind to shared objects, so assignment never copies. Mutating a shared mutable object (a list, dict) is visible through every name bound to it; you copy explicitly with copy/deepcopy.
Deterministic destruction & finalizersWhether resources are released at a predictable, well-defined moment (scope exit / refcount zero) versus whenever a collector eventually runs. NoGuji has no destructors, finalizers, or RAII; cleanup is the managed runtime's concern and timing is not specified. File handles from open/stdin are released by the runtime rather than at a user-visible deterministic point. PartialGo has no destructors; the idiom is defer to release resources at function-scope exit, which is deterministic and explicit. runtime.SetFinalizer exists but runs at an unpredictable GC time and is discouraged for resource cleanup. PartialOCaml has no RAII; deterministic cleanup is done explicitly with Fun.protect / try ... finally-style wrappers (e.g. In_channel.with_open_text). Gc.finalise registers a finalizer, but it fires at collection time, not deterministically. PartialWith tracing GC there is no deterministic destruction or finalizers tied to scope - mkWeak/addFinalizer run at unpredictable collection time. Deterministic resource cleanup uses the bracket pattern instead: bracket/finally (and withFile, ResourceT) release at a well-defined point regardless of exceptions. YesReference counting gives Perl prompt, deterministic destruction: an object's DESTROY method runs the instant its last reference goes away (typically scope exit). This is why RAII-style guard objects work cleanly - the cycle-leak caveat aside. NoBecause Raku uses tracing GC, there is no timely destruction - DESTROY (submethod) may run much later or not at all. For deterministic cleanup you use a LEAVE phaser or explicit .close, not object destruction. YesRust's Drop trait gives fully deterministic destruction: a value's destructor runs exactly when it goes out of scope (or on explicit drop), in reverse declaration order. This is the foundation of RAII for files, locks, and memory. PartialRefcounting makes __del__ usually prompt in CPython, but it is not guaranteed (cycles, other implementations) so it is not relied on. The deterministic idiom is the context manager - with/__enter__/__exit__ - which releases at block exit.

Modules, Tooling & Errors

FeatureGujiGoOCamlHaskellPerlRakuRustPython
Module / package systemHow code is split into namespaced units and brought into scope. YesOne file is one module; its path relative to the project root is the module path, with / written as ::. import brings a module into scope under its final segment and members are reached with ::. Top-level declarations are module-private unless marked pub; selective/aliased imports are deferred. YesA package is a directory of files sharing a package clause; capitalized identifiers are exported. import paths are resolved through the module system (go.mod), and a module declaration sets the import-path root. YesEach .ml file is a module (named by the capitalized filename) with an optional .mli interface controlling what is exported. First-class modules, functors (modules parameterized by modules), and explicit signatures make the module system unusually expressive. YesEach .hs file is a module declared with module Name (exports) where; the export list controls visibility (omit it to export everything). import brings names into scope, with qualified, hiding, and selective/aliased forms for fine control. YesA package is a namespace (package Foo::Bar;), conventionally one per .pm file under a matching path. use Module; loads the file and runs its import, typically exporting symbols via Exporter. YesCode is organized into module, class, role, and package blocks; unit module Foo; makes a whole file one module. use/need/require load distributions and you control exports with the is export trait. Lexical, fine-grained importing is the default. YesA crate is the compilation unit; inside it mod defines a tree of modules and pub controls visibility. use brings paths into scope and items are private to their module by default. YesEach .py file is a module and a directory with __init__.py (or a namespace package) is a package. import/from ... import load modules and bind names; everything is public by convention, with a leading _ signalling private.
Package manager & registryThe standard way to declare, fetch, and version third-party dependencies. NoGuji has no package manager or registry. Modules and import work within a project root only; dependency resolution and a published ecosystem are out of scope at v0.1-alpha. YesGo modules are built into the go tool: go.mod pins versions and go get/go mod tidy resolve them with minimal version selection. There is no central registry - modules are fetched directly from their VCS URLs, fronted by a public module proxy and checksum database. Yesopam is the standard package manager and opam.ocaml.org the main repository; it manages packages and switches (isolated compiler+package environments). dune is gaining integrated lockfile-based package management (dune lock) layered on opam's repositories. YesPackages live on Hackage, the central registry, with Stackage providing curated LTS snapshots. Cabal (with cabal install/.cabal files and a dependency solver) and Stack (snapshot-based, reproducible) are the two standard package managers. YesCPAN is the long-standing central archive; cpanm (App::cpanminus) is the popular zero-config installer. cpanfile plus Carton (or Carmel) gives reproducible, project-local dependency installs. Yeszef is the official installer that resolves and installs distributions; the modern fez ecosystem is the primary upload/registry path (browsable at raku.land). A META6.json describes a distribution's dependencies. YesCargo is the official package manager and build tool; Cargo.toml declares dependencies and Cargo.lock pins them. Packages ("crates") are published to and fetched from the central crates.io registry. Partialpip ships with CPython and installs from PyPI, but it alone doesn't lock or manage virtual environments well, so projects layer on tools. The fast Rust-based uv has become the popular modern all-in-one (resolver, lockfile, venv, Python versions); Poetry/PDM are older alternatives.
Build toolThe standard command that compiles/assembles a project into a runnable artifact. YesThe guji tool is the build tool: guji build -o app main.guji compiles a single native executable, and running guji main.guji interprets the source directly. Native compilation is verified working, so this is a real build command, not a planned one. YesThe go tool is the build system: go build compiles a static native binary and go run builds-and-runs. Builds are fast and dependency-free thanks to the integrated module system. Yesdune is the de-facto build system, driven by small declarative dune files; dune build compiles to native or bytecode. It composes across packages and handles tests, docs, and install rules. YesCabal (cabal build) is the standard build tool, driven by a .cabal package file and invoking GHC; Stack (stack build) is the popular alternative layering reproducible Stackage snapshots on top. Both compile to a native executable. PartialPerl is interpreted, so scripts run with no build step. For distributions, ExtUtils::MakeMaker (Makefile.PL) or Module::Build produce the standard perl Makefile.PL && make && make test && make install flow, often driven by Dist::Zilla. PartialPrograms run directly via the raku runtime (compiling to MoarVM bytecode on the fly), so there is no separate build command for scripts. Distribution build/test/install is handled by zef build/zef test against META6.json. YesCargo is the build tool: cargo build (debug) / cargo build --release (optimized) compile via rustc, caching artifacts and wiring up all crate dependencies. cargo run builds and runs. Via libraryPython runs source directly, so most projects need no build. Packaging into wheels uses a PEP 517 build backend (setuptools, Hatchling, Flit) invoked through the build frontend or uv build.
Code formatterA canonical auto-formatter that normalizes source layout. NoNo canonical auto-formatter ships with the toolchain. The language leans on its 'one obvious way' design - minimal redundant syntax - but a gofmt-style formatter is not part of v0.1-alpha. Yesgofmt (and go fmt) is the canonical, non-configurable formatter shipped with the toolchain; goimports adds import management. Formatting is culturally mandatory, ending all style debates. Yesocamlformat is the standard auto-formatter, configured per-project via an .ocamlformat file and integrated with editors and dune build @fmt. It is distributed through opam. Via libraryNo formatter ships with GHC, but the ecosystem has several widely used auto-formatters installed from Hackage - fourmolu and ormolu (the de-facto modern choices), plus older stylish-haskell and brittany. There is no single mandated canonical style. Via libraryPerl::Tidy (perltidy) is the widely used formatter, configurable via .perltidyrc. It is a CPAN module rather than part of core Perl. Via libraryThere is no official formatter in core; community tooling (e.g. the Rakudo::Formatter / editor-based efforts) exists but is not yet a single canonical standard, so layout is largely manual. Yesrustfmt (cargo fmt) is the official formatter, installed with the standard toolchain and tunable via rustfmt.toml. Running it is standard practice across the ecosystem. Via libraryNo formatter ships with CPython, but Black ("the uncompromising formatter") and the much faster Rust-based Ruff formatter (Black-compatible) are the de-facto standards installed from PyPI.
Error-handling modelHow a program signals, propagates, and recovers from failure. YesGuji has no exceptions: fallible operations return Result[T, E] or Option[T], and the postfix ? operator propagates an Err/None early (§11). Truly unrecoverable bugs use panic, which aborts the process and cannot be caught. Verified: ? short-circuits both Result and Option. YesErrors are ordinary error values returned as the last result and checked explicitly with if err != nil; errors wrap/unwrap via %w, errors.Is/errors.As. panic/recover exists but is reserved for exceptional, unrecoverable situations. YesOCaml offers both styles: built-in exceptions (raise/try ... with) and a typed result/option discipline encouraged by the standard library and let* binding operators. Idiom increasingly favours result for recoverable errors. YesHaskell strongly favors typed errors as values: Maybe a for absence and Either e a (often via the ExceptT monad transformer) for recoverable failure, threaded with do-notation. Runtime exceptions also exist (throwIO/catch, Control.Exception) for IO and truly exceptional conditions. YesClassic Perl signals errors by die-ing (a string or object) and catching with eval { }; many functions also return a false value plus $!. Perl 5.34+ provides native try/catch (stable in 5.40), and Try::Tiny remains popular. YesRaku uses exceptions thrown with die/fail and handled by try/CATCH blocks; fail returns a lazy Failure that throws only when used. Typed exception classes and resumable control flow round it out. YesRecoverable errors use Result<T, E> (and Option<T>) with the ? operator for early propagation; panic! handles unrecoverable bugs. Crates like thiserror/anyhow ease error types - Rust's ? model directly inspired Guji's. YesPython is exception-based: raise signals errors and try/except/else/finally handles them, with a rich exception class hierarchy. "Easier to ask forgiveness than permission" (EAFP) is the idiom.
Testing storyThe standard way to write and run automated tests. NoThere is no in-language test framework; the implementation's own strategy is a directory of .guji fixtures (319 of them) each paired with expected output, run end-to-end through both interpreter and compiler. A user-facing testing API is not specified. YesTesting is built in: *_test.go files with func TestXxx(t *testing.T) are run by go test, which also handles benchmarks, fuzzing, and coverage. No third-party framework is needed. Via libraryNo test runner ships with the compiler, but dune orchestrates tests (dune test) and the ecosystem standardizes on libraries like Alcotest, OUnit, and inline ppx_expect tests. Via libraryNo runner ships with GHC, but the ecosystem is strong: HUnit for unit tests, hspec/tasty as test frameworks, and especially QuickCheck - property-based testing was pioneered in Haskell. Tests run via cabal test or stack test. YesTesting is a core strength: Test::More/Test2 emit TAP (Test Anything Protocol) and prove runs t/*.t suites. The toolchain has assumed TAP-based tests since the early days. YesThe core Test module ships with Rakudo and emits TAP; zef test / prove6 run t/ suites. Functions like ok, is, and is-deeply are available out of the box. YesTesting is built in: #[test] functions (unit, integration in tests/, and doc-tests) are run by cargo test. Assertions like assert_eq! and #[should_panic] come with the language. YesThe standard library bundles unittest and doctest, but the third-party pytest is the de-facto standard for its plain-assert style and fixtures. Tests run via pytest or python -m unittest.
OOP / classesSupport for user-defined types with fields, methods, and (optionally) inheritance. PartialGuji has class (product types with has fields, public $./private $! twigils, and $self methods) and enum sum types with methods - but it is functional-first: values are immutable, 'mutating' methods return new instances, and there is no inheritance (polymorphism is via match). Verified. PartialGo has struct types with methods and interfaces for polymorphism, plus composition via struct embedding - but deliberately no classes or implementation inheritance. Interface satisfaction is structural (implicit). YesOCaml has a full object system (class, objects, inheritance, structurally-typed row-polymorphic methods), though idiomatic OCaml favours modules, records, and variants instead. Objects are present but used sparingly. NoHaskell has no classes, objects, or inheritance - despite the name, a class is a type class (an interface of overloaded functions), not an object class. Data is modeled with algebraic data types and records, and polymorphism comes from parametric types plus type classes. YesClassic Perl OOP is built from packages, blessed references, and @ISA inheritance, usually smoothed by Moose/Moo. Perl 5.38+ adds a native class/field/method system (Corinna), stabilizing but still marked experimental as of 5.42. YesRaku has a rich native object model: class with has attributes and methods, roles for composable behavior, multiple inheritance, and multi-dispatch. The metaobject protocol makes it deeply introspectable. PartialRust has struct/enum types with impl methods and traits for polymorphism (static or dyn dynamic dispatch), but no classes and no inheritance - composition and traits replace it. YesPython is thoroughly object-oriented: class with methods, __init__, multiple inheritance (C3 MRO), and dynamic duck typing. @dataclass reduces boilerplate for data-holding classes.
Foreign function interface (FFI)Calling into C or other native libraries from the language. NoGuji has no FFI: there is no way to declare or call into C or other native libraries. Platform IO is provided through built-in primitives (print, note, read_file, open, stdin, args, exit), not through user-facing foreign bindings. Yescgo lets Go call C by importing the pseudo-package "C" with C in a preamble comment; the toolchain compiles and links it. There is overhead per call, so pure-Go is preferred when practical. YesOCaml has a mature C FFI via external declarations and stub C files (the manual's interop chapter), and the ctypes library binds C libraries without writing C stubs by describing types at the OCaml level. YesThe Foreign Function Interface is part of the standard (Haskell 2010): foreign import ccall declares a C function as a Haskell value, with the Foreign/Foreign.C modules for marshalling. Tools like c2hs/hsc2hs and bindings generators ease larger interop. YesThe classic route is XS (C glue compiled into a .so); the modern, no-compiler-needed route is FFI::Platypus, which binds shared libraries at runtime. Both are widely used CPAN-backed mechanisms. YesRaku ships NativeCall in core: declare a sub is native('lib') and Raku marshals arguments to the C library at runtime, with CStruct/CArray for native data. No separate compile step is needed. YesRust calls C through extern "C" blocks inside unsafe, with the libc crate and tools like bindgen (C→Rust) and cbindgen (Rust→C) generating bindings. It is a first-class, zero-overhead-friendly path. YesThe standard library bundles ctypes to call shared libraries with no compilation, and cffi is the popular alternative. For performance-critical glue, C-extension modules and Cython/PyO3 are used.

Text & Metaprogramming

FeatureGujiGoOCamlHaskellPerlRakuRustPython
First-class regular expressionsRegex patterns as a built-in literal/type with a dedicated match operator, rather than strings passed to a library. YesRegex is a built-in type with a slash-delimited literal (/\w+/) and the ~~ match operator yielding Option[Match]; Regex.compile(s) builds one from a string. Named captures (?<n>..) are read as $m<n> (an Option[Str]). Verified against the compiler. Via libraryNo regex literals or operators; the stdlib regexp package (RE2 engine) compiles patterns from strings via regexp.MustCompile. No backreferences or lookaround by design. Via libraryNo regex in the language or stdlib Str aside; idiomatic code uses the re library (or pcre). Patterns are values built from string syntax, with no literal form. Via libraryNo regex literals or operators in the language; idiomatic code uses libraries such as regex-tdfa (POSIX, linear-time) or pcre-light, exposed through the Text.Regex.Base =~ operator whose result type is polymorphic. For richer parsing, parser-combinator libraries are usually preferred. YesRegex is core to the language: =~ and !~ operators, /.../ literals, m//, s///, named captures via %+. Perl's regex engine is the de-facto reference for many others. YesRegexes are first-class objects with rx/.../ literals and the smartmatch ~~ operator; they double as the building blocks of grammars. A full redesign of Perl 5 regex with declarative, composable syntax. Via libraryNo regex in std; the official regex crate (linear-time, no backtracking) is near-universal. Patterns compile from strings at runtime, or via macros like regex! for compile-time checking. Via libraryNo regex literals/operators, but the re module ships in the stdlib and is idiomatic everywhere. Patterns are strings (commonly raw r"...") compiled with re.compile.
String interpolationEmbedding variables and expressions directly inside a string literal that is evaluated to a formatted result. YesDouble-quoted strings interpolate sigil bindings ($x, @xs, %m) directly and arbitrary expressions inside { ... }; single-quoted strings are literal. Verified: "total: { @scores.sum() }" evaluates the embedded call. NoNo interpolation; strings are concatenated or formatted with fmt.Sprintf using verbs like %d/%s/%v. The verbs and arguments are positional and separate from the literal. NoNo interpolation in the language; OCaml has no universal value-to-string, so Printf.sprintf with typed format specifiers is idiomatic. PPX extensions (e.g. ppx_string) add interpolation as a library. Via libraryNo built-in interpolation; ordinary code uses Text.Printf.printf/++ or Data.Text. Quasi-quoter libraries like interpolate, string-interpolate, or neat-interpolation add [i|...|] interpolation as a Template Haskell extension. YesDouble-quoted strings interpolate scalars ($x) and array/hash elements directly; arbitrary expressions interpolate via @{[ ... ]}. Single quotes are literal. YesDouble-quoted strings interpolate scalars and, with { ... }, any expression; method calls on a sigil'd variable also interpolate. Fine-grained control via quoting adverbs (q/qq). Partialformat!/println! macros support inline captured identifiers in {name} (since Rust 1.58), but arbitrary expressions inside braces are not allowed - only named arguments or captured variables. Yesf-strings (f"...", since 3.6) embed arbitrary expressions in { }, with format specs and (3.8+) = self-documenting form. Evaluated at runtime in the local scope.
Grammars / PEG parsingA built-in, reusable, structured parser construct (named rules, ordered choice) for parsing beyond what flat regex can express. Yesgrammar is a signature feature: PEG ordered-choice with token/rule/regex productions, a TOP entry, and <name> subrule references. G.parse(input) returns Option[Bush], a navigable parse tree. Verified: an Email grammar parses a real address. Via libraryNothing built in; parser generators like goyacc (LALR), or PEG libraries such as pigeon and participle, are used. Hand-written recursive-descent parsers are also common. Via libraryNo grammar construct in the language, but OCaml has strong parsing tooling: ocamllex/menhir (LR), and PEG/combinator libraries like angstrom. Menhir is the de-facto standard. Via libraryNo grammar construct in the language, but Haskell is renowned for parser-combinator libraries - parsec, megaparsec, and attoparsec - that build composable, named parsers directly as ordinary values. Generators like happy (LALR) and alex (lexer) also exist. Via libraryNo grammar keyword, but recursive regexes ((?R), (?1)) and modules like Regexp::Grammars and Marpa provide full grammar parsing on top of the regex engine. YesFirst-class grammar declarations group named token/rule/regex productions with a TOP; grammars are inheritable, overridable classes and can carry semantic actions. Raku's own parser is a Raku grammar. Via libraryNo grammar construct in std; ecosystem crates cover it: pest (PEG from a .pest grammar), nom (parser combinators), and lalrpop (LR). pest derives a parser from a grammar file at compile time. Via libraryNo grammar in the language for user code, though CPython 3.9+ uses a PEG parser internally. Libraries like pyparsing, parsimonious (PEG), and lark provide grammar-based parsing.
Macros / syntactic metaprogrammingCompile-time transformation of source/AST into code, letting users extend the language's syntax or generate code. NoNo macro system. Following 'one obvious way', Guji has no user-facing syntactic metaprogramming; code generation lives only in the compiler, and grammars handle structured text rather than language extension. NoNo macros by design. The closest mechanism is go generate, which runs external code-generators (e.g. stringer) as a build step, but this is text tooling, not in-language macros. Via libraryNo macros in the core language, but the PPX ecosystem rewrites the typed AST at compile time ([@@deriving ...], [%expr ...]) via ppxlib. This is the standard metaprogramming path. Via libraryNo macros in the core language, but Template Haskell provides compile-time metaprogramming: splices ($(...)) and quotations ([| ... |]) generate and inspect the AST, and quasi-quoters ([name| ... |]) embed custom sub-languages. It is the standard, if heavyweight, codegen path. PartialNo true macros, but source filters and especially Keyword::Declarator/PL_keyword_plugin (used by modules like Function::Parameters) let modules add new keywords at parse time. Powerful but niche. PartialRaku is designed for self-extension via slangs and a compile-time macro/AST API, but macros remain experimental and unstable pending the RakuAST rewrite. BEGIN-time code and custom operators cover most needs today. YesTwo kinds: declarative macro_rules! (pattern-based, hygienic) and procedural macros (function-like, derive, and attribute) that transform a TokenStream. Macros are central to the ecosystem (vec!, derive, serde). NoNo macro system. Metaprogramming is done at runtime via decorators, metaclasses, and ast/exec, but there is no compile-time syntactic macro facility that extends the grammar.
Runtime reflection / introspectionInspecting and manipulating types, fields, and values dynamically at runtime. NoGuji is fully static with no runtime type information needed for dispatch (§18); there is no reflection API. Type identity is erased at compile time and match over sum types replaces runtime type inspection. YesThe stdlib reflect package inspects and manipulates arbitrary values, types, struct fields, and tags at runtime. Widely used by encoders (encoding/json) and ORMs. NoStatically typed with full type erasure and no runtime type information, so there is no reflection. Introspection is approximated at compile time with PPX deriving rather than at runtime. PartialHaskell is statically typed with type erasure, so there is no general runtime reflection. Data.Typeable/Data.Dynamic allow limited runtime type representation and casting, and Data.Data (the SYB "scrap your boilerplate" approach) enables generic traversal of values that derive Data. YesHighly introspective: symbol-table access (%main::), ref/blessed, can/isa, and modules like Class::MOP/Moose provide a full meta-object protocol at runtime. YesBuilt on a Meta-Object Protocol: .^name, .^methods, .^attributes, and the full .HOW interface expose and modify classes/roles at runtime. Reflection is a first-class language facility. PartialNo general reflection; types are erased. std::any::Any allows limited runtime downcasting of 'static types, and trait objects give dynamic dispatch, but field/type enumeration is not available. YesDeeply reflective at runtime: type, dir, getattr/setattr, inspect, __dict__, and metaclasses let code examine and modify objects, classes, and call frames dynamically.
Dynamic code evaluation (eval)Compiling or interpreting source code constructed at runtime, from within the running program. NoThere is no eval: a native-compiled binary embeds no compiler or interpreter for arbitrary source. Runtime dynamism is limited to Regex.compile, which builds patterns (not arbitrary code) from strings. NoCompiled with no runtime eval; the standard go/parser and go/types packages can parse and type-check source, but executing it requires invoking the toolchain or a third-party interpreter (yaegi). PartialNo eval in compiled programs, but the bytecode compiler-libs/toplevel can be embedded to evaluate source at runtime, and the MetaOCaml dialect offers staged code generation. Not idiomatic for ordinary code. Via libraryCompiled programs have no built-in eval, but the GHC API and the hint package wrap the interpreter so a program can compile and run Haskell source supplied at runtime. This is not idiomatic and carries the usual safety caveats. YesString eval EXPR compiles and runs Perl source at runtime (block eval { } is exception handling, not codegen). Powerful and a classic security caveat. YesThe EVAL routine compiles and runs a string of Raku source at runtime; it must be explicitly enabled with use MONKEY-SEE-NO-EVAL as a safety gate. NoAhead-of-time compiled with no runtime eval. Dynamic code requires shelling out to rustc, embedding an interpreter, or scripting crates like rhai/mlua for a different language. YesBuilt-in eval() evaluates an expression and exec() runs statements from a runtime string, with optional namespace dicts. compile() produces reusable code objects.
Operator overloadingGiving built-in operators (e.g. +, ==, []) custom meaning for user-defined types. NoOperators have fixed meaning on built-in types only; users cannot overload them. This is deliberate under 'one obvious way' - behavior is expressed through named, data-first methods instead. NoNo operator overloading at all; operators work only on built-in types. User types compose behavior through ordinary methods (e.g. a.Add(b)). PartialOperators cannot be overloaded (no ad-hoc polymorphism), but you can define brand-new infix operators or locally rebind existing symbols (( +. ), custom ( ^^ )). Each numeric type already has its own operators (+ vs +.). YesOperators are ordinary functions resolved through type classes, so +, ==, and <> are overloaded by writing instances (e.g. Num, Eq, Semigroup). You can also define brand-new infix operators from almost any symbol characters and set their fixity. YesThe overload pragma maps operators (+, "", ==, ...) to methods on a blessed class. Stringification and numification overloads are common. YesOperators are just multi-dispatch subs; define multi sub infix:<+> (or prefix/postfix/circumfix) to overload existing operators or create new ones with arbitrary Unicode names. YesOperators are sugar for std::ops traits; implement Add, Mul, Index, PartialEq, etc. for a type to overload +, *, [], ==. Cannot invent new operator symbols. YesDunder methods (__add__, __eq__, __getitem__, ...) define operator behavior on classes. Pervasive in numeric/array libraries like NumPy.
Compile-time code generation / derivationAutomatically generating boilerplate (printers, equality, serializers) for a type at build time instead of writing it by hand. NoNo user-facing derivation mechanism: there is no derive, and traits/interfaces are deferred. Common operations are provided as built-in prelude functions rather than generated per type. PartialNo language-level derivation, but go generate plus tools (stringer, mockgen) generate .go source as a build step. Reflection covers many cases at runtime instead. Via library[@@deriving ...] (via ppx_deriving/ppxlib) generates show, eq, compare, and (with ppx_yojson) serializers from a type definition at compile time. The standard boilerplate solution. YesThe deriving mechanism auto-generates type-class instances (Eq, Ord, Show, Read, Functor, etc.) from a data declaration; DeriveGeneric plus libraries like aeson derive serializers, and GeneralizedNewtypeDeriving/DerivingVia extend it. Template Haskell covers anything else. Via libraryNo compile-time derivation, but object frameworks like Moose/Moo generate accessors and methods from attribute declarations at class-construction time (effectively at BEGIN). PartialNo derive attribute, but the Meta-Object Protocol lets traits and roles synthesize methods/accessors at compile time; is traits and custom trait_mods generate behavior. Accessors are auto-generated from public attributes. Yes#[derive(...)] invokes procedural macros to generate trait impls (Debug, Clone, PartialEq, and serde's Serialize/Deserialize) at compile time. Central to idiomatic Rust. Partial@dataclass and attrs/pydantic generate __init__/__repr__/__eq__ at class-definition time from field annotations. Done at runtime (import time) rather than via a compiler.

Type System

FeatureGujiGoOCamlHaskellPerlRakuRustPython
Static vs dynamic typingWhether types are checked at compile time or carried by values at run time. YesStatically typed: every binding and expression has a type known at compile time, with no runtime type information needed for dispatch. The two operands of an arithmetic operator must be the same numeric type - there is no implicit Int/Float coercion. YesStatically typed and compiled; every variable has a fixed type the compiler enforces. Conversions between numeric types must be explicit (float64(i)), and there is no implicit coercion. YesStatically typed with a sound Hindley-Milner system; ill-typed programs are rejected before they run. It never coerces between numeric types, which is why it has separate operators for Int (+) and Float (+.). YesStatically and strongly typed: every expression has a type fixed at compile time via Hindley-Milner inference, and ill-typed programs are rejected by GHC before they run. There is no implicit numeric coercion - you convert with fromIntegral, realToFrac, etc. NoDynamically typed: a scalar holds any value and its meaning comes from context - "10" + 5 is 15 (numeric), "10" . 5 is "105" (string). Errors like calling a missing method surface only at run time. PartialGradually typed: untyped by default like a dynamic language, but optional type constraints on variables, parameters, and attributes are enforced - some at compile time, the rest at run time. You opt into as much static checking as you want. YesStatically and strongly typed with no implicit numeric coercion; mismatches are compile errors, and even integer widths must be converted explicitly with as or .into(). NoDynamically typed: names are rebindable to any type and checks happen at run time. Optional type hints (PEP 484) are accepted but not enforced by CPython - they document intent and feed external checkers like mypy.
Type inferenceHow much type information the compiler works out for you versus what you must annotate. YesInference is local and complete within a function body - bindings and sub-expressions rarely need annotations, and functions need no return-type annotation. Only top-level pub declarations must annotate their parameter and return types to pin a stable module interface. PartialThe short form := infers a local's type from its initializer, and Go 1.21+ infers many generic type arguments. But function parameters, return types, and struct fields must always be annotated - inference is deliberately local. YesFull Hindley-Milner inference: most code needs no annotations at all, and the compiler infers the most general (polymorphic) type. Annotations are an occasional aid for clarity or to resolve ambiguity (e.g. record-field overloading). YesFull Hindley-Milner type inference (with type-class constraints) means most code needs no annotations and GHC infers the most general type. Top-level signatures are optional but conventional for documentation and to pin down ambiguous or polymorphic types. NoNo type inference because there are no static types to infer. The sigil ($, @, %) marks the broad shape of a variable, but a scalar's value type is decided at run time by context. PartialVariables default to the untyped Any; there is no Hindley-Milner-style inference, but literals carry concrete types and the compiler can constant-fold and check some things early. You annotate where you want static guarantees. YesLocal type inference is pervasive: let bindings and closure parameters are usually inferred from context, including backward from later use. Function signatures, however, must be fully annotated. NoThe runtime needs no inference - names just bind to objects. Static checkers (mypy, Pyright) do infer types of locals from assignments, but that is tooling layered on top, not part of the language runtime.
Sum / algebraic data typesA type that is exactly one of several labeled variants, each carrying its own typed fields. Yesenum is a first-class sum type: a value is one of its variants, each with typed fields, read only by match. Enums may be generic, and Option/Result are just standard-library enums. Verified working. NoGo has no sum type. The usual workaround is a sealed interface implemented by a fixed set of structs, recovered with a type switch - but the compiler does not check that all variants are handled. YesVariant types (type t = A | B of int) are OCaml's original algebraic data type and the construct that popularized pattern matching. They compose with records, tuples, and generics to form arbitrary algebraic shapes. YesAlgebraic data types are core: data declares a sum of constructors, each carrying typed fields, read only by pattern matching. Maybe and Either are just ordinary library ADTs, and types may be parametric. NoNo sum type exists. The closest idiom is a tagged array (or hash) whose first element names the variant, dispatched with a hand-written if/elsif ladder on the tag - with no exhaustiveness check. PartialPlain enum gives only flat constant values, not payload-carrying variants. To model a true sum you build a role that a closed set of classes does, then dispatch with given/when or multi dispatch - without compile-time exhaustiveness. Yesenum is a true sum type whose variants carry named or positional fields, and it is the backbone of the standard library (Option, Result). match over it is checked for exhaustiveness at compile time. PartialThere is no native sum type, but a Union[A, B] / A | B annotation plus structural match/case (3.10+) approximates one for checkers. Exhaustiveness is verified only by external tools (e.g. mypy via assert_never), not by the runtime.
Exhaustiveness checkingWhether the compiler proves that a match handles every possible case. Yesmatch must be exhaustive: the compiler rejects a match that does not cover every possible value and reports which cases are missing. Verified: omitting a Rect arm fails with non-exhaustive match: missing Rect. NoA switch on a type or value is never checked for completeness; a missing case simply falls through (or hits default). Catching unhandled cases relies on a runtime panic or third-party linters like exhaustive. YesThe compiler emits a non-exhaustive match warning (promotable to an error) when a constructor is unhandled, and also flags redundant arms. This is a core part of why ADTs are safe to refactor. YesGHC checks pattern coverage and, with -Wincomplete-patterns (part of -Wall), warns on any constructor left unhandled - promotable to an error with -Werror. Adding a new constructor surfaces every now-incomplete match. NoNo structural matching and therefore no exhaustiveness. An if/elsif chain on a tag leaves a forgotten variant to fall through to else/die at run time, if it is caught at all. Nogiven/when is an ordered first-match construct with no completeness analysis; a value matching none of the when arms just runs the default (or nothing). You guarantee coverage yourself. Yesmatch must cover every variant or the program does not compile, making an added enum variant a guided refactor across the codebase. A _ arm explicitly opts out of that check for the remaining cases. NoStructural match/case performs no exhaustiveness check at run time - an unmatched value falls through and the function returns None. Static checkers can flag it via a case _ as x: assert_never(x) guard.
Generics / parametric polymorphismWriting functions and types parameterized over the types they operate on, with compile-time type safety. YesType parameters are written in square brackets after the name (sub id[T]($x: T): T), and enum/class declarations may be generic too. Inference usually lets you call generic code without spelling out the type arguments. Verified: a generic id works on Int and Str. YesGenerics arrived in Go 1.18: type parameters in square brackets with constraint interfaces (now defined as type sets) bound the permitted arguments. any is the unconstrained constraint; there is no specialization or variance. YesParametric polymorphism is built in and inferred: a function over 'a list is generic with no annotation, and types like 'a option are everywhere. The compiler infers the most general type automatically. YesParametric polymorphism is built in and inferred: a function over [a] is generic with no annotation, and type variables range over all types. Type classes add bounded (ad-hoc) polymorphism as constraints like Ord a =>. YesBecause everything is dynamically typed, subroutines are uniformly polymorphic - a sub that indexes an array works for any element type with no type machinery at all. There is no static parametricity to enforce, though. YesGenerics come via parameterized roles (role Container[::T] {...}) and type-captures (::T) in signatures, letting you constrain and reuse a type variable across a routine. It is more dynamic than HM but fully typed when you ask. YesGenerics with trait bounds (fn first<T>(xs: &[T]) -> Option<&T>) are monomorphized for zero-cost specialization. Bounds like T: Ord let the compiler verify the operations a generic uses are available. PartialFunctions are duck-typed and thus generic at run time. For static generics, type hints use TypeVar/Generic (and the PEP 695 def f[T](...) syntax in 3.12+), checked only by external tools - CPython erases them.
Nullability / optionalsHow a possibly-absent value is represented and whether the absence is visible in the type. YesThere is no null. Absence is the standard sum type Option[T] (Some/None), so a possibly-missing value is visible in the type and the compiler forces you to handle None via match, unwrap_or, or the ? operator. PartialPointers, interfaces, maps, slices, and channels can be nil, and the type does not record whether a value may be absent - a nil dereference panics at run time. The common 'comma-ok' idiom (v, ok := m[k]) signals absence with a separate bool. YesOCaml has no null. The 'a option type (Some/None) makes absence explicit and the pattern match forces you to handle the None case, eliminating null-dereference bugs. YesHaskell has no null. Absence is the Maybe a type (Just/Nothing), so a possibly-missing value is visible in the type and the compiler forces you to handle Nothing via pattern matching, fromMaybe, or the Maybe monad. PartialThe single absent value is undef; any scalar may be undef and you test it with defined. The defined-or operator // supplies a fallback. Absence is not tracked in any type - using undef numerically just warns. PartialEvery type has an undefined (type-object) state and a defined state, expressed with definedness smileys: Int:D requires a value, Int:U an undefined one. The defined-or // returns the first defined operand as a fallback. YesRust has no null; Option<T> (Some/None) encodes absence in the type and match/?/unwrap_or force you to deal with it. A niche optimization makes Option<&T> and Option<Box<T>> cost no extra space. PartialNone is the universal absent value and any reference may be None, so attribute access on it raises AttributeError at run time. The Optional[T] / T | None hint records the possibility for static checkers only.
Structural vs nominal typingWhether type compatibility is decided by shape (members/methods) or by the declared type name. NoTyping is nominal: a class or enum is compatible only with itself, constructed and matched by name, and two classes with identical fields are distinct types. There is no structural or interface-style typing (traits are deferred). PartialStructs are nominal, but interfaces are structural: a type satisfies an interface just by having the right method set, with no implements declaration. So Go mixes both - named structs, duck-typed interfaces. PartialRecords and variants are nominal (compatibility is by type name), but objects and polymorphic variants are structural via row polymorphism - an object type is inferred from the methods invoked, with no class needed. PartialData types and type classes are nominal: a data type is compatible only with itself, and an instance must be declared explicitly, so two identical-looking types are distinct. Records are nominal too, though extensions and libraries add some structural-style row/anonymous-record typing. PartialObject dispatch is essentially duck typing: a method call works on any blessed reference that can respond, regardless of class - so compatibility is by behavior. The blessed package name gives a loose nominal ref/isa check when you want it. PartialClasses and roles are nominal by default (matched by name / does), but subset types add structural-style where constraints, and smartmatch can test shape. The community also explores explicit structural protocols on top. NoRust is nominal: structs and enums are matched by name, and a type implements a trait only via an explicit impl block. Two structurally identical structs are unrelated types - there is no structural subtyping. PartialAt run time Python is duck typed - behavior is all that matters. Statically, Protocol classes give structural typing (a type matches by having the right methods), alongside ordinary nominal class/isinstance checks.