086 — Space Age
Tutorial Video
Text description (accessibility)
This video demonstrates the "086 — Space Age" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Given an age in seconds, calculate how old a person would be on each planet in the solar system, based on each planet's orbital period relative to Earth's year (31,557,600 seconds). Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
Given an age in seconds, calculate how old a person would be on each planet in the solar system, based on each planet's orbital period relative to Earth's year (31,557,600 seconds). Implement age_on(planet: Planet, seconds: f64) -> f64 using both a match-based orbital period table and a lookup-array alternative.
🎯 Learning Outcomes
Copy enums to represent fixed domain sets (Planet) with no heap allocationconst array Planet::ALL for iterating over all variantsmatch to return a compile-time float constant per variantage = seconds / (earth_year * orbital_period) cleanlymatch-based dispatch with OCaml's equivalent function matchCode Example
#![allow(clippy::all)]
/// Space Age — Float Computation with Variants
///
/// Ownership: Planet is Copy (enum with no data). All computations use f64 (Copy).
#[derive(Debug, Clone, Copy)]
pub enum Planet {
Mercury,
Venus,
Earth,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
}
impl Planet {
pub fn orbital_period(self) -> f64 {
match self {
Planet::Mercury => 0.2408467,
Planet::Venus => 0.61519726,
Planet::Earth => 1.0,
Planet::Mars => 1.8808158,
Planet::Jupiter => 11.862615,
Planet::Saturn => 29.447498,
Planet::Uranus => 84.016846,
Planet::Neptune => 164.79132,
}
}
pub const ALL: [Planet; 8] = [
Planet::Mercury,
Planet::Venus,
Planet::Earth,
Planet::Mars,
Planet::Jupiter,
Planet::Saturn,
Planet::Uranus,
Planet::Neptune,
];
}
const EARTH_YEAR_SECONDS: f64 = 31_557_600.0;
pub fn age_on(planet: Planet, seconds: f64) -> f64 {
seconds / (EARTH_YEAR_SECONDS * planet.orbital_period())
}
/// Version 2: Using a lookup table instead of match
pub fn age_on_table(planet_index: usize, seconds: f64) -> f64 {
const PERIODS: [f64; 8] = [
0.2408467, 0.61519726, 1.0, 1.8808158, 11.862615, 29.447498, 84.016846, 164.79132,
];
seconds / (EARTH_YEAR_SECONDS * PERIODS[planet_index])
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 0.01
}
#[test]
fn test_earth() {
assert!(approx(age_on(Planet::Earth, 1_000_000_000.0), 31.69));
}
#[test]
fn test_mercury() {
assert!(approx(age_on(Planet::Mercury, 1_000_000_000.0), 131.56));
}
#[test]
fn test_neptune() {
assert!(approx(age_on(Planet::Neptune, 1_000_000_000.0), 0.19));
}
#[test]
fn test_all_planets() {
for &p in &Planet::ALL {
let age = age_on(p, EARTH_YEAR_SECONDS);
assert!(approx(age, 1.0 / p.orbital_period()));
}
}
#[test]
fn test_table_version() {
assert!(approx(age_on_table(0, 1_000_000_000.0), 131.56));
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Variant copy | #[derive(Copy)] | Value type by default |
| Float ops | *, / (same as integer) | *., /. (distinct operators) |
| Constant | const EARTH_YEAR_SECONDS: f64 | let earth_year_seconds = … |
| All variants | const ALL: [Planet; 8] | Manual list or macro |
| Dispatch | match self { … } | function \| Mercury -> … |
| Code size | ~40 lines | ~15 lines |
The Planet::ALL constant is a useful pattern for any enum where you need to iterate over all variants. Without a derive macro like strum, Rust requires defining it manually — but as a const array it is zero-overhead and compile-time verified.
OCaml Approach
OCaml's version is nearly identical: a type planet variant, an orbital_period function using function, and age_on computing the quotient. The OCaml float operators are suffixed (/., *.), making the arithmetic explicit. There is no Copy concept — variants are value types by default. OCaml lacks a const array of all constructors, so exhaustive iteration would require a manually defined list.
Full Source
#![allow(clippy::all)]
/// Space Age — Float Computation with Variants
///
/// Ownership: Planet is Copy (enum with no data). All computations use f64 (Copy).
#[derive(Debug, Clone, Copy)]
pub enum Planet {
Mercury,
Venus,
Earth,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
}
impl Planet {
pub fn orbital_period(self) -> f64 {
match self {
Planet::Mercury => 0.2408467,
Planet::Venus => 0.61519726,
Planet::Earth => 1.0,
Planet::Mars => 1.8808158,
Planet::Jupiter => 11.862615,
Planet::Saturn => 29.447498,
Planet::Uranus => 84.016846,
Planet::Neptune => 164.79132,
}
}
pub const ALL: [Planet; 8] = [
Planet::Mercury,
Planet::Venus,
Planet::Earth,
Planet::Mars,
Planet::Jupiter,
Planet::Saturn,
Planet::Uranus,
Planet::Neptune,
];
}
const EARTH_YEAR_SECONDS: f64 = 31_557_600.0;
pub fn age_on(planet: Planet, seconds: f64) -> f64 {
seconds / (EARTH_YEAR_SECONDS * planet.orbital_period())
}
/// Version 2: Using a lookup table instead of match
pub fn age_on_table(planet_index: usize, seconds: f64) -> f64 {
const PERIODS: [f64; 8] = [
0.2408467, 0.61519726, 1.0, 1.8808158, 11.862615, 29.447498, 84.016846, 164.79132,
];
seconds / (EARTH_YEAR_SECONDS * PERIODS[planet_index])
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 0.01
}
#[test]
fn test_earth() {
assert!(approx(age_on(Planet::Earth, 1_000_000_000.0), 31.69));
}
#[test]
fn test_mercury() {
assert!(approx(age_on(Planet::Mercury, 1_000_000_000.0), 131.56));
}
#[test]
fn test_neptune() {
assert!(approx(age_on(Planet::Neptune, 1_000_000_000.0), 0.19));
}
#[test]
fn test_all_planets() {
for &p in &Planet::ALL {
let age = age_on(p, EARTH_YEAR_SECONDS);
assert!(approx(age, 1.0 / p.orbital_period()));
}
}
#[test]
fn test_table_version() {
assert!(approx(age_on_table(0, 1_000_000_000.0), 131.56));
}
}#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 0.01
}
#[test]
fn test_earth() {
assert!(approx(age_on(Planet::Earth, 1_000_000_000.0), 31.69));
}
#[test]
fn test_mercury() {
assert!(approx(age_on(Planet::Mercury, 1_000_000_000.0), 131.56));
}
#[test]
fn test_neptune() {
assert!(approx(age_on(Planet::Neptune, 1_000_000_000.0), 0.19));
}
#[test]
fn test_all_planets() {
for &p in &Planet::ALL {
let age = age_on(p, EARTH_YEAR_SECONDS);
assert!(approx(age, 1.0 / p.orbital_period()));
}
}
#[test]
fn test_table_version() {
assert!(approx(age_on_table(0, 1_000_000_000.0), 131.56));
}
}
Deep Comparison
Space Age — Comparison
Core Insight
Simple enum + pattern matching translates almost identically between OCaml and Rust. When all data is Copy (enums, floats), ownership is invisible. The languages converge on the same clean pattern.
OCaml Approach
type planet = Mercury | Venus | ... — simple variant typelet orbital_period = function | Mercury -> 0.24... — pattern match function/. for float division (separate operator from integer /)Rust Approach
enum Planet { Mercury, Venus, ... } with #[derive(Copy, Clone)]impl Planet { fn orbital_period(self) -> f64 { match self { ... } } }/ for both int and float divisionconst ALL array for iteration over all variantsComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Variant | type planet = Mercury \| ... | enum Planet { Mercury, ... } |
| Match | function \| pat -> ... | match self { pat => ... } |
| Float ops | /. *. | / * |
| Constants | let x = 31557600.0 | const X: f64 = 31_557_600.0 |
| Iterate variants | Manual list | const ALL array |
Learner Notes
const in Rust is compile-time; OCaml let at module level is similar_ separators: 31_557_600.0Exercises
Display implementation for Planet that returns the planet name as a string.age_on_all(seconds: f64) -> [(Planet, f64); 8] that returns the age on every planet.Planet::ALL.iter().min_by.Pluto variant (orbital period 247.92065 Earth years) even though it is not officially a planet, and verify the formula still works.all_planets : planet list and write age_on_all seconds using List.map.