So, I thought a nice goal for 2026 was to finally get to know rust better and I'm doing quite okay I think. Since I want to do rust and not my-usual-mash-up-of-languages in rust I thought I'd ask your opinions.
In some languages you absolutely have to define your items first (as in, above the first usage) with some minor exceptions (e.g. ocaml) and in others you can access them as long as their "associated value" is defined before usage (e.g. js). In rust it seems you can use whatever is statically know (?).
So compare the following two examples and tell me which one you think is more idiomatic:
impl Foo for Bar {
fn f() -> Y {
fn some_complex_predicate(x: &X) -> bool;
fn some_specific_function(x: &X) -> Y;
match foo {
A(x) if some_complex_predicate(&x) => some_specific_function(x)
}
}
}
which forces a reader to at least skim over some code that isn't immediately relevant
or
impl Foo for Bar {
fn f() -> Y {
return match foo {
A(x) if some_complex_predicate(&x) => some_specific_function(x)
};
fn some_complex_predicate(x: &X) -> bool;
fn some_specific_function(x: &X) -> Y;
}
}
which uses early returns to sort of mimic haskells where. If rust didn't have where as a keyword with similar semantics already I'd pick the first option, but since it does... what's your opinion here?
Note: please keep in mind that I just started with rust a week ago after reading the book, rust by example and a bit of the nomicon. Maybe I'm missing some crucial piece to e.g. write "open traits" which would allow defining the functions outside of f.
Apart from the point that Rust's where is only for trait bounds, not local definitions like Haskell's, the problem with your second example is that, people who have worked for a good amount of time, now instinctively read code after return as unreachable. Your helper definitions will trigger a mental "wait, why is this here?" reaction. But I have a personal preference that may get similar reactions too (see below) :'D
From what I have seen, people use one of the following:
impl Foo for Bar {
fn f() -> Y {
match foo {
A(x) if some_complex_predicate(&x) => some_specific_function(x),
}
}
}
fn some_complex_predicate(x: &X) -> bool { ... }
fn some_specific_function(x: &X) -> Y { ... }
Closures (for short, context-capturing logic)
impl Foo for Bar {
fn f() -> Y {
let threshold = compute_threshold();
let some_complex_predicate = |x: &X| -> bool { x.value > threshold };
let some_specific_function = |x: &X| -> Y { ... };
match foo {
A(x) if some_complex_predicate(&x) => some_specific_function(x),
}
}
}
I really dislike free functions, just a personal thing and it it's not a common pattern in Rust codebases. So I usually just make lots of Util unit structs for module level helpers for namespacing. And often it is helpful, because I can export them to other files if needed as a struct (I mean you can still do that with a module directly, which is what most people do, but like I said, personal preference :'D)
impl Foo for Bar {
fn f() -> Y {
match foo {
A(x) if FooUtils::some_complex_predicate(&x) => FooUtils::some_specific_function(x),
}
}
}
struct FooUtils;
impl FooUtils {
fn some_complex_predicate(x: &X) -> bool { ... }
fn some_specific_function(x: &X) -> Y { ... }
}
Nested function items are harder to test, which is probably one of the reasons why people don't use that pattern.
Neither is particularly idiomatic, for a few reasons:
Trait methods almost always either take a self argument (perhaps &self, &mut self, or some other variant), or return Self, or both. It's entirely legal to have methods on a trait that do neither, such as your <Bar as Foo>::f() method, but it's unusual and will burn a certain amount of your expressiveness budget with most readers.
Defining functions inside of functions is unusual as well. It's legal, and there can be good reasons for it, but in general I'd expect the helpers some_complex_predicate and some_specific_function to be (private) top-level functions in the module, or to be methods in an impl Bar block, depending on their relationship to the Bar type (and the <Bar as Foo>::f() method). I might even expect to see them defined as methods on X.
In rust it seems you can use whatever is statically know (?).
The unit of compilation in Rust is the whole crate, and the compiler does name resolution in multiple passes so that names defined anywhere in the crate can be referenced anywhere else within the crate, subject to structural visibility rules but not to ordering rules. There are probably some niche exceptions to this, but for most practical purposes, declaration order is a matter of choice and style, rather than a technical constraint.
people who have worked for a good amount of time, now instinctively read code after return as unreachable
with rust, you mean, right?
better [...] which makes for a more natural control flow.
debatable in general, but if you say that's what's idiomatic in rust I'll happily accept that. Though it would make me wonder why the "let else" syntax exists then.
I really dislike free functions
very much the same, simply because I don't know (yet?) where to put them. Maybe I should just have "one struct per file" but that feels so javaesque
Trait methods almost always either take a self argument (perhaps &self , &mut self , or some other variant), or return Self , or both
true, but not the point here Then again I'm not sure why I chose to demo this with a trait impl in the first place would have worked just the same with a simple function... oh well
Closures (for short, context-capturing logic)
I would probably go with that right now. Let's see how this plays out.
Anyhow, thank you all for answering, it's really great to learn something new again