I think you've succinctly described why iterator/functional style feels safer to me. The control flow and state management is very clear and self-contained. But... it's only clear once you've internalized the vocabulary of functional iterator adapters -- it really ends up being a language in and of itself.
If you don't know the vocabulary, it can be understandably confusing and obtuse. I learned this style via Ruby, where it's quite popular and idiomatic... when I first saw it, I was pretty skeptical, but have come to prefer it in many situations. And yeah, sometimes the vocabulary isn't a good fit for the problem space, but in many cases it is!
I find I make fewer mistakes with functional style transformations, especially around boundary conditions and state management. (of course, this is not a scientifically rigorous statement )
And to be clear, since this is is a Rust forum, by safer I really mean: more likely to do what I intended.
I agree with @Yandros's sentiment about scoping. Personally speaking, a pretty good deal of the things I do to clean up code is motivated by reducing the number of variables in a given scope.
But notice too that it is possible to limit the scope of state in imperative code as well:
let sum = {
let mut sum : u32 = 0;
let mut state = (0, 1);
loop {
state = (state.1, state.0 + state.1);
if state.0 >= 4000000 { break sum; }
if state.0 % 2 == 0 { sum += state.0 }
}
};
Is this a preference against functional programming or just you can't see the use case? One of the reasons that I like Rust comparing to functional languages is that it can support both.
I think I'm pragmatic enough. If imperative is easier, then I'll go with the loop. However, it means that you probably only need the Fibonacci series for one particular time in all of your program. If that's the case, then certainly go with the loop.
There is a reasoning behind the iterator/functional model and it enable you to do stuff that would be hard/impossible with the loop.
First, you start by defining a general fibonnaci sequence function
fn fib_seq() -> std::iter::RepeatWith<impl FnMut() -> u32> {
std::iter::repeat_with({
let mut state = (1, 1);
move || {
state = (state.1, state.0 + state.1);
state.0
}
})
}
This function returns an Iterator. Iterators in Rust are both lazy and zero-cost. So they won't execute unless they need to and also they are as good as the loop.
This iterator pattern allows you to use your code in different ways. Let's say you want to take the first 99 fib numbers and calculate their sum
let sum: u32 = fib_seq().take_while(|&x| x < 100).sum();
Let's say you want to take the first 99 numbers that are even and take calculate their sum
let sum_filter: u32 = fib_seq()
.take_while(|&x| x < 100)
.filter(|x| x % 2 == 0)
.sum();
This is not possible to do with imperative code without code repetition and making your code more complex.
I hope you can start to see how functional code can be useful
Thanks for that -- I learned from it! I'm still relatively new to Rust, so I have some questions.
First, would it be better to have the signature be less specific about the return type, i.e. impl Iterator<Item = u32> ? Or is there some reason why you made it more explicit?
Second, the state could have been outside of the repeat_with, so that you don't need to make a separate block for it, e.g.
Or do you think it is better style to try to put state captured by the closure in a separate block immediately around the closure?
Also, I was wondering why we have to say impl in impl Iterator<Item = u32> ? I understand that that means "some type that implements this trait," but what else could Iterator<Item = u32> mean? I don't see how the impl adds anything. With other languages I use that support interfaces, you just use the interface as if it were a type, without a special keyword. Why does Rust require the keyword?
If you are just writing that fib function, then keeping it outside is fine. But if things get longer, then keeping it in a tight scope makes it easier to reason about the lifetime of the variables. i.e. We can know that nums will only be used in the closure, and no where else.
Maybe one does not need to know too much to understand the code, if the said code is structured better:
fn main() {
let sum: u32 = fibonacci()
.take_while(|&x| x < 4_000_000)
.filter(|x| x % 2 == 0)
.sum();
println!("{}", sum);
}
fn fibonacci() -> impl Iterator<Item = u32> {
let mut state = (0, 1);
std::iter::repeat_with(move || {
state = (state.1, state.0 + state.1);
state.0
})
}
Never underestimate the power of abstraction and the might of "extract into a function" trick. To be honest, I find the "old school way" way too convoluted for the simple task presented in the topic. No offence, but I would not be pleased to find code written like that by a coworker. On the other hand, the iterator approach reads pretty much as the description of the problem presented by @thor314. For a trained rustacean, the code actually reads faster than that. Basic iterator knowledge is pretty much default here anyway.
FWIW, I consider mem::replace to be perhaps the most fundamentally-Rust thing in existence (though mem::take is close), and have no desire to eliminate it from things, even in cases like this where Copy types mean you can cheat a bit. (Change this to BigInteger and see how things change, for example.)
I'm far more sympathetic to "I don't feel like the combination of iterator adapters is worth it here" (though of course that's not the point in this thread) than to eliminating core rust things like replace and move ||.
I still don't get the need to have to talk about memory (mem::...) as if we were doing some memcpy in C or whatever.
On the one hand we are in some high level abstract world of iterators, lambdas, filters, tuples, even could old variables and such, and then boom! we are talking about memory replacement. What is up with that?
Well I can understand that it is confusing if you missed that part of the solution. I'm not sure where else you'd put replace than in the mem module? I don't think it's dissimilar to memcpy; they both just move some values around.
But that is the thing, in a high level language we have variables, with names and types and sizes and such. We have operators like "=" to move them around. Why not use them? Why have to talk about memory? You know, that anonymous array of bytes we work in as dealt with by memcpy and friends in C.
I mean in this case the replace was used to get around this:
move || {
let fib = state.0;
state = (state.1, state.0 + state.1);
fib
}
And nothing wrong with this, but what comes to my mind when I see the replace compared to the above is "huh that's neat". I have always felt annoyed by having to take things out then put other things in, and then using the thing I took out. I try to avoid juggling values too much.
Brilliant, I love it. It clearly states what the intention is. Without the need for a std library call and the extra conceptual overhead baggage.
Perhaps what I don't understand in this conversation is why obfuscation and added complexity seem to be the preferred solutions. It's a neat trick and all but really.
I start to worry about such things when it would be clearer to write the code for the given problem in assembler than whatever high level language we are talking about at the time.
I mean all I really wanted was to assign a new value and return the old one. There's a function that does exactly this. Why should I juggle values around instead of using that? I understand that I am using additional vocabulary, but once you have that vocabulary I think it describes what I want to do more directly.
Like, my alternative consists of three actions in my head, while the replace version consists of one.
The problem you're having is that this is not obfuscation and added complexity, it just a different set of abstractions. Just because you have a hard time reading it doesn't make it obfuscated; I can't read Mandarin, but that doesn't mean Mandarin is inherently obfuscating anything. It means I'm biased and ignorant.
And given that most proponents of functional programming will describe it as holding unparallelled elegance, beauty, and clarity, it is especially striking to hear it described as obfuscating or inelegant, but perhaps not surprising given that you've already said you have little experience with functional programming.
It is unfamiliar to you. That means you are not in a position to evaluate it. It doesn't mean you can dismiss it without listening to the people who do understand it -- certainly not with any sense of objectivity.
This thread is now a week old and involved me in a lot of head actions to fathom what is going on. And why various people would do it whatever way they suggested.
Where as, my rude and crude solution using "loop" above requires minimal head actions for anyone who understands the basics of expressions, conditions and loops in almost any other programming language.
I think that the thing about functional programming is that fundamentally it's a vocabulary that allows you to talk about what you want the computer to do, without directly having to care as much about how it does it.
I do not feel very strongly about mem::replace compared to iterators, but I do feel that it illustrates the same reasons that makes them intuitive to me and less so to you.