DEV Community

Cover image for Python Idioms in Rust
Benjamin Congdon
Benjamin Congdon

Posted on • Originally published at benjamincongdon.me on

Python Idioms in Rust

I’ve been going through a period of programming language wanderlust over the past couple months. Recently, I’ve been quite interested in Rust. Coming from Python, I’ve found a lot of Rust’s language features to be quite powerful.

Earlier this year, I dug deep into Go. For the most part, I enjoy Go: It’s a simple language that can be learned in a few hours (literally). Its structure promotes good programming practices and it has a wonderful ecosystem.

However, in Go, I felt limited by the expressiveness of the language. Its much-bemoaned lack of generics is less disruptive than one would expect but makes clever functional-programming tricks essentially impossible. That’s fine — it’s not what Go was designed for — but as a result, I found Go a bit less “fun” to work in.

While Rust does have additional complications coming from it’s infamous borrow checker and the complexity of generics, I’ve found that what you gain in expressiveness is worth the tradeoff.

In fact, I found Rust to be rather Pythonic1. ✨ Sure, you have to be careful about types and ownership, but there is a strong case to be made that Rust shares many of the strengths of Python’s syntax.

After doing a bit of work in Rust, I identified some idioms2 common to Rust and Python. These common features are all things that I enjoy using in Python, and I’ve found to be useful while coming up-to-speed on Rust.

Enumerate

A common pattern in when iterating over lists in Python is to use enumerate to have get both the index and value of list elements:

arr = ['a', 'b', 'c']
for idx, val in enumerate(arr):
    print("{}, {}".format(idx, val))
# 0, a
# 1, b
# 2, c
Enter fullscreen mode Exit fullscreen mode

Rust iterators also support enumerate! (And you don’t even have to learn a new name for the same functionality)

let arr = vec!['a', 'b', 'c'];
for (idx, val) in arr.iter().enumerate() {
    println!("{}, {}", idx, val);
}
Enter fullscreen mode Exit fullscreen mode

Zip

Another function on iterators that you see pretty frequently in Python is zip, which combines 2 iterators element-wise, creating an iterator of tuples with an element of each list.

letters = ['a', 'b', 'c']
numbers = [1, 2, 3]
for l, n in zip(letters, numbers):
    print("{}, {}".format(l, n))
# a, 1
# b, 2
# c, 3
Enter fullscreen mode Exit fullscreen mode

Again, Rust iterators also support zip:

let letters = vec!['a', 'b', 'c'];
let numbers = vec![1, 2, 3];
for zipped in numbers.iter().zip(letters) {
    println!("{:?}", zipped);
}
// (1, 'a')
// (2, 'b')
// (3, 'c')
Enter fullscreen mode Exit fullscreen mode

Like Python’s zip, Rust’s zip halts when either of the iterators is exhausted. This allows you to do cool tricks with infinite iterators:

let start_at_10 = 10..;
let letters = vec!['a', 'b', 'c'];
for zipped in start_at_10.zip(letters) {
    println!("{:?}", zipped);
}
// (10, 'a')
// (11, 'b')
// (12, 'c')
Enter fullscreen mode Exit fullscreen mode

Tuples

Rust has tuples! This is a feature that I sorely missed in Go. Yes, structs work just as well, but for “scripting” or prototype work, tuples are a great language feature.

Python’s tuple system is pretty flexible. Functionally, Python tuples act like immutable lists. Tuples can be aliased into separate variables via unpacking, and are indexed-into like lists.

foo = ('foo', 1, None)
a, b, c = foo
assert a == 'foo'
assert foo[0] == 'foo'

# Tuples are immutable in Python
tup = (1, 2, 3)
tup[0] = 4
# TypeError: 'tuple' object does not support item assignment
Enter fullscreen mode Exit fullscreen mode

Rust’s tuple feature is also quite powerful. Similar to Python, you can alias tuple values into separate variables using syntactic sugar, although in Rust this is called “destructuring” instead of “unpacking”.

let foo = ("foo", 1, None::<i32>);
let (a, b, c) = foo;
println!("{}", a); // "foo"
println!("{}", foo.0); // "foo"
Enter fullscreen mode Exit fullscreen mode

Also, Rust tuples aren’t indexed using square brackets (foo[0]), but rather by the dot operator (foo.0). This is a byproduct of the fact that Rust tuples are sized at compile-time, unlike Python tuples which are dynamic.

# Dynamically sized tuples in Python
tup = tuple(['foo' for _ in range(randint(1, 10))])
Enter fullscreen mode Exit fullscreen mode

Unlike in Python, Rust tuples can be made mutable:

let mut tup = (1, 2, 3);
tup.0 = 4;
Enter fullscreen mode Exit fullscreen mode

List Comprehensions (kind of)

List comprehensions are the bread-and-butter of Python syntactic sugar. It’s a super expressive way of generating/filtering lists.

evens_squared = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
Enter fullscreen mode Exit fullscreen mode

Rust doesn’t have list comprehensions per se, but Rust iterators do have map and filter, which allow you to perform similarly expressive “list” generation:

let evens_squared = (0..10)
    .filter(|x| x % 2 == 0)
    .map(|x| x * x)
    .collect();

println!("{:?}", evens_squared);
// [0, 4, 16, 36, 64]
Enter fullscreen mode Exit fullscreen mode

Personally, I like Rust’s extensive iterator functions even better than Python’s. I found functions like flat_map and scan (among others) to be quite useful. These iterator functions bring aspects of functional programming to Rust that I really enjoy.

Single-Line If

Neither Python nor Rust has ternary operators (which may be for the best). However, they both support single-line conditional expressions. This can be useful in certain conditions, but it’s really just syntactic sugar.

result = expensive_function() if some_condition else None

let result = if some_condition { expensive_function() } else { 0 }
Enter fullscreen mode Exit fullscreen mode

Lambda Functions

An oft complained-about language feature of Python is its lambda functions. They’re useful, but the syntax isn’t great. Other languages like Ruby and Javascript have more terse syntax for specifying closures or inline functions.

Python lambdas take a set of arguments and return the evaluation of the expression that follows the :

arr = [3, 10, 15]
sorted(arr, key=lambda x: abs(10-x))
# [10, 15, 3]
Enter fullscreen mode Exit fullscreen mode

Rust closures are similarly expressive. They use pipes (|) to hold arguments and the expression that follows is used as the return value.

let mut arr: Vec<i32> = vec![10, 15, 3];
arr.sort_by_key(|a| (10 - a).abs());
Enter fullscreen mode Exit fullscreen mode

Rust closures can also be multiline functions using brackets, which is quite handy:

let mut arr = vec![10, 15, 3];
arr.sort_by_key(|a| {
    let intermediate_value = some_function(a);
    intermediate_value + 10
});
Enter fullscreen mode Exit fullscreen mode

Both Python lambdas and Rust closures perform variable capture, so you can have access to variables that are in-scope where the lambda/closure is created:

a = 10
plus_a = lambda x: x + a
plus_a(5)
# 15
Enter fullscreen mode Exit fullscreen mode
let a = 10;
let plus_a = |x| x + a;
plus_a(5); // 15
Enter fullscreen mode Exit fullscreen mode

First-Class Functions

In Python, functions are essentially objects that are invoked with the __call__ magic function. They can be passed around like any other object and can even have attributes assigned to them. This can be useful for passing callback functions. While idiomatic Python doesn’t make extensive use of functions-as-variables, this feature comes in handy pretty often.

def plus_one(x):
    return x + 1

def map_on_list(arr, func):
    return [func(x) for x in arr]

arr = [1, 2, 3]
map_on_list(arr, plus_one)
# [2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Rust also has first-class functions. Functions and closures can be used as argument to other functions:

fn plus_one(x: i32) -> i32 {
    x + 1
}

fn map_on_vec(vec: &Vec<i32>, func: fn(i32) -> i32) -> Vec<i32>
{
    let mut new_vec = Vec::new();
    for i in 0..vec.len() {
        new_vec.push(func(vec[i]));
    }
    new_vec
}

let vec = vec![1, 2, 3];
println!("{:?}", map_on_vec(&vec, plus_one));
// [2, 3, 4]

// Equivalently:
println!("{:?}", map_on_vec(&vec, |x| x + 1));
Enter fullscreen mode Exit fullscreen mode

Interestingly, there are some subtle differences between Rust functions and closures. 3 In general, they can be treated as the same thing. Of course, functions are not objects in Rust, but a Rust struct can implement the Fn, FnMut, or FnOnce traits to similar effect of Python’s __call__ (though this is not recommended).

Conclusion

Hopefully, I’ve given a bit of a hint as to why a Python dev might be interested in picking up Rust (for aesthetic reasons, if nothing else!). There’s a definite learning curve to Rust, but it’s a speedy language that still remains quite expressive.

From a syntactic sense, I found Rust to be more similar to Python than Python is to Go. Using generics and type inference, you can end up writing Rust code that has only minimal “type boilerplate” and feels pretty similar to Python.

If you’re interested in looking into Rust, I’d highly recommend the following resources:

Cover: Pixabay

Footnotes

1. Armin Ronacher did a great talk about pairing Rust and Python that expands upon this notion of Rust being “Pythonic”

2. Some of these are probably better defined as syntactic sugar or language features. 😛

3. Ricardo Martins wrote a blog post about this distinction between functions and closures

Top comments (0)