Build, Run, REPL: the toolchain experience in eight languages
How eight languages answer the same three questions - how do I build it, run it, and poke at it live?
Every language eventually hands you the same three doors: build it into something, run that something, and - when you are lost - drop into a prompt and poke at it live. The doors look alike from a distance. Up close, each language has decided something different about what a programmer's day should feel like. Here is how eight of them answer.
The compiled, batteries-heavy crowd
Rust gives you cargo, and cargo is the whole world. One tool builds, tests, benchmarks, fetches dependencies, and publishes. The compile step is the experience: the borrow checker is a conversation, not a formality, and the first build of a fresh crate is slow enough that everyone has opinions about it.
fn greet(name: &str) -> String {
format!("hello, {name}")
}
fn main() {
println!("{}", greet("range"));
}
You cargo run for the loop and cargo build --release for the artifact. There is no official REPL; people reach for evcxr when they want one, and mostly they do not, because cargo run on a scratch file is fast enough to stand in.
Go aims the same compiled-binary promise at a different temperament: make the toolchain boring on purpose. go run main.go to run, go build to drop a single static binary, go fmt so nobody argues about braces. Compilation is fast enough that the edit-run loop feels almost interpreted, which is the whole pitch.
package main
import "fmt"
func main() {
fmt.Println("hello, range")
}
Go has no REPL and is unbothered about it. The culture says: write a tiny main, run it, delete it. The reward for the spartan language is a build story so simple it fits in one verb.
The typed functional pair
Haskell is the language that makes the REPL central. ghci is where Haskell is actually written: you load a module, ask :t for a type, and let the compiler tell you what you meant. The language is pure and lazy, so a value is a recipe that does nothing until forced, and the type system - type classes, monads - is the load-bearing wall.
greet :: String -> String
greet name = "hello, " ++ name
main :: IO ()
main = putStrLn (greet "range")
runghc runs a script, ghc compiles a native binary, and ghci is the home you return to. The IO type in that signature is the honest part: a String -> String cannot print, so anything that touches the world wears IO on its sleeve. The build is slower than the functional-but-pragmatic alternatives, but the interactive top level is the best in this list.
OCaml sits one room over with the same ML roots and a more eager disposition. It ships a bytecode compiler (ocamlc), a native one (ocamlopt), and a top level (utop, the one people actually use). dune has become the cargo-shaped front end that ties building, testing, and running together.
let greet name = "hello, " ^ name
let () = print_endline (greet "range")
OCaml's draw is that you get strict evaluation, a fast native compiler, and a genuinely useful REPL in the same box - the practical middle between Haskell's purity and a scripting language's immediacy.
The scripting trio
Python is the one most people meet first, and its toolchain is the interpreter, full stop. python3 file.py runs it; python3 alone is the REPL; there is no separate build step because there is no build.
def greet(name: str) -> str:
return f"hello, {name}"
print(greet("range"))
Those type hints are checked by nobody at runtime - they are documentation that tools like mypy can read. The REPL is woven so deeply into how Python is taught that "open a shell and try it" is the default debugging move, and notebooks are that same loop wearing a nicer coat.
Perl is the elder scripting statesman, and its loop is perl script.pl with no ceremony. There is no standard REPL; the language's answer was always the one-liner - perl -e '...' - and -n, -p, and -i turn the interpreter into a stream editor you drive from the command line.
use strict; use warnings;
sub greet { my ($name) = @_; "hello, $name" }
print greet("range"), "\n";
Larry Wall built Perl so that easy things stay easy and hard things stay possible, and the toolchain honors that: the barrier between "thought" and "running program" is a single command and a pair of quotes.
Raku - Perl's redesigned sibling - keeps the sigils but adds the REPL Perl never had. Run raku script.raku, or type raku for an interactive prompt that handles multi-line definitions. It is gradually typed, with grammars and a richer object model baked in, and it leans on a precompilation cache so the heavy startup amortizes across runs.
sub greet(Str $name) { "hello, $name" }
say greet("range");
guji: the new compiled one
guji (v0.1-alpha, in-house) is the outlier worth dwelling on, because it tries to take both doors at once. It is compiled and statically typed, functional-first, and it keeps a reference interpreter and a native AOT compiler in deliberate parity - the same source runs both ways. The day-to-day loop is guji file.guji to interpret; the artifact is guji build -o out file.guji, which produces a native binary that prints the same thing.
The surface borrows Perl's sigils - $x for scalars, @xs for lists - but the body is ML underneath: pattern matching, Result and Option, and string interpolation that runs expressions inline with { ... }. Functions need no return-type annotation; main is just sub main() { ... } with no ceremonial return.
sub greet($name: Str): Str {
"hello, $name"
}
sub main() {
print(greet("range"))
mut $total = 0
for $n in [1, 2, 3, 4, 5] {
$total = $total + $n
}
print("sum: $total")
}
That program runs identically under the interpreter and as a compiled binary, which is the parity promise made concrete. The IO story is real and not a toy: print to stdout, note to stderr, args() for the command line, read_file($p) returning a Result[Str, Str] you have to match, and stdin as a real Handle whose .lines() yields a channel of strings. Lists answer .count(); strings answer .length() - a small seam to watch for.
Concurrency is first-class rather than bolted on. You hatch { } a lightweight task and hand work across a typed Chan[T], and a for loop drains the channel until it closes:
sub main() {
$ch: Chan[Int] = channel()
hatch {
for $n in [1, 2, 3] {
$ch.send($n * $n)
}
$ch.close()
}
mut $sum = 0
for $sq in $ch {
$sum = $sum + $sq
}
print("sum of squares: $sum")
}
That prints 14. On top of this sit generics, first-class regex with named captures ($line ~~ /(?<user>\w+)@.../), and PEG grammars - a Raku-flavored ambition on a statically typed, compiled base. There is no REPL yet at v0.1-alpha; the interpreter on a scratch file is the stand-in, the same bargain Go and Rust quietly made.
What the three doors reveal
Line them up and a pattern falls out. The scripting languages collapse build and run into one verb and treat the REPL as either central (Python) or absent-by-tradition (Perl). The typed functional languages make the REPL a design tool, somewhere you negotiate with the type checker before you commit. The compiled systems languages spend their care on the build artifact and shrug at the prompt. guji is the interesting bet: a compiler that wants the immediacy of a script, the safety of ML, and the texture of Perl, with interpreter-compiler parity standing in for the REPL it has not grown yet. Which door matters most is really a question about what you do all day - and every one of these languages has already answered it for you.