Types & Records
The same record in all seven languages: define a Point type with integer x and y fields, construct one, then translate it by (3, 4) to get a second point. Watch how a record type is declared (a class/struct/record/blessed hash) and whether updates mutate or copy: the functional-first languages here return a brand-new Point rather than changing the original. Notice too how much boilerplate each needs — a derived constructor, explicit field accessors, or just an annotated field list.
# A `class` is guji's product type: it "has all of" these fields at once.
class Point {
has $.x: Int # `$.` makes the field publicly readable
has $.y: Int
# A method takes `$self`; immutability means it returns a NEW Point.
sub translated($self, $dx: Int, $dy: Int): Point {
Point(x: $self.x + $dx, y: $self.y + $dy)
}
}
sub main(): Int {
$p = Point(x: 1, y: 2) # construct with named fields
$q = $p.translated(3, 4) # data-first method call
print("p = ({ $p.x }, { $p.y })")
print("q = ({ $q.x }, { $q.y })")
0
}A class is the product type — it has all of x and y simultaneously — with each field declared by has and a $. twigil that makes it publicly readable. Construction names the fields (Point(x: 1, y: 2)); a method's first parameter is $self, and because bindings are immutable by default, translated returns a fresh Point instead of mutating in place. Field access is $p.x, but inside a string it must be wrapped as { $p.x } since a bare $p.x interpolates only $p.
package main
import "fmt"
// A struct is Go's record: a fixed set of named, typed fields.
type Point struct {
X, Y int
}
// A value receiver returns a new Point rather than mutating the original.
func (p Point) Translated(dx, dy int) Point {
return Point{X: p.X + dx, Y: p.Y + dy}
}
func main() {
p := Point{X: 1, Y: 2}
q := p.Translated(3, 4)
fmt.Printf("p = (%d, %d)\n", p.X, p.Y)
fmt.Printf("q = (%d, %d)\n", q.X, q.Y)
}Go's record is a struct with named fields; capitalized field names (X, Y) are exported, the only visibility control Go offers. The method Translated uses a value receiver (p Point), so it gets a copy and returns a new Point — Go would mutate in place only with a pointer receiver (p *Point). There is no method syntax inside the type body: methods are declared separately with the func (recv T) form.
(* A record type: a fixed set of named, typed, immutable fields. *)
type point = { x : int; y : int }
(* Functional update: { p with ... } copies p, overriding some fields. *)
let translated p dx dy = { x = p.x + dx; y = p.y + dy }
let () =
let p = { x = 1; y = 2 } in
let q = translated p 3 4 in
Printf.printf "p = (%d, %d)\n" p.x p.y;
Printf.printf "q = (%d, %d)\n" q.x q.yOCaml records declare named, typed fields and are immutable by default, so a field can only be changed by building a new record. Field access is p.x, and the functional-update form { p with x = ... } copies a record overriding chosen fields — here written out fully as { x = ...; y = ... }. Types are inferred everywhere: translated needs no annotations, the compiler reads point from the field names alone.
use strict;
use warnings;
# A Point record: a blessed hash ref with x and y fields.
package Point {
sub new {
my ($class, %args) = @_;
return bless { x => $args{x}, y => $args{y} }, $class;
}
sub x { $_[0]{x} }
sub y { $_[0]{y} }
# Returns a NEW Point rather than mutating $self.
sub translated {
my ($self, $dx, $dy) = @_;
return Point->new(x => $self->x + $dx, y => $self->y + $dy);
}
}
my $p = Point->new(x => 1, y => 2);
my $q = $p->translated(3, 4);
printf "p = (%d, %d)\n", $p->x, $p->y;
printf "q = (%d, %d)\n", $q->x, $q->y;Classic Perl has no record syntax: an object is a hash ref blessed into a package, and accessors like x/y are hand-written subs. Construction goes through a new that unpacks named arguments from @_, and a method's first argument is always the invocant $self. Modern Perl would lean on Moose/Object::Pad or the new built-in class feature for fields and accessors, but the blessed-hash idiom shows the bare mechanism.
class Point {
has Int $.x; # `$.` generates a public read accessor
has Int $.y;
# `self.clone` copies; named args override the chosen fields.
method translated(Int $dx, Int $dy --> Point) {
self.clone(x => $.x + $dx, y => $.y + $dy)
}
}
my $p = Point.new(x => 1, y => 2);
my $q = $p.translated(3, 4);
say "p = ({ $p.x }, { $p.y })";
say "q = ({ $q.x }, { $q.y })";Raku is the direct ancestor of guji's syntax: has Int $.x declares a typed field whose $. twigil auto-generates a public accessor, and Point.new(...) constructs from named arguments. method translated carries an implicit self, and self.clone(...) copies the object overriding chosen fields, so the original is untouched. The { ... } interpolation in strings — and the whole sigil/twigil system — is exactly what guji borrowed.
// A struct: a record of named, typed fields. Derive Debug to print it.
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
impl Point {
// Takes self by value and returns a new Point via struct-update syntax.
fn translated(self, dx: i32, dy: i32) -> Point {
Point { x: self.x + dx, y: self.y + dy }
}
}
fn main() {
let p = Point { x: 1, y: 2 };
let q = p.translated(3, 4);
println!("p = ({}, {})", p.x, p.y);
println!("q = ({}, {})", q.x, q.y);
}Rust's record is a struct; methods live in a separate impl block, keeping data and behavior textually distinct. #[derive(Debug, Clone, Copy)] auto-generates printing and copy semantics, and because Point is Copy, taking self by value here leaves the caller's p fully usable afterward. Struct-update syntax Point { x: ..., ..self } could copy the remaining fields, but with only two fields it is clearest to name both.
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Point:
x: int
y: int
def translated(self, dx: int, dy: int) -> "Point":
return replace(self, x=self.x + dx, y=self.y + dy)
p = Point(x=1, y=2)
q = p.translated(3, 4)
print(f"p = ({p.x}, {p.y})")
print(f"q = ({q.x}, {q.y})")A @dataclass turns an annotated field list into a full record — generating __init__, __repr__, and equality for free. frozen=True makes instances immutable, so translated uses dataclasses.replace to produce a copy with some fields changed rather than reassigning attributes. The type hints (x: int) drive the generated __init__ but are not enforced at runtime; they document the record's shape the way the other languages' static types do.