cheech has asked for the wisdom of the Perl Monks concerning the following question:

In this problem, I'm looping over radius length from 1 micron to 100 microns in 2 micron intervals. If the length is < 20 microns, I send it to the subroutine "sigmalow," compute sigma, and return it. If the length is >= 20 microns, I send it to a different subroutine because the equation for sigma is different. After all computations, each sigma result is stored in the same array and printed along with the radii.

I've done a few programs like this so far and I understand it pretty well. Everything works fine, but again, I feel like there are certain codes that could be done better/more efficiently. Thanks again to those who commented on my post yesterday.. I tried to keep my variables defined locally to the block they are working in. Any comments or suggestions are greatly appreciated. Thanks!

use strict; use warnings; my ($i); #declare global $i for for loop my (@sigmas, @rads); #declare arrays for storage of radii and sigma + results for ($i=1;$i<=100;$i+=2) { # 1, 3, 5, ..., 99 my $r = ($i)*(1e-6); #radii are in microns push (@rads, $r); #store the correct radii in array for pri +nt at end if ($r<2e-5) { #if the radius is <20microns, se +nd to sub sigmalow my $sig = sigmalow($r); #store the return in $sig push (@sigmas, $sig); #add result to the final sigma ar +ray } else { #if the radius is >=20microns, s +end to sub sigmahigh my $sig = sigmahigh($r); #store the return in $sig push (@sigmas, $sig); #add result to the final sigma ar +ray } } print "radii: @rads \n"; print "sigmas: @sigmas \n"; exit; sub sigmalow { #to compute sigma when radius < 20microns my ($pi, $a, $c, $rad, $sig); my (@r); $pi = 3.14159; #constants $a = 1.18; $c = .28e6; @r = @_; #transfer the argument to @r $rad = $r[0]; #is this step necessary? $sig = $pi*($rad**2)*$a*(1-exp(-$c*$rad)); #calculation of sigma return ($sig); #send back the result } sub sigmahigh { #to compute sigma when radius >= 20microns my ($pi, $rad, $sig); my (@r); $pi = 3.14159; #constant @r = @_; #transfer the argument $rad = $r[0]; #necessary? $sig = 2*$pi*($rad**2); #compute return ($sig); #return it }

Sorry about the long, wrapping comments!

Replies are listed 'Best First'.
Re: Improve my Code: Subroutines
by hangon (Deacon) on Jun 13, 2009 at 21:14 UTC

    Unless you have a compelling reason to have separate arrays, I suggest you use an AoA to put everything in the same data structure. This makes it easier to keep associated values together.

    my @radsigma; for ( my $i=1; $i<=100; $i+=2 ) { my $sig; if ( $r <2 e-5 ) { $sig = sigmalow($r); }else{ $sig = sigmahigh($r); } push @radsigma, [ $r, $sig ]; }

    The "constant" $pi is used in multiple places and should be declared only once at the top of the program. There is also a pragma to make it more like a real constant.

    use strict; use warnings; use constant PI => 3.1416;

    You've done way too much work to get the subroutine arguments. This is how it's generally done:

    sub sigmalow { my $rad = shift; # shift it off the front of @_ my $rad = $_[0]; # or explicitly assign the value

    About your loop, there is no reason to declare $i on a separate line (see above). Also, C style for loops are generally ugly and unperlish. Your example is one of the few cases where these may actually be cleaner than the alternatives, but you should at least be aware that there are alternatives:

    for my $i (1..100){ next unless $r % 2; for (0..49){ my $i = $_ * 2 + 1; for my $i ( grep {$_ %2} (0..99 ) ){

    A lot of your code is crowded horizontally. Using more blank spaces helps readability.

      Arguments are usually done with
      sub f { my ($arg1, $arg2) = @_; ... }

      or

      sub f { my $arg1 = shift; my $arg2 = shift; ... }

      Using $_[0] is rarer.

      Also, C style for loops are generally ugly and unperlish

      True, but none of your alternatives are particularly better. I would move the my into the loop, though.

      for (my $i=1; $i<=100; $i+=2) { # 1, 3, 5, ..., 99
      Thanks for the advice.. everything makes sense except the alternative loop examples, but I'll get there.
        ... everything makes sense except the alternative loop examples ...

        They were meant to illustrate the Perl style for loop (aka foreach loop), so you would be aware of it. The C style loop actually makes more sense in your program, but that is usually not the case.

        The basic idea of the Perl for loop is that it loops over a list of values, aliasing each one to the loop variable in turn. The following should help clarify:

        # foreach $variable ( @list_of_values ){ ... # or # for $variable ( @list_of_values ){ ... # $variable is aliased to each successive item in list # @list_of_values can be a literal list, an array or an # expression that evaluates to a list: for my $var ( 1..4 ){ ... # expression using range operator for my $var ( @array ){ ... # array for my $var ( 6, 7, 'A', 'B' ){ # literal list print "$var "; } # if a variable is not specified, $_ is used by default for ( 6, 7, 'A', 'B' ){ print "$_ "; }
Re: Improve my Code: Subroutines
by CountZero (Bishop) on Jun 14, 2009 at 08:11 UTC
    my $r = ($i)*(1e-6); #radii are in microns

    I would keep the value of the radii as an integer as long as possible and only "scale" it in the actual calculation. Integers will be presented without any loss of precision in Perl, but once they are turned into a real, it is not certain that they are still exact as their internal representation may involve a repeating sequence of bits, which may be a little bit more or less than you expect and tests such as if ($r<2e-5) may give unexpected results in edge-cases. In this case, it seems to be OK.

    An interesting discussion on this issue can be found in My floating point comparison does not work. Why ?.

    Running your code through Perl::Critc gives the following:

    Code before strictures are enabled at line 1, column 1. See page 429 +of PBP. Severity: 5 Code before warnings are enabled at line 1, column 1. See page 431 of + PBP. Severity: 4 Use "<>" or "" or a prompting module instead of "" at line 1, column 9 +. See pages 216,220,221 of PBP. Severity: 4 Always unpack @_ first at line 29, column 1. See page 178 of PBP. Se +verity: 4 Magic variable "$a" should be assigned as "local" at line 35, column 4 +. See pages 81,82 of PBP. Severity: 4 Always unpack @_ first at line 46, column 1. See page 178 of PBP. Se +verity: 4
    Nothing too bad and all of these critics can be easily mended.

    CountZero

    A program should be light and agile, its subroutines connected like a string of pearls. The spirit and intent of the program should be retained throughout. There should be neither too little or too much, neither needless loops nor useless variables, neither lack of structure nor overwhelming rigidity." - The Tao of Programming, 4.1 - Geoffrey James

      my $r = ($i)*(1e-6);    #radii are in microns

      Why do you put both factors in brackets?

      Integers will be presented without any loss of precision in Perl

      But only as long as the number "fits" into perls integer type, which typically is a 32 bit signed integer or 64 bit signed integer. Once you exceed the limit (+/- 231 resp. +/- 263), perl silently converts the value to a floating point number with loss of precision (perlnumber).

      Alexander

      --
      Today I will gladly share my knowledge and experience, for there are no sweeter words than "I told you so". ;-)
        Why do you put both factors in brackets?
        I just copied it from the OP. There is no harm to using brackets here, they are just superfluous.

        CountZero

        A program should be light and agile, its subroutines connected like a string of pearls. The spirit and intent of the program should be retained throughout. There should be neither too little or too much, neither needless loops nor useless variables, neither lack of structure nor overwhelming rigidity." - The Tao of Programming, 4.1 - Geoffrey James

Re: Improve my Code: Subroutines
by graff (Chancellor) on Jun 14, 2009 at 14:48 UTC
    Total agreement with CountZero and ikegami, and with most of what hangon says, plus a couple "new" ideas:
    use strict; use warnings; use Math::Trig; # why settle for less (precision)? my ( @sigmas, @rads ); my ( $a1, $c ) = ( 1.18, .28e6 ); my $i = 1; while ( $i < 20 ) { my $r = $i * 1e-6; push @rads, $r; push @sigmas, pi * ($r**2) * $a1 * (1-exp(-$c*$r)); $i += 2; } while ( $i < 100 ) { my $r = $i * 1e-6; push @rads, $r; push @sigmas, 2 * pi * ($r**2); $i += 2; } for $i ( 0 .. $#rads ) { printf "rad= %8.3g sigma= %8.3g\n", $rads[$i], $sigmas[$i]; }
    That changes the output format, so that it's easier to see the relationships between "rad" and "sigma" values.

    It also shows a clean alternative to the C-style for loop, which some monks consider to be rather gauche. (Don't get me wrong -- C-style for loops have their place, but it tends to be a very small place, rarely visited by Perl hackers.)

    UPDATE: Oops -- I just realized that your focus for this thread is on subroutines, and I completely avoided that part of today's lesson. Just replace the arithmetic expressions with sub calls that take "$r" as the sole parameter, and trim down the subs themselves as explained in the other replies.

    Oh, and pick a name other than "$a" for the "1.18" constant -- perl has a global $a variable that is used by the built-in "sort" function. (I've updated my own code here to follow this advice.)

    Okay, last update -- I decided/realized that all the repetition in the two while loops was silly, so here it is again (with subs; note how the return value of the sub is simply the value of the last expression in the sub -- you could add "return" there, but it isn't needed):

Re: Improve my Code: Subroutines
by JavaFan (Canon) on Jun 14, 2009 at 14:58 UTC
    Instead of hard coding $pi twice, I'd put near the top of the file:
    my $PI = 2 * atan2(1, 0);
    I use capitals to indicate the variable plays the role of a constant - it shouldn't be assigned to. Some people will suggest using use constant PI => 2 * atan2(1, 0), but that style gives constants that are harder to interpolate, so I tend to shy away from them.

    I also note that both subroutines are almost the same. I would use just one subroutine, and an extra parameter. Something like:

    my $PI = 2 * atan2(1, 0); my $A = 1.18; my $C = 0.28e6; my $HWM = 2e-5; my $SCALE = 1e-6; sub sigma; my (@rads, @sigs); for (my $i = 1; $i <= 100; $i += 2)) { my $r = $i * 1e-6; push @rads, $r; push @sigs, sigma($r, $r >= $HWM) } print "radii: @rads\n"; print "sigmas: @sigs\n"; sub sigma { my ($rad, $high) = @_; $PI * $rad ** 2 * ($high ? 2 : $A * (1 - exp(-$C * $rad))); }
    I didn't run or even compiled the code above, so it may be full of typos.
Re: Improve my Code: Subroutines
by Marshall (Canon) on Jun 14, 2009 at 16:30 UTC
    I think the code is "devolving". The code in original post seemed better to me.
    maybe as JavaFan suggests calculate pi like:
    my $PI = 2 * atan2(1, 0);
    I would calculate $rad*$rad rather than $rad**2, pi*a0 is another possibility to factor out. There are some scaling things that you could do. But basically the code below is fine (2nd listing or this next one or combo).

    Update: One obvious other thing is: Why push when you can print? Get rid of this push stuff and just output what you want, like this below. You use $r once, calculate some stuff and print it. In the loop below, there is a scaling factor of 1e-6, moving it out of this loop will do pretty much nothing. Below there is a simple integer "if" and a choice of 2 floating point equations. It won't get much better than this.

    foreach my $r (@radii) { my $rad = (1e-6)*$r; print "$rad "; if ($r>20) { my $sig = $twopi*($rad*$rad); print "$sig\n"; } else { my $sig = $pi*($rad*$rad)*$a0*(1-exp((-$c0)*$rad)); print "$sig\n"; } }
    #3_3.pl #loop over drop radius from 1 micron to 100 microns in steps of 2 micr +ons #use each radius to compute cross section #if radius <20 microns, use (pi)*(r^2)*a*(1-exp(-c*r)) #if radius >= 20 microns, use 2*pi*(r^2) # orginal post at: # http://www.perlmonks.org/?node_id=771126 use strict; use warnings; my @radii = (1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31 +, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, + 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99); my @rads = (); my @sigs = (); my $pi = 3.14159; my $twopi = (2)*(3.14159); my $a0 = 1.18; my $c0 = (0.28e6); foreach my $r (@radii) { my $rad = (1e-6)*$r; push (@rads, $rad); if ($r>20) { my $sig = $twopi*($rad*$rad); push (@sigs, $sig); } else { my $sig = $pi*($rad*$rad)*$a0*(1-exp((-$c0)*$rad)); push (@sigs, $sig); } } print "@rads \n"; print "@sigs \n"; __END__ Added some \n for formatting... 1e-006 3e-006 5e-006 7e-006 9e-006 1.1e-005 1.3e-005 1.5e-005 1.7e-005 1.9e-005 2.1e-005 2.3e-005 2.5e-005 2.7e-005 2.9e-005 3.1e-005 3.3e-005 3.5e-005 3.7e-005 3.9e-005 4.1e-005 4.3e-005 4.5e-005 4.7e-005 4.9e-005 5.1e-005 5.3e-005 5.5e-005 5.7e-005 5.9e-005 6.1e-005 6.3e-005 6.5e-005 6.7e-005 6.9e-005 7.1e-005 7.3e-005 7.5e-005 7.7e-005 7.9e-005 8.1e-005 8.3e-005 8.5e-005 8.7e-005 8.9e-005 9.1e-005 9.3e-005 9.5e-005 9.7e-005 9.9e-005 9.05328279702527e-013 1.89602315397586e-011 6.98230615994953e-011 1.56060261711467e-010 2.76113310847353e-010 4.27940909754303e-010 6.10048892522909e-010 8.21584452164295e-010 1.06216829881331e-009 1.33170675846969e-009 2.77088238e-009 3.32380222e-009 3.9269875e-009 4.58043822e-009 5.28415438e-009 6.03813598e-009 6.84238302e-009 7.6968955e-009 8.60167342e-009 9.55671678e-009 1.056202558e-008 1.161759982e-008 1.27234395e-008 1.387954462e-008 1.508591518e-008 1.634255118e-008 1.764945262e-008 1.90066195e-008 2.041405182e-008 2.187174958e-008 2.337971278e-008 2.493794142e-008 2.65464355e-008 2.820519502e-008 2.991421998e-008 3.167351038e-008 3.348306622e-008 3.53428875e-008 3.725297422e-008 3.921332638e-008 4.122394398e-008 4.328482702e-008 4.53959755e-008 4.755738942e-008 4.976906878e-008 5.203101358e-008 5.434322382e-008 5.67056995e-008 5.911844062e-008 6.158144718e-008