Debugging fluent style coding

I've been converting my code from loops to fluent style, and it led me to ask "Why did I ever program using loops?" Oh, yeah, my mother tongue is FORTRAN. (You people under 30 should Google it.) Now my problem is how I debug what is a just a single statement. Consider (for illustration only, neither example actually run)

let mut max = 0;
for element in elements.iter() {
    match element.val {
        Some(val) => {
            if element.is_active() && val > max { 
                max = val; 
            },
        None => ()
    }
}

versus

if let Some(max) = elements 
    .iter()
    .filter(|element| element.is_active())
    .max_by(|x, y| x.val.cmp(y.val)) {
} else {
    0
}

I find the fluent style easier to read and simpler to understand, plus it eliminates the need for a mutable variable. It even handles Option and Result for me, but how do I debug?

In the "for" style, I can set break points on the "if" statement and on any statements under the "if". In the fluent style, I can only set a break point on the "if let". I've done kludges, such as inserting a map that does a println!, but I'd prefer to use the debug features of my IDE. (If it helps, I've been editing in IntelliJ Community Edition and debugging with VSCode while I wait for debugging Rust in IntelliJ.)

7 Likes
if let Some(max) = elements 
    .iter()
    .filter(|element| element.is_active())
    .max_by(|x, y| x.val.cmp(y.val)) {
} else {
    0
}

There are better ways to write this example code, using max_by_key and using a unwrap_or(0) at the end instead of an if. Regarding your question a debugging help comes from using inspect:

4 Likes

Yes, of course, I should be using unwrap_or and max_by_key. The inspect() method is better than my map() kludge, but it's still println!. I was hoping there would be a way to set break points.

I just tried using lldb to set a breakpoint on the line with the filter operation and on the max_by operation. It seems to work flawlessly for me. I don't use neither IntelliJ or VSCode, but imagine that it is possible to use lldb as debugger in these IDEs, otherwise it could be worth to give it a try from console.

2 Likes

If you want something code-based that you can stick into arbitrary method chains, I don't think there's a way to do that (yet?).
For Iterator specifically there are for_each() and inspect(), those might help.

2 Likes

Oh, in theory there is a very bad thing™ you can do: explicitly put a breakpoint in your code. On windows you can use DebugBreak, on unixes you can raise a SIGINT.

Both solutions are unsafe, probably dependent on both OS and debuggers, and you should remember to remove that line of code later... :smile:

Thanks folks. If I put the breakpoint on the .filter statement, I can't see the elements being operated on.
The trick is to put the body of the closure on a separate line and set the breakpoint there. It's still weird, because "step over" takes me into some assembler code, but "step out" takes me back. The point is that I can see the elements as I process them.

max = elements 
    .iter()
    .filter(|element| 
        element.is_active())
    .max_by_key(|element| 
        element.val)) 
    .unwrap_or(0)
5 Likes

Ok, I just want to check if I am doing the same thing. Take the following test code:

 1  struct Element {
 2      val: i32,
 3  }
 4  
 5  impl Element {
 6      fn new(val: i32) -> Self {
 7          Element { val }
 8      }
 9  
10      fn is_active(&self) -> bool {
11          self.val % 3 == 0
12      }
13  }
14  
15  fn test() -> i32 {
16      let elements: Vec<_> = (0..17).into_iter().map(Element::new).collect();
17  
18      if let Some(max) = elements
19        .iter()
20          .filter(|element| element.is_active())
21          .max_by(|x, y| x.val.cmp(&y.val))
22      {
23          max.val
24      } else {
25          0
26      }
27  }
28  
29  fn main() {
30      println!("{}", test());
31  }

Using lldb I can do the following:

(lldb) b 20
Breakpoint 1: where = test`test::test::_$u7b$$u7b$closure$u7d$$u7d$::h91f3392e4a6c5779 + 14 at test.rs:20, address = 0x00000000000098de
(lldb) b 21
Breakpoint 2: where = test`test::test::_$u7b$$u7b$closure$u7d$$u7d$::h6364a40ef2361205 + 19 at test.rs:21, address = 0x0000000000009913
(lldb) r
Process 7616 launched: '/tmp/test' (x86_64)
Process 7616 stopped
* thread #1, name = 'test', stop reason = breakpoint 1.1
    frame #0: 0x000055555555d8de test`test::test::_$u7b$$u7b$closure$u7d$$u7d$::h91f3392e4a6c5779((null)=&0x7fffffffe1e0, element=&0x7fffffffe198) at test.rs:20
   17  	
   18  	    if let Some(max) = elements
   19  	        .iter()
-> 20  	        .filter(|element| element.is_active())
   21  	        .max_by(|x, y| x.val.cmp(&y.val))
   22  	    {
   23  	        max.val
(lldb) p **element
(test::Element) $0 = Element {
val: 0
}
(lldb) breakpoint delete 1
1 breakpoints deleted; 0 breakpoint locations disabled.
(lldb) cont
Process 7616 resuming
Process 7616 stopped
* thread #1, name = 'test', stop reason = breakpoint 2.1
    frame #0: 0x000055555555d913 test`test::test::_$u7b$$u7b$closure$u7d$$u7d$::h6364a40ef2361205((null)=&0x7fffffffe268, x=&0x7fffffffdf38, y=&0x7fffffffdf20) at test.rs:21
   18  	    if let Some(max) = elements
   19  	        .iter()
   20  	        .filter(|element| element.is_active())
-> 21  	        .max_by(|x, y| x.val.cmp(&y.val))
   22  	    {
   23  	        max.val
   24  	    } else {
(lldb) p **x
(test::Element) $1 = Element {
val: 0
}
(lldb) p **y
(test::Element) $2 = Element {
val: 3
}

I tested the behaviour using gdb, and it worked as well.
If I understood correctly what you are saying, you are not able to perform the same thing (it looks like the debugger stopped right before calling the lambda, or something similar). Can you confirm that the behaviour I am getting is just what you expected?

1 Like

Yes, I can stop on the line (20 in the example). It looks like I can also examine "element" in most cases. What fooled me was that sometimes I see nonsense values, but not always. I'll have to track down what's happening. Step Over and Step Into take me into Rust internals assembly code, but Step Out gets me back to where I want to be. The bottom line is that now I can use VSCode for debugging, which was the whole point of this thread.

2 Likes

If you can reproduce the problem, I am quite interested. Do you think that the issue could be related to an optimized compilation? It happens, and generally you only realize it when you see your instruction pointer jumping around like crazy :smile:

There are two oddities. The first is the strange values when I look at a variable in the closure. At least in my case the problem is that when the value of the Option is None, the VSCode debugger shows Some({...}), but {...} is garbage. Odd, but I can work with that.

The second is Step Over in the debugger. It takes me into the compiled code for the appropriate function, e.g., FilterMap if that's the line I'm debugging. I don't think this problem is related to the optimizer, because eventually I see the data I'm looking for where I'm looking for it. Step Out takes me back to the closure looking at the result of iterator.next(). I can work with that, too.

There's one other unfortunate aspect; I have to put a breakpoint on each step of the fluent statement or Step Over skips the rest of the statement. Now that I know what's happening, I can live with that, as well.

We should probably continue this discussion in a VSCode discussion group. I don't think it has anything to do with Rust, unless the latest version of Java doesn't have the same problem.

Rust will often times assign instructions to a line that contain a binding definition, even though nothing has been assigned to this variable yet. If you continue, the next time you hit the breakpoint will be for the useful instructions and you will then generally see correct values.

I don't believe that's what I'm seeing here. A println! shows that the values that appear as garbage are definitely None, while the debugger shows them as Some(garbage).

I've been quite impressed with how the VSCode debugger shows me what I expect to see even with code optimizations. In particular, I haven't seen what you describe.

Just for education's sake, it'd be useful to state the operating system you're running on.

macOS High Sierra Version 10.13.6

Ok, I did not say explicitly what you figured out by yourself: the actual steps are not contiguous at all in debug. The call chain is generally inlined in release, but at that point it is mainly impossible to follow the program flow with a debugger. At the state of the art, you are basically right: there is no way to "step over" monadic operations, you can only set many breakpoints at the beginning of each step (if you have a multi-line lambda, breaking at the first line of it should be sufficient to step over until its last line).

I don't know if a gdb plugin developed for C++ could be useful in some way: I cannot remember the name (google could help you better than me), but this plugin let the debugger step over only on your code, automatically stepping each instruction in different source files (or without a proper one).

Honestly, I don't have any specific suggestion for this. I can only think that about some problems of debugger type representation (this part is performed from a script, not exactly the debugger itself). Maybe you are in one of the cases Option is able to steal a bit from the underlying type in order to switch between Some and None. And, maybe, the debugger script is not able to understand that he got a None with a good amount of garbage bits.

Just for curiosity, are you able to check if you can reproduce the behaviour with rust-gdb and rust-lldb? Just to see if this problem is only related to VSC, to one debugger (I don't know what VSC uses as debugger backend) or to all of them.

I used to use gdb -- 25 years ago :frowning: !!! I thought a colleague was a current user, but he just told me he's in the same boat as me. I tried to get gdb working on my Rust program, but all I got was a bunch of warning messages.

I was able to get lldb running and set a break point on the line with filter. However, when I try to display element, it tells me that no variable named 'element' is found in this frame. That says to me that the break point is not in the closure. I next split the line, so that element.is_active() is on a separate line, and that worked. When I display the variable (frame variable *element), I get the expected result unless the value is None. Then lldb tells me it's not a pointer or reference type.

I confirmed that VSCode is not handling None properly by defining foo: Element = None. VSCode displays an Element with garbage in the fields.