Previous ToC Up Next

7. Initial State Output

7.1. The to_s Method

Bob: We are approaching the end of the second journey. There is only one method left, to_s, apart from a little helper method, add_tabs, that acts as a little accountant, keeping care of how many tabs to add to make the output pretty. Let me first show the little helper:

   def add_tabs(s, reference_size, n)
     (1..n).each{|i| s += "\t" if reference_size < 8*i}
     return s
   end

Alice: That last one is keeping tabs on tabs, right? Well, I'm happy to skip that one for now. I can easily judge from the output whether you did a good job there or not. Can you show me the to_s method? Judging from the name, it converts the result of an option block into a string. But what kind of string? As if we haven't dealt with enough strings already!

Bob: It prepares the string that the program will print out at the start, as an echo of its initial state. In our example case of a time step, it will print something like:

    dt = 0.001
Alice: And that is the only thing the user needs to know, after having used an option -- or after having left its value to be the default value, as the case may be. Fine. That method should be quite short, no?

Bob: No, not really. It started off short and sweet, but it grew and grew while I was improving the code. There is quite a bit of bookkeeping that needs to be done here. Here is the code:

   def to_s
     if @type == nil                                                         
       s = @description + "\n"                                               
     elsif @type == "bool"                                                   
       if eval(@valuestring)                                                 
         s = @description + "\n"                                             
       else                                                                  
         s = ""                                                              
       end                                                                   
     else
       s = @description                                                      
       s = add_tabs(s, s.size, 4)                                            
       s += ": "                                                             
       if @printname                                                         
         s += @printname                                                     
       else                                                                  
         s += @globalname                                                    
       end                                                                   
       s += " = " unless @printname == ""                                    
       s += "\n  " if @type =~ /^float\s*vector$/                            
       s += "#{eval("$#{@globalname}")}\n"                                   
     end
     return s
   end

7.2. The Header Option

Alice: It is longer than I thought. And it is not just a matter of many when options in a case statement: there seems to be some genuine complexity here.

Bob: Let me run you through the various switches. The first if statement concerns the description of the program as a whole. We have decided that the here document that contains this one long list of all options will start off with information about the program as a whole. This could take the form of:

  Description:          A code doing such-and-such
  Long description:
    This is a code doing such-and-such for the purpose of so-and-so.
    This code may come in handy if you want to do this-and-that.
    Be warned that it may not always work.  And beware of the dog.
This is an unusual option block, as option blocks go. Parsing so far has posed no problem, since we could parse the Description and Long Description like we parsed any other option. Also, when we talked about the eval_value method, we could forget about this header option that is a not-an-option option.

Alice: Could we, really? Now that I look at eval_value again, I see that it raises an error if @type is not one of the known values. But this header option has no type at all! So it surely will raise an error.

Bob: It will raise an error when it is invoked for the header option. But the key here is that it will never be invoked! I now realize that we forgot to talk about that. We were so distracted by what you called the comic book sequence, ("$#{@, that we did not really finish looking at that line till the end. It came from the one-line method initialize_global_variable:

   def initialize_global_variable
     eval("$#{@globalname} = eval_value") if @globalname
   end

Note that it only invokes the eval_value method if the variable @globalname exists. And that variable springs into existence only when parse_single_lines_done? encounters a line in the definition string that starts with Global Variable or Global variable. And since the header option block contains no such line, the eval_value method will never be called for that option.

Alice: All is well then. I had completely forgotten about the header option. And I now see the meaning of the if statement at the top of to_s:

 :include .clop.rb-14
The header option is the only one that does not carry a type, so at the start of the program, the one-line short description will be echoed.

Bob: Indeed. What will happen is:

  |gravity> ruby some_code.rb
  A code doing such-and-such
  Time to stop integration        : 
  First interesting variable      : x  =  1.0
  Second interesting variable     : y  =  3.14
  . . .
After the one-line summary of what the code is about, it will start listing the values of the global variables.

Alice: But using their print name.

7.3. A Boolean Option

Bob: Exactly. We'll get to that in a moment. We're done with the case of the header option. Let us have a look at what needs to happen for an option of type boolean:

     elsif @type == "bool"                                                   
       if eval(@valuestring)                                                 
         s = @description + "\n"                                             
       else                                                                  
         s = ""                                                              
       end                                                                   

If the value is true, in other words if @valuestring == "true", that means that the user has invoked this option on the command line. In that case, a short message should be printed out. Ah, I see that the example I just gave only contained a header option and specific values of non-boolean type. Let me throw in a boolean option:

  |gravity> ruby some_code.rb
  A code doing such-and-such
  First interesting variable      : x  =  1.0
  Extra diagnostics will be provided
  Second interesting variable     : y  =  3.14
  . . .
This `Extra diagnostics' line would be the result of the user providing a -x option, for example, on the command line. Now if the user chooses not to provide that option, there is nothing to report. Also, in that case, the option will retain its default value, which for a boolean variable is always false. In that case, the else part of the if...else statement will kick in, and only the empty string will be added to the string s that will be returned by to_s: nothing will be added at all.

Alice: Continuing down the to_s function, we have dealt with the cases of the header option and of boolean options. All other options are dealt with in the final else clause of the initial if statement.

Bob: Yes, because all other options have genuine values that need to be reported as such.

7.4. A Hack

Alice: The following lines:

       s = @description                                                      
       s = add_tabs(s, s.size, 4)                                            
       s += ": "                                                             
       if @printname                                                         
         s += @printname                                                     
       else                                                                  
         s += @globalname                                                    
       end                                                                   

must result in producing the example line

  First interesting variable      : x
Right?

Bob: Right indeed. The description of the variable is followed by some white space counting magic followed by a colon, and then we get the proper print name, if it is provide. If not, we just use the internal name for the global variable associated with this option.

Alice: But I'm puzzled about what follows next:

       s += " = " unless @printname == ""                                    

What is the meaning of the unless here?

Bob: Ah, this is another `feature' that I built in. I was thinking about running an N-body code . . .

Alice: . . . which is how we got into all this . . .

Bob: . . . yes, hard to believe, I feel like I'm turning into a software engineer. But just like an observer needs a telescope, which requires quite a bit of hardware engineering, a theorist interested in simulations needs a software environment, which requires quite a bit of software engineering.

Alice: Hear, hear!

Bob: So, thinking about running an N-body code, I realized that I would like to see the time step echoed, as well as the other physical parameters, in the form of description : name = value, as in

  Integration time step         : dt = 0.001
but for the case of an option -o output_file, it would look a bit strange to have in the initial state list the line:

  Name of the outputfile        : output_file = run.out
It would be much more natural to have:

  Name of the outputfile        : run.out
since there is no reason in this case to echo the global variable name used to hold the output file name, nor is there any appropriate print name I could think of. In this case, the description says it all!

So I decided to build in a way to block the appearance of "name = " for such a case.

Alice: How did you do that?

Bob: I did not feel like adding yet another variable, like @no_name_requested. So I made an inventory of possible cases. If an option does have a print name, that name will be used. If it does not have a print name, the global variable name will be used. And then I realized the solution: if an option has a print name of length zero, just the empty string "", that could be interpreted as a request to remain silent and not print anything, neither the name, which is already nothing, nor the equal sign normally following it.

I admit, it is a bit of a hack, but it works. So if @printname == "" there is no need to put an = sign after the description, and that what is expressed in the line with the unless in it that you asked about:

       s += " = " unless @printname == ""                                    

7.5. A Vector Option

Alice: Yes, it is a hack alright, but if it works, it works.

Bob: It works.

Alice: Good! But now I'm puzzled by the next line in the to_s code:

       s += "\n  " if @type =~ /^float\s*vector$/                            

What does that do?

Bob: I again had in mind our experience with an N-body code. When we print out a vector, we may want to have full machine accuracy, double precision. And if we do a three-dimensional simulation, which is normally the case, we need to print three numbers, each of which will take a space of two dozen characters. That together will already span a normal output line. Therefore, if we start a line with a description, the float vector will run off the page. To prevent that, I added a new line, just after the colon, for these kind of vectors.

As an example for an N-body code, where you could specify choosing three particles, and a shift in the center of mass position as follows:

  ruby some_N_body_code.rb -n 3 -v [ 3, 4, 5 ]
You will then have the following initial state print-out:

  Number of particles           : N = 3
  Shifts center of mass by      : rcom = 
    3.0000000000000000e+00  4.0000000000000000e+00  5.0000000000000000e+00
Alice: That does look much better than running off the page, I agree.

Bob: Somehow we are still under the influence of the original 80 column limitation of Fortran, it seems.

Alice: I admit that I always try to keep my output fitting within 80 columns. For of habit, I guess. And I don't like people sending me email that runs over 80 columns either.

7.6. A Pyramid of Evaluations

Bob: All a matter of taste. Well, one more line to go, at the end of to_s :

       s += "\n  " if @type =~ /^float\s*vector$/                            

This must finally produce the actual value, that what appears to the right of the equal sign in the initial state output.

Bob: Yes, and it does so by a three-stage evaluation, all bundled in one statement line.

Alice: Wow, that looks impressive. This will be my final test to see whether I now really understand what is going on. A concrete example will help. In the case of a time step size option, we have

  @globalname = "dt"
The first evaluation produces the string

  "$#{@globalname}" = "$dt"
which is a string holding the name of the global variable $dt.

The second evaluation is a call to eval and it produces:

  eval("$#{@globalname}") = eval("$dt") = $dt
the global variable itself.

Then the third evaluation produces a string that contains the value of this global variable:

  "#{eval("$#{@globalname}")}\n" = "#{eval("$dt")}\n" = "#{$dt}\n" = "0.01\n"
with a new line character at the end, to finish of the line.

Bob: Congratulations! You passed the exam.

Alice: And I also realize that, if we wanted to actually use the value of the time step here, we would have to do a fourth evaluation, to go from the string "0.01" to the value 0.01. Let me just write it down, to see what it looks like. And I can forget about the new line here. The value should be:

  eval("#{eval("$#{@globalname}")}") = 0.01
Bob: That looks right to me.

Alice: So you go from a variable to a string to a variable to a string to a variable. Well, well, and you told me the whole thing works?

Bob: Yes, it works. I'll give you a demonstration after we have completed our three journeys. This, by the way, is the end of the second journey, which is by far the longest one.

Alice: Not too surprising, since it is a journey into the kitchen, so to speak, to sea what is actually cooking in the various pots and pans. The other two journeys take place in the restaurant, where you're dealing with the food and the menu, but not the details of the preparation.

Bob: Indeed, the third journey will be shorter.
Previous ToC Up Next