Appendix B: Coming From Another Language
From Lux
Lux was built to be outgrown, and Rust is one of the three languages it was
built to be outgrown into. Most of what you learned transfers without
translation, because Lux took its data model from the same family Rust belongs
to: values that hold still unless you say otherwise, no null, a match that has
to cover every case, and a failure that is a value rather than a hidden
mechanism.
| Lux | Rust |
|---|---|
let x = 5 | let x = 5; |
var x = 5 | let mut x = 5; |
int, float, string, bool | i64 (or i32), f64, String, bool |
[int] | Vec<i64>, or &[i64] when borrowed |
func f(n: int) -> int { } | fn f(n: i64) -> i64 { } |
return n * 2 | n * 2 as the last line, or return n * 2; |
print("x is", x) | println!("x is {x}") |
length(xs) | xs.len() |
queue += 4 | queue.push(4); |
for x in xs | for x in &xs |
for i in 0..5 | for i in 0..5 |
struct Point { x: int } | struct Point { x: i64 } |
Point(x: 3, y: 4) | Point { x: 3, y: 4 } |
enum Shape { circle(radius: float) } | enum Shape { Circle(f64) } |
Shape.circle(radius: 2.0) | Shape::Circle(2.0) |
match s { circle(let r) => ... } | match s { Shape::Circle(r) => ..., } |
some(x), none | Some(x), None |
ok(x), err(reason) | Ok(x), Err(reason) |
Option<int>, Result<int, string> | Option<i64>, Result<i64, String> |
int(3.9), string(42) | 3.9 as i64, 42.to_string() |
parseInt(s) | s.parse::<i64>(), which gives a Result |
readFile(path) | std::fs::read_to_string(path) |
eprint(msg) | eprintln!("{msg}") |
func main() { } | fn main() { }, and it is required |
The mechanical differences are semicolons at the ends of statements, commas
between match arms, :: where Lux writes a dot for a namespace, CamelCase
variant names, and the type names being explicit about width. Fix those and most
Lux programs are nearly Rust.
Four differences are real.
Ownership. Lux copies values; assigning an array gives you an independent
array. Rust moves them, and the original becomes unusable. This is Chapter 5, and it is
the largest single difference between the two languages. When it frustrates you, .clone() is
the Lux behavior, spelled out loud.
Mutation is one word, and it is a different word. var becomes let mut,
and mut also appears on parameters, on references, and on methods. Lux asks
whether a name can be reassigned; Rust asks that, and also who is allowed to
modify a value through a borrow.
parseInt gives an Option, parse gives a Result. Rust tells you why
the parse failed. Use .ok() when you want the Lux shape.
Strings are bytes underneath. length(s) in Lux counts characters, and
s.len() in Rust counts bytes. Use s.chars().count() for the Lux answer.
Chapter 7 explains why Rust makes you choose.
You also have a tool. lux convert rust program.lux prints the Rust translation
of any Lux program you have written, and comparing the two is a fast way to
learn the mapping in the terms of code you already understand. The output is
deliberately literal, and it clones more than a Rust programmer would, for the
reason Chapter 5 gives.
From Python
| Python | Rust |
|---|---|
x = 5 | let mut x = 5;, or let x = 5; when it never changes |
def f(n): return n * 2 | fn f(n: i64) -> i64 { n * 2 } |
print(f"x is {x}") | println!("x is {x}"); |
[1, 2, 3] | vec![1, 2, 3] |
{"a": 1} | HashMap::from([("a", 1)]) |
{1, 2} | HashSet::from([1, 2]) |
(1, "two") | (1, "two") |
for x in xs: | for x in &xs { |
[f(x) for x in xs if p(x)] | xs.iter().filter(|x| p(x)).map(|x| f(x)).collect() |
len(xs) | xs.len() |
xs.append(v) | xs.push(v) |
d.get(k) | d.get(&k), an Option |
None | None, but the type must say Option<T> |
try / except | Result and ? |
raise | return Err(...), or panic! for a bug |
with open(p) as f: | scope ends, the file closes itself |
class C: | struct C plus impl C |
isinstance(x, C) | usually an enum and a match |
int("42") | "42".parse::<i64>(), a Result |
# type: ignore | no equivalent, and that is the point |
The adjustments that take longest: every type is fixed at compile time, integers
do not grow without bound, a dictionary lookup gives you an Option rather than
a KeyError, and there is no truthiness.
From JavaScript
| JavaScript | Rust |
|---|---|
const x = 5 | let x = 5; |
let x = 5 | let mut x = 5; |
function f(n) { return n * 2 } | fn f(n: i64) -> i64 { n * 2 } |
n => n * 2 | |n| n * 2 |
[1, 2, 3] | vec![1, 2, 3] |
{a: 1} as a record | a struct |
{} as a lookup table | a HashMap |
xs.map(f).filter(p) | xs.iter().map(f).filter(p).collect() |
xs.length | xs.len() |
null and undefined | Option<T>, and there is only one of it |
try / catch | Result and ? |
throw | return Err(...), or panic! for a bug |
=== | ==, which is the only kind there is |
JSON.parse | serde_json, a dependency |
async / await | async / await, with a runtime you choose |
npm install | cargo add |
The adjustments: numbers are not all floats, == compares values of the same
type and refuses anything else, there is no implicit conversion anywhere, and
the absence of a value has one spelling rather than two.