Variables, Binding & Basic Types
The same tiny program in seven languages: bind four immutable values of the four basic scalar types (an Int, a Float, a Str, and a Bool), then use one mutable accumulator to sum 1..count and print a summary. Watch two axes vary: how mutability is opted into (immutable-by-default with mut/let/ref, versus mutable-by-default), and how a value's type is signalled — a leading sigil that is part of the name ($, @, % in guji, Perl, and Raku) versus a bare name whose type lives only in a declaration or annotation (Go, OCaml, Rust, Python).
“As pointed out in a followup, Real Perl Programmers prefer things to be visually distinct.” — Larry Wall
sub main(): Int {
# Immutable bindings; the sigil ($) is part of the name,
# types (Int, Float, Str, Bool) are inferred.
$count = 42
$pi = 3.14
$name = "ada"
$active = true
# Mutation is opt-in with `mut`; everything else can't be reassigned.
mut $total = 0
for $n in 1..$count {
$total = $total + $n
}
print("name=$name active=$active pi=$pi")
print("sum of 1..$count = $total")
0
}Bindings are immutable by default — $total needs mut to be reassigned, while $count/$pi/$name/$active cannot change. The $ sigil is part of the name and invariant; the static types Int/Float/Str/Bool are inferred, so no annotation is written. Double-quoted strings interpolate $name directly, and main returns its Int exit code (0).
package main
import "fmt"
func main() {
// Typed, immutable-ish: const for compile-time constants,
// var for the rest. Types are explicit or inferred with :=.
const count int = 42
const pi float64 = 3.14
name := "ada"
active := true
total := 0 // ordinary variables are mutable
for n := 1; n <= count; n++ {
total += n
}
fmt.Printf("name=%s active=%t pi=%g\n", name, active, pi)
fmt.Printf("sum of 1..%d = %d\n", count, total)
}Go variables are mutable by default; true immutability is reserved for const, which works only for compile-time constants like count and pi. The := short form infers the type from the initializer (name is string, active is bool), while const count int shows the explicit annotation. fmt.Printf verbs (%s, %t, %g, %d) are type-specific, so the types stay visible at the call site.
let () =
(* `let` bindings are immutable values, not variables. *)
let count = 42 in (* int *)
let pi = 3.14 in (* float *)
let name = "ada" in (* string *)
let active = true in (* bool *)
(* Mutation needs an explicit mutable cell: a `ref`. *)
let total = ref 0 in
for n = 1 to count do
total := !total + n
done;
Printf.printf "name=%s active=%b pi=%g\n" name active pi;
Printf.printf "sum of 1..%d = %d\n" count !totalIn OCaml let introduces an immutable binding, and types are inferred by Hindley–Milner — count : int, pi : float, name : string, active : bool need no annotation. Genuine mutation requires an explicit ref cell, written ref 0, read with !total, and updated with :=. Printf.printf is type-checked against its format string (%d int, %g float, %b bool, %s string).
use strict;
use warnings;
# Sigils mark shape: $ for a scalar. `my` declares a lexical;
# Perl scalars are typeless and hold any kind of value.
my $count = 42;
my $pi = 3.14;
my $name = "ada";
my $active = 1; # no native Bool: 1 is true, 0/"" is false
my $total = 0;
$total += $_ for 1 .. $count;
printf "name=%s active=%s pi=%s\n", $name, $active ? "true" : "false", $pi;
print "sum of 1..$count = $total\n";Every variable wears a sigil ($ for a scalar) that is part of the name, and my gives it lexical scope. Perl scalars are dynamically typed and mutable by default — the same $total is happily reassigned, and there is no distinct boolean type, so 1/0 (or the empty string) stand in. Strings interpolate $name and $count directly inside double quotes.
# Sigils are part of the name; optional type constraints add static checks.
my Int $count = 42;
my Rat $pi = 3.14; # decimal literals are exact rationals (Rat)
my Str $name = "ada";
my Bool $active = True;
# `my` bindings are mutable; immutability is opt-in with the `is readonly`
# trait or by binding with `:=`.
my $total = [+] 1 .. $count; # reduce 1..count with the + operator
say "name=$name active=$active pi=$pi";
say "sum of 1..$count = $total";Raku keeps Perl's invariant sigils but adds optional type constraints (my Int $count) that the runtime enforces, and a real Bool type with True/False. A decimal literal like 3.14 is an exact Rat (rational), not a float. The whole sum collapses to [+] 1 .. $count — the [ ] reduction metaoperator folds + over the range — and say stringifies each value.
fn main() {
// `let` bindings are immutable by default; types are inferred.
let count: i64 = 42;
let pi = 3.14_f64;
let name = "ada";
let active = true;
// Opt into mutation with `mut`.
let mut total: i64 = 0;
for n in 1..=count {
total += n;
}
// Idiomatically: let total: i64 = (1..=count).sum();
println!("name={name} active={active} pi={pi}");
println!("sum of 1..{count} = {total}");
}Rust bindings are immutable by default; total needs let mut before it can be reassigned, mirroring guji's mut. Types are inferred but can be annotated (i64, f64), and 1..=count is an inclusive range. The captured-identifier form println!("{count}") interpolates a named binding directly, and the inclusive range also has a .sum() that would replace the explicit loop.
# Names are untyped at runtime; annotations are optional hints.
count: int = 42
pi: float = 3.14
name: str = "ada"
active: bool = True
total = 0 # every binding is mutable; there is no `const`
for n in range(1, count + 1):
total += n
print(f"name={name} active={active} pi={pi}")
print(f"sum of 1..{count} = {total}")Python names are mutable by default and dynamically typed — the : int / : float / : str / : bool annotations are optional hints checked by tools, never at runtime. range(1, count + 1) is half-open, so + 1 makes the sum inclusive. f-strings interpolate {name} and {count} the way guji, Perl, and Raku interpolate $name. The whole loop could be sum(range(1, count + 1)).