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

UPDATE: thank you everyone for your help!! it made a really big difference, and i feel like i understand what i'm doing, now. i've got the code working perfectly, and the assignment turned in. now i know not to be nervous about asking for help, heheh.

i am at the end of my rope here, honestly - i'm totally new to programming as a whole, and the textbook provided to us for my course reads like jibberish to me. i can only fully understand the logic behind a code if i work it out in a command terminal myself, and with this code... i've been trying all night to figure out how to get this while loop to work, and this is my last desperate attempt for help, after scouring forums and questions. here's the code:

#! /usr/bin/perl use strict; my %son_father; my $choice; my $name1; my $add_dad; %son_father = (Jeff => "Doug", Thomas => "Evan", Robert => "Jason", Bruce => "Richard", Clark => "Jon") ; my $menu = <<MENU; SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father MENU while (1) { # display the menu print $menu, "\n\n"; # get the user choice print "Make your choice: "; chomp($choice = lc <STDIN>); # fulfill the user request if ($choice eq 'a'){ print "Enter a male name: "; chomp ($name1 = lc <STDIN>); } if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; } else { print "Add a father: "; chomp ($add_dad = lc <STDIN>); $son_father{$name1} = {$add_dad}; } if ($choice eq 'd') { print "Enter a male name: "; chomp ($name1 = lc <STDIN>); } if (exists $son_father{$name1}) { delete $son_father{$name1}; } else { print "Sorry, couldn't find you -- try again later!"; } if ($choice eq 'e') { print "Come back again -- goodbye!"; exit; } else { print "Invalid choice, TRY AGAIN!\n"; } }

i'm near tears here.

it feels like nothing is working. no matter what i pick, "Add a father:" shows up. and it seems like "Invalid choice" shows up no matter what, as well, although that may just be because i haven't finished adding in every potential operation.

(then again, i'm not sure how to tell the script "if (input) does not exist, then (print)"? i haven't been able to find anything opposite of "exists" beyond using "else", but that has to be part of an "if" loop, doesn't it? should i just change it to an "if" loop? i'm wary of changing the code too much, as the skeleton was provided TO us, but at this point, i'd almost rather just start this code from scratch...)

i'm just not sure how to get the chomp prompt under choice "a" to actually work. i think i can figure out the other chomp prompts based on solving that one problem, but i'm just. stumped. the loop works, and choice "e" does break it, but i don't understand why "Add a father:" shows up no matter what i choose, or how to tell the script to perform an operation based on a choice not being valid, or to perform an operation based on a <STDIN> not existing within the hash...

i'm sorry this is such a long question (and one that perhaps doesn't have a very clear question at all), but i didn't know where else to go to get any help. my teacher isn't readily available, and, again, my textbook just makes me want to cry from frustration. i feel like an idiot for not being able to just understand the text, but i really don't get it. thank you in advance. T_T

Replies are listed 'Best First'.
Re: help with user selected hash operations?
by NetWallah (Canon) on Oct 30, 2017 at 05:31 UTC
    Since you have attempted something .. here is corrected/working code for you to enhance/complete the missing options(r,x).

    I have added the "o" option, so you can check results at each step.

    #! /usr/bin/perl use strict; my %son_father; my $choice; my $name1; my $add_dad; %son_father = (Jeff => "Doug", Thomas => "Evan", Robert => "Jason", Bruce => "Richard", Clark => "Jon") ; my $menu = <<MENU; SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father MENU while (1) { # display the menu print $menu, "\n\n"; # get the user choice print "Make your choice: "; chomp($choice = lc <STDIN>); # fulfill the user request if ($choice eq 'a'){ print "Enter a male name: "; chomp ($name1 = <STDIN>); if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; next; } print "Add a father: "; chomp ($add_dad = <STDIN>); $son_father{$name1} = $add_dad; print "Added\n"; next; # <<<<< added } if ($choice eq 'd') { print "Enter a male name: "; chomp ($name1 = <STDIN>); if (exists $son_father{$name1}) { delete $son_father{$name1}; print "Deleted.\n"; } else { print "Sorry, couldn't find '$name1' -- try again later!" +; } next; } if ($choice eq 'o') { print "$_\t=> $son_father{$_}\n" for sort keys %son_father; next; } if ($choice eq 'e') { print "Come back again -- goodbye!"; exit; }else { print "Invalid choice, TRY AGAIN!\n"; } }
    Significant changes:
    * Added "next" statements to return to top of the loop after an option completes
    * Changed "if" statements to enclose subordinate input (Asking for names)
    * Corrected the "Add Father" assignment .. removing extra {}, which made it a hashref.
    * Remove "lc" for Name inputs to allow correct case names.
    * Added the "o" option.

                    All power corrupts, but we need electricity.

      i've definitely solved the repetition problem! i'm not sure i can /see/ what "next;" is doing, but it does seem to be helping! haha.

      i think my confusion now is, after doing all the other steps i can, i'm still not positive how to make the final if statement work: it's meant to be, "if user inputs none of the menu options, the invalid message appears". when i had it set to "if ($choice ne 'a'...'e')" then print the error message, but it seems like it's taking the input for $name1, etc, as an invalid input, too.

      } if ($choice eq 'r'){ print "Enter a male name: "; chomp (my $name3 = lc <STDIN>); if (exists $son_father{$name3}) { print "Enter a new father name: "; chomp ($newname = <STDIN>); $son_father{$name3} = {$newname}; } else { print "Sorry, couldn't find you -- try again later!"; next; } } if ($choice ne 'a', 'r', 'o', 'd', 'e') { print "Invalid choice, TRY AGAIN!\n"; } }

      i'm going to try and ask my professor about this more tomorrow, but i'm wondering if i'm just missing something obvious. will it read /every/ non-$choice standin as false if i write the if statement like this? is there any way to fix that? i thought assigning each standin a different variable would keep it separate from the if statement...

      i'm definitely feeling less anxious after getting some help, but i'm getting pretty delirious after working on this for seven hours, hahah. sorry for being a bit incoherent.

      oh, gosh, i forgot to say thank you. that's the problem with staying up until 4am. thank you so much!

        if ($choice ne 'a', 'r', 'o', 'd', 'e') { print "Invalid choice, TRY AGAIN!\n"; }
        No, conditions don't work that way. You cannot have a list of options on the right hand side of an eq.
        Possible solutions:
        # Very explicit: if ($choice ne 'a' && $choice ne 'r' && $choice ne 'o' && $choice ne ' +d' && $choice ne 'e') { print "Invalid choice, TRY AGAIN!\n"; } # Testing if the element is in a List of options: unless ( grep { $_ eq $choice } ('a', 'r', 'o', 'd','e') ) { print "Invalid choice, TRY AGAIN!\n"; } # Testing if the element matches a regular expression: unless ( $choice =~ /^(a|r|o|d|e)$/ ) { print "Invalid choice, TRY AGAIN!\n"; }


        holli

        You can lead your users to water, but alas, you cannot drown them.
        i'm not sure i can /see/ what "next;" is doing
        for ( 1 .. 3 ) { print "The father\n"; print "The son\n"; next; print "But no holy ghost\n"; }
        Do you see it now? You do really need to understand the core concepts, like loops, if's and most importantly true/false. If you don't: this happens. I'm all for learning by example - this is the natural way our brain works - but you need examples of one core concept at a time.


        holli

        You can lead your users to water, but alas, you cannot drown them.

        In addition to the other excellent advice you have received, I will offer a diagnostic tool which is as useful as it is simple: don't just say something's wrong, say in what way it is wrong. ie. instead of

        print "Invalid choice, TRY AGAIN!\n";

        do this:

        print "Invalid choice ($choice), should be one of a, r, o, d or e. TRY + AGAIN!\n";

        This will let you know either that your condition is badly written (as it was) or if $choice is not being correctly set.

        For other handy tips, do peruse the Basic debugging checklist.

Re: help with user selected hash operations?
by kcott (Archbishop) on Oct 30, 2017 at 11:48 UTC

    G'day lunette,

    Welcome to the Monastery.

    "i am at the end of my rope here, honestly ..."

    Don't Panic! There are no insurmountable problems here.

    "it feels like nothing is working. ..."

    Most of the individual parts of your code are fine; in fact, for a raw novice, it's pretty good (I've certainly seen a lot worse). I get the impression that you've got the basic logic correct in your head — you just haven't translated that to Perl code.

    In the following, I've aimed to highlight the good and the bad, and how to fix the latter. Your assignment may have read something like "Do A, B and C only using X, Y and Z", but you haven't told us that. You also haven't said what the main focus of the assignment was: are you learning about hashes? I/O? UI presentation? something else? If you have another question at a later date, that's useful information to include. I've tried to keep my suggestions in line with with the level of code you've presented; for instance, I haven't used any references or regular expressions. If I've used something you weren't supposed to use, and you can't determine an alternative, just ask.

    The first thing I had a problem with was your indentation. Your first if block is like this:

    if ($choice eq 'a'){ ... }

    At first glance, it appears that '}' is the closing brace for the if block. But it's not! That's the closing brace for a different if block: one that appears in "...", indented two more levels, inside another line of code.

    Indentation doesn't matter to Perl (it does in some other languages and file formats); it does, however, matter a lot to humans who have to read, and possibly modify, the code. You've done the same thing with "if ($choice eq 'd') ...". In both cases, Perl sees a code structure like this:

    if (CONDITION1) { ... } if (CONDITION2) { ... }

    However, what you really wanted was a code structure more like this:

    if (CONDITION1) { ... if (CONDITION2) { ... } }

    Fixing that code structure throughout, will resolve a lot of your current issues.

    Also, instead of a series of if statements, all of whose conditions Perl has to check; consider the following construct, where conditions are only checked until one is found to be TRUE.

    if (CONDITION1) { ... } elsif (CONDITION2) { ... } elsif (CONDITION3) { ... } else { ... }

    There may be better ways to handle that but, as I said at the start, I'm aiming to keep things simple. See "perlintro: Conditional and looping constructs" for more about that.

    [By the way, the expression if loop is wrong. If you call it a loop, and start thinking in terms of loops, you likely add another level of confusion.]

    Another problem you have is with changing cases. It's good that you thought to canonicalise the input; unfortunately, you haven't got it quite right. If the user enters "Jeff", "JEFF", or any other combination (perhaps even something daft like "jEfF"), lc will change that to "jeff":

    $ perl -E 'my @x = qw{Jeff JEFF jEfF}; say lc $_ for @x' jeff jeff jeff

    But, "jeff" isn't one of the keys of '%son_father' — "Jeff" is though (oops!). You can fix that by also using ucfirst:

    $ perl -E 'my @x = qw{Jeff JEFF jEfF}; say ucfirst lc $_ for @x' Jeff Jeff Jeff

    Although there would be better ways to do that in general code, it works with the data you've presented, so I've avoided anything more complex. You can use it for both the keys and values. You do need both the ucfirst and the lc:

    $ perl -E 'my @x = qw{Jeff JEFF jEfF}; say ucfirst $_ for @x' Jeff JEFF JEfF

    If you deal with those two main areas, you can probably get your program up and running reasonably well. There are a few other areas which, while not actually breaking your current code, are poor choices, bad practices, and habits you don't really want to get into.

    • While exit will exit the loop, it also exits the entire program: no code after while would ever be executed. To exit just the loop, use last: that documentation has links to other, useful, loop control functions.
    • It's good that you've used lexical (my) variables; however, declaring them all at the start of your code means that all subsequent code has access to them and can change them. You should use a limited scope for all such variables. That may be a bit beyond where you're up to in your studies: see my example code below for how I've handled this; ask for more information if you're interested.
    • Choose more meaningful names for your variables. Reusing '$choice' throughout was a bad move anyway (as per my last point); however, to someone reading your code (including yourself in a few weeks or months time), it's very unclear what data that variable holds (menu selection? father's name? etc.) which makes it error-prone. I also found '%son_father' ambiguous: I had to look for usage in subsequent code before I was confident (i.e. no longer guessing) of its meaning.
    • Similar to the last point, present the user with meaningful information (in prompts, messages and so on). The prompt "Enter a male name: " is unclear to the user: given the chosen function, that could be the father or the son. Again, I had to look at surrounding code to understand the intent: users of your software don't have this option.
    • In general, avoid monolithic scripts. Abstract code into subroutines: you can then reuse them or, even if only used once, that code won't clutter the main logic. Again, this may be beyond where your studies are: ask for more information, if interested.

    Here's an example script that just implements the "a" and "e" menu selections. It has examples of all the points I've made above.

    #!/usr/bin/env perl use strict; use warnings; { my %father_of = get_son_to_father_map(); display_menu(); while (1) { my $menu_selection = get_menu_selection(); if ($menu_selection eq 'e') { print "Exiting ...\n"; last; } elsif ($menu_selection eq 'a') { print "Son-father pair addition.\n"; print "Enter son's name: "; chomp( my $son = ucfirst lc <STDIN> ); if (exists $father_of{$son}) { print "$father_of{$son} is the father of $son\n"; } else { print "Enter father's name: "; chomp( my $father = ucfirst lc <STDIN> ); $father_of{$son} = $father; } } else { print "Invalid menu selection! Try agin.\n"; } } print "Done!\n"; } sub display_menu { print <<'MENU'; Menu Selections =============== a - add son-father pair e - exit MENU return; } sub get_menu_selection { print "\nEnter menu selection: "; chomp( my $selection = lc <STDIN> ); return $selection; } sub get_son_to_father_map { return ( Jeff => 'Doug', Thomas => 'Evan', Robert => 'Jason', Bruce => 'Richard', Clark => 'Jon', ); }

    Here's a sample run:

    Menu Selections =============== a - add son-father pair e - exit Enter menu selection: qwerty Invalid menu selection! Try agin. Enter menu selection: Invalid menu selection! Try agin. Enter menu selection: a Son-father pair addition. Enter son's name: JEFF Doug is the father of Jeff Enter menu selection: a Son-father pair addition. Enter son's name: bRuCe Richard is the father of Bruce Enter menu selection: a Son-father pair addition. Enter son's name: fred Enter father's name: BILL Enter menu selection: a Son-father pair addition. Enter son's name: FrEd Bill is the father of Fred Enter menu selection: e Exiting ... Done!

    — Ken

      oh, my gosh, this is more than i expected. thank you so much. T_T

      i was hesitant to post the entire code because i thought it might be too long, but with little edits i've made so far, this is what i have:

      #! /usr/bin/perl use strict; my %son_father; my $choice; my $name1; my $name2; my $name3; my $name4; my $newname; my $add_dad; %son_father = (Jeff => "Doug", Thomas => "Evan", Robert => "Jason", Bruce => "Richard", Clark => "Jon") ; my $menu = <<MENU; SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father MENU while (1) { # display the menu print $menu, "\n\n"; # get the user choice print "Make your choice: "; chomp($choice = lc <STDIN>); # fulfill the user request if ($choice eq 'a'){ print "Enter a male name: "; chomp (my $name1 = ucfirst lc <STDIN>); } if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; } else { print "Add a father: "; chomp (my $add_dad = ucfirst lc <STDIN>); $son_father{$name1} = {$add_dad}; next; } if ($choice eq 'd') { print "Enter a male name: "; chomp (my $name2 = ucfirst lc <STDIN>); } if (exists $son_father{$name2}) { delete $son_father{$name2}; } else { print "Sorry, couldn't find you -- try again later!"; next; } if ($choice eq 'e') { print "Come back again -- goodbye!"; last; next; } if ($choice eq 'g'){ print "Enter a son's name: "; chomp (my $name4 = ucfirst lc <STDIN>); } if (exists $son_father{$name4}) { print "$son_father{$name4}\n"; next; } if ($choice eq 'o'){ my ($key, $value); foreach $value (sort keys %son_father){ print "$value\t$son_father{$value}\n"; next; } } if ($choice eq 'r'){ print "Enter a male name: "; chomp (my $name3 = ucfirst lc <STDIN>); if (exists $son_father{$name3}) { print "Enter a new father name: "; chomp ($newname = ucfirst lc <STDIN>); $son_father{$name3} = {$newname}; } else { print "Sorry, couldn't find you -- try again later!"; next; } } if ($choice ne 'a', $choice ne 'r', $choice ne 'o', $choice ne 'd' +, $choice ne 'e', $choice ne 'g') { print "Invalid choice, TRY AGAIN!\n"; next; } }

      i can change my scalar variable names to make it easier to understand, these are just the names the professor asked us to use.

      the problem i seem to be having is that the secondary STDINs seem to trigger the "invalid response", even with "next" as part of the code. along with that, if i do the "a" command first, the "duplicate name" prompt shows up in subsequent command loops... on top of that, i keep getting HASH(x) in place of the names i'm putting in for the secondary STDINs in the "a" and "r" commands. like this:

      SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father Make your choice: a Enter a male name: Bruce Duplicate name -- try again! Invalid choice, TRY AGAIN! SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father Make your choice: a Enter a male name: Phil Add a father: Justin SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father Make your choice: o Duplicate name -- try again! Bruce Richard Clark Jon Jeff Doug Phil HASH(0x6453d0) Robert Jason Thomas Evan Invalid choice, TRY AGAIN! SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father Make your choice: e Duplicate name -- try again! Come back again -- goodbye!

      sorry, this is a lot. >_< i really do want to understand how to make this run perfectly, not just for my class, but just so i can use it again without getting this mixed up.

      and thank you again for being so helpful. it makes me feel really welcome, despite how clueless i can be, haha.

      EDIT: i forgot to mention - if i don't explicitly use "my (variable)" at the beginning of the code, i'm continuously told by my terminal AND my writing program that it isn't explicitly defined at all, regardless of if i used "my" within the chomp or not. i know i should be able to just use the "my" command within the STDIN perimeters, but no matter how i fiddle with it, it just tells me it's wrong!

        lunette:

        You didn't quite get what kcott was telling you about your if statements and indentation. I've reformatted the code a little to hopefully help you see what he means:

        $ cat pm_1202351.pl #! /usr/bin/perl use strict; my %son_father; my $choice; my $name1; my $name2; my $name3; my $name4; my $newname; my $add_dad; %son_father = (Jeff => "Doug", Thomas => "Evan", Robert => "Jason", Bruce => "Richard", Clark => "Jon") ; my $menu = <<MENU; SON_FATHER Hash Operations a- Add a son-father pair d- Delete a son-father pair e- exit the program g- Get a father o- Output the hash neatly r- Replace a father x- get a grand father MENU while (1) { # display the menu print $menu, "\n\n"; # get the user choice print "Make your choice: "; chomp($choice = lc <STDIN>); # fulfill the user request if ($choice eq 'a'){ print "Enter a male name: "; chomp (my $name1 = ucfirst lc <STDIN>); } if (exists $son_father{$name1}) { #46 print "Duplicate name -- try again!\n"; } else { print "Add a father: "; chomp (my $add_dad = ucfirst lc <STDIN>); $son_father{$name1} = {$add_dad}; next; } if ($choice eq 'd') { print "Enter a male name: "; chomp (my $name2 = ucfirst lc <STDIN>); } if (exists $son_father{$name2}) { #59 delete $son_father{$name2}; } else { print "Sorry, couldn't find you -- try again later!"; next; } if ($choice eq 'e') { print "Come back again -- goodbye!"; last; next; } if ($choice eq 'g'){ print "Enter a son's name: "; chomp (my $name4 = ucfirst lc <STDIN>); } if (exists $son_father{$name4}) { #76 print "$son_father{$name4}\n"; next; } if ($choice eq 'o'){ my ($key, $value); foreach $value (sort keys %son_father){ print "$value\t$son_father{$value}\n"; next; } } if ($choice eq 'r'){ print "Enter a male name: "; chomp (my $name3 = ucfirst lc <STDIN>); if (exists $son_father{$name3}) { print "Enter a new father name: "; chomp ($newname = ucfirst lc <STDIN>); $son_father{$name3} = {$newname}; } else { print "Sorry, couldn't find you -- try again later!"; next; } } if ($choice ne 'a', $choice ne 'r', $choice ne 'o', $choice ne 'd', +$choice ne 'e', $choice ne 'g') { print "Invalid choice, TRY AGAIN!\n"; next; } }

        Pay close attention to the lines marked 46, 59 and 76: You're intending them to be inside the prior if block, but it's hard to see that because your braces and indentation aren't consistent with each other.

        ...roboticus

        When your only tool is a hammer, all problems look like your thumb.

        "oh, my gosh, this is more than i expected. thank you so much. T_T"

        You're very welcome.

        Some further responses came to mind as I was reading your reply; however, it appears they've already been covered, so there's no need for me to repeat them. As the update to your initial question indicates that your code is now "working perfectly", I'll assume that additional advice has clarified any outstanding issues.

        You've apologised a number of times for the length of what you've posted. There's really no need: I don't see anything that's excessively long. In general, avoid adding anything not directly related to the question, and keep the code that does demonstrate the problem to a minimum. You can wrap long tracts of code (or data, or output, or indeed anything else you want to post) in <spoiler>...</spoiler> or <readmore>...</readmore> tags: but don't overdo it though — presenting too little can be just as bad as presenting too much.

        "i forgot to mention - if i don't explicitly use "my (variable)" at the beginning of the code, i'm continuously told ... that it isn't explicitly defined at all, ..."

        I suspect that was related to your indentation/nesting problem:

        if (COND1) { my $var = ... } if (COND2) { # $var OUT-OF-SCOPE here }

        That code is really:

        if (COND1) { my $var = ... } if (COND2) { # $var OUT-OF-SCOPE here }

        But the code you needed/intended was:

        if (COND1) { my $var = ... if (COND2) { # $var IN SCOPE here } }

        If you're referring to something else, you'll need to post example code.

        — Ken

        ... i keep getting HASH(x) in place of the names i'm putting in ...
        ... Phil HASH(0x6453d0) ...

        That's because:

        1. You still have statements like
              $son_father{$name1} = {$add_dad};
          which add an (incomplete) hash reference instead of a name; and
        2. You still do not have a
              use warnings;
          statement at the start of your script to enable Perl to tell you (update: or at least give you a hint) about stuff like this.

        I would add that you're also still using a weird indenting scheme that makes the inherent structure of your code very hard for me to follow; I hope it's easier for you, but I suspect not.


        Give a man a fish:  <%-{-{-{-<

        If you indent this block

        if ($choice eq 'a'){ print "Enter a male name: "; chomp (my $name1 = ucfirst lc <STDIN>); } if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; } else { print "Add a father: "; chomp (my $add_dad = ucfirst lc <STDIN>); $son_father{$name1} = {$add_dad}; next; }

        correctly you can see it is 2 blocks not 1

        if ($choice eq 'a'){ print "Enter a male name: "; chomp (my $name1 = ucfirst lc <STDIN>); } if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; } else { print "Add a father: "; chomp (my $add_dad = ucfirst lc <STDIN>); $son_father{$name1} = {$add_dad}; next; }

        What you want is probably

        if ($choice eq 'a'){ print "Enter a male name: "; chomp (my $name = ucfirst lc <STDIN>); if (exists $son_father{$name}) { print "Duplicate name $name -- try again!\n"; } else { print "Add a father: "; chomp (my $add_dad = ucfirst lc <STDIN>); $son_father{$name} = $add_dad; print "$name added with father $add_dad\n"; } next; }
        poj
Re: help with user selected hash operations?
by AnomalousMonk (Archbishop) on Oct 30, 2017 at 05:59 UTC

    Some thoughts:

    • Is the Son => Father key/value relationship that I see as the basic organization of your data structure valid? My first thought would have been Father => Son: A father may have many sons, but a son has only one father (if we ignore step-fathers, fathers-in-law, (update: godfathers,) and similar relationships from other cultures of which I am unaware — and always remembering that it's a wise child that knows its own father).
    • You do not  use warnings; Had you done so, Perl would have warned you that the statement
          $son_father{$name1} = {$add_dad};
      isn't quite kosher:
      c:\@Work\Perl\monks>perl -wMstrict -le "use strict; use warnings; ;; use Data::Dump qw(dd); ;; my %son_father; ;; my $name1 = 'Jeff'; my $add_dad = 'Doug'; ;; $son_father{$name1} = {$add_dad}; ;; dd \%son_father; " Odd number of elements in anonymous hash at -e line 1. { Jeff => { Doug => undef } }
      If you're learning Perl, always  use strict; and  use warnings;
      If you're not learning Perl, always  use strict; and  use warnings;
    • Spurious indentation may be leading you astray. Consider the following re-formatted code:
      while (1) { # display the menu print $menu, "\n\n"; # get the user choice print "Make your choice: "; chomp($choice = lc <STDIN>); # fulfill the user request if ($choice eq 'a'){ print "Enter a male name: "; chomp ($name1 = lc <STDIN>); } if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; } else { print "Add a father: "; chomp ($add_dad = lc <STDIN>); $son_father{$name1} = {$add_dad}; } ... } # end while loop
      Is it more clear that after the  $choice eq 'a' if-test is done, the  exists $son_father{$name1} if-test will always be done and, at least the first time through the loop, will always be false, so the else-clause  print "Add a father: "; will always execute? In a similar way, work through the other conditional statements in the loop body.
    • Update: Perhaps a few print-points for debugging would be helpful, or  print Dumper ...; (see Data::Dumper), or  dd ...; (see Data::Dump)?
    All I can think of for the moment. HTH.


    Give a man a fish:  <%-{-{-{-<

      haha, i do think it would make more sense if it were formatted like that, but the professor has a strict outline for us. but, thank you! i think i managed to solve that specific problem! i'm not getting the repeating message anymore. :)

Organizing your program with subroutines (was Re: help with user selected hash operations?)
by roboticus (Chancellor) on Oct 30, 2017 at 18:29 UTC

    lunette:

    You've already got some good suggestions, so I'll make a different one.

    TL;DR; OK, so I occasionally go off and write a little (ha!) more than I expected to. My coffee took longer than normal to wake me, so by the time I noticed, I had already written all of this stuff. It's not quite as polished as I'd like, but I've got stuff to do, and it's time to move on.

    Anyway, the primary point I wanted to make was that you'd benefit by using some subroutines to organize your code. You can stop reading now, or if you want to see why, then read on...

    Start with an outline

    One of the most important things about programming is to break your big problems up and turn them into smaller problems. This makes 'em easier to write and easier to test. The key is to make some subroutines. Don't worry about them being "too small" or "too large" yet. Just use them to simplify your code a bit.

    When I first started programming, I'd write an outline using comments:

    # initialize variables # Loop # Display menu and choose item # Process menu item: # a: add son/father pair # d: delete son/father pair # e: exit # g: get a father # o: output hash neatly # r: replace a father # x: get a grandfather

    Comments can be important, but later I learned that good variable and subroutine naming makes most comments superfluous. So then I started making my outlines nearly executable by just going ahead and giving my subroutines good names. Let's look at the main body of your code but simplify it to outline form:

    initialize_variables(); my $is_time_to_exit = 0; while (! $is_time_to_exit) { my $choice = choose_menu_item(); if ($choice eq 'a') { add_son_father_pair(); } elsif ($choice eq 'd') { delete_son_father_pair(); } elsif ($choice eq 'e') { $is_time_to_exit = 1; } elsif ($choice eq 'g') { get_a_father(); } elsif ($choice eq 'o') { output_hash_neatly(); } elsif ($choice eq 'r') { replace_a_father(); } elsif ($choice eq 'x') { get_a_grandfather(); } else { print "Invalid choice, TRY AGAIN!\n"; } } print "Come back again -- goodbye!";

    This way it's easy to read--it nearly reads like english. You're not cluttering up your main loop with lots of little bits of code.

    Now you can create each of your subroutines independently. Let's try add_son_father_pair. Plugging in your original code (with a couple tweaks to accomodate it being in a subroutine) would be something like this:

    sub add_son_father_pair { print "Enter a male name: "; chomp (my $name1 = lc <STDIN>); if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; return; } print "Add a father: "; chomp (my $add_dad = lc <STDIN>); $son_father{$name1} = $add_dad; }

    Stub out your routines

    During development, I rarely write the while program in a single pass. Instead, I'll frequently create a "Not Yet Implemented" function for each subroutine that I create so that I don't forget about it. You don't need to do that, as perl will give you an error message if you call a subroutine that doesn't exist. I find it useful, though, because I can easily see what functions I haven't created yet. For functions with particular argument lists, special cases or return values, I can add a little information while I'm thinking about it. To do so, I typically create them like:

    # Nothing special about these: sub add_son_father_pair { die "NYI" } sub delete_son_father_pair { die "NYI" } sub get_a_father { die "NYI" } sub output_hash_neatly { die "NYI" } sub replace_a_father { die "NYI" } sub get_a_grandfather { die "NYI" } # I wanted a bit more detail about this one, though. sub add_daily_task { die "NYI"; my ($task_name, $day_of_week, $task) = @_; # Dies on error, otherwise returns $time_of_day the task is schedul +ed for }

    Putting the extra detail there when you're thinking about it lets you write it down and then forget it. You only have so much room in your head for active thoughts, so you don't want to clutter your head with details that you'll need later. Write 'em down, forget about it, and free up your brain to work on the next chunk.

    Now, any time I need to figure out what is left to be written, I can just:

    $ grep NYI my_perl_script.pl sub add_son_father_pair { die "NYI" } sub delete_son_father_pair { die "NYI" } sub get_a_father { die "NYI" } sub output_hash_neatly { die "NYI" } sub replace_a_father { die "NYI" } sub get_a_grandfather { die "NYI" } sub add_daily_task { die "NYI";

    and see what needs to be written at a glance.

    Break out commonly used code into subroutines

    That's not too bad, but I would simplify it a little further so it reads better. You've got a couple places (and will likely add more when you add options r and x) where you're printing a prompt, fetching a value from the user, changing it to lower case and chomping the line ending. While the code is short, it's just a bit noisy, and we can make it read more like english by making an ask_question subroutine:</c>:

    sub add_son_father_pair { my $name1 = ask_question("Enter a male name: "); if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; return; } my $add_dad = ask_question("Add a father: "); $son_father{$name1} = $add_dad; } sub ask_question { my $question = shift; print $question; chomp(my $answer = lc <STDIN>); return $answer; }

    Keep your momentum and defer problems

    As we just mentioned, we want to put repetitious code into subroutines to simplify and shorten your programs. You can also use subroutines to keep your coding speed up. When you're working at a high level of abstraction, you know the overall operations you want to accomplish. But diving into the details makes you shift perspectives. You can avoid that by simply deferring a particular problem into a subroutine: giving it its own little box for you to solve later, while you're still coding at your higher level of abstraction. Once you finish at your higher level, shift your perspective to another area, and do the same.

    A subroutines value isn't only the ability to reuse code in multiple places: It lets you put boundaries around a problem and give it a name. So when you're debugging your program later, you don't get overwhelmed with details or have to suss out details of multiple levels of your program at once.It turns out that subroutines are often useful even if you use them only once: It lets you put a defer a problem--putting it in its own little box as it were.

    Refactoring your code

    Once you start breaking things up into subroutines, you'll sometimes find that you didn't choose the best place to split things up or use the best name for the subroutine to make it read well. Don't worry about it, just change it. For example, what if you had so many menu choices that you had upper case and lower case choices? Then you couldn't use ask_question for the menu choice.

    One way we could adjust things would be to explicitly use your print "Make your choice: "; chomp(my $choice = <STDIN>); in your choose_menu_item subroutine, which wouldn't be so bad, since it would be isolated in only one location.

    Another choice would be to remove the lc function from ask_question, but that would require that every time you're getting a name from the user, you'd be doing something like:

    my $name1 = lc ask_question("Enter a male name: ");

    That doesn't feel nearly as good as the previous choice, because you'd be having to remember where to use lc and where not to. In your program, though, you're presumably mapping the names to lower case to ensure that you don't have difficulties with your hash keys (i.e. accidental duplicates, or not being able to find a name because it was capitalized in one place and not in another). Since subroutines are cheap, you could capture that requirement by creating a new subroutine to handle that case:

    sub ask_question { my $question = shift; print $question; chomp(my $answer = lc <STDIN>); return $answer; } sub ask_for_name { my $question = shift; return lc ask_question($question); }

    This way, any time you're asking for a name related to your hash, you can use ask_for_name, and any time you don't need the lower-casing, use ask_question. These are the kinds of tradeoffs you'll find when programming. No-one starts out automatically knowing the best bits of code and subroutine names to use--that comes with practice. When you start out, you might reformulate subroutines many times, and later you'll gain experience and make much better guesses.

    What subroutines to create first?

    When you start fleshing out your program as an outline, it's easy to wind up with many functions that aren't implemented yet. How do you know which one(s) to implement first?

    Frequently, it doesn't really matter. Just create them in any order that's comfortable for you. I generally implement whatever feels most comfortable to me at the time. However, I'll often start with the ones that provide useful information for debugging. In your case, I'd implement output_hash_neatly() first. Then I normally choose functions used to alter the data (such as add_son_father_pair or delete_son_father_pair). Since we've already got output_hash_neatly, I can use that in development to start building and testing my code, like this:

    sub add_son_father_pair { # show the "before" data print "add_son_father_pair before\n"; output_hash_neatly(); my $name1 = ask_question("Enter a male name: "); if (exists $son_father{$name1}) { print "Duplicate name -- try again!\n"; return; } my $add_dad = ask_question("Add a father: "); $son_father{$name1} = $add_dad; # Did we do it right? print "add_son_father_pair after:\n"; output_hash_neatly(); }

    I only put all that extra code in if I'm having trouble with a function. Normally, I'll just write it as I expect to be able to use it.

    Another criteria I use to figure out what to write next is by running the script, and letting perl tell me what it wants next:

    $ perl my_perl_script.pl Undefined subroutine &main::choose_menu_item called at my_perl_script. +pl line 7.

    Now I know that to make progress running my program, I'd best implement choose_menu_item! ;^)

    Uh...Conclusion

    Subroutines are a powerful tool to help you organize and develop code. Sometimes they'll make your code longer, but that's not a problem. Breaking your code into logical chunks with good names makes it easier for you (or your colleagues) to understand the code and make successful changes with a mimimum of effort. Yes, subroutines often will pay off with a shorter program, but that's really not the best part, it's just an added bonus to the ability to make your programs clear and maintainable.

    One final hint: When you have a large monolithic chunk of code, you don't need to start over and rewrite it. In fact, that's normally a bad idea--you're spending a lot of time on something that may be less important than your next task. Limit your refactoring to the part of code you need to modify. If your changing a hairy bit of code and can pull it out into a subroutine, that's great. If that bit of code has a chunk that should be turned into a subroutine, then do it. If other parts of the program should also be using that subroutine, then OK, make the change and update the call sites. Remember, however, you need to carefully test all the changes you made in addition to the overall operation of the program, so don't burden yourself with making gratuitous changes.

    I'll frequently add # TODO: extract this into a subroutine comments while I'm working in a program, though, both to inform others of a refactoring opportunity and as a reminder list. After finishing your modification, you can mention to the owner of the program some improvements that can be made, and you may be tasked with making those improvements: either immediately if you have time on your schedule, or when you next have to modify the program.

    Before making any modifications to a program, I'll frequently scan for TODO comments to see if there are any items that could/should be added to the currently specified change(s):

    $ grep TODO my_perl_script.pl # TODO pull these next few lines into a frobnicate() subroutine # TODO Can solve world hunger .. but this line is too short to contain + the solution # TODO Try to remember the solution to above TODO!

    Scanning the TODO items is a good way to help refresh your memory on some internals, and can help you refine requirements a bit for your current change. Sometimes a change is intrusive enough to make many refactorings, so having a TODO list is a good way to make sure you don't miss something obvious.

    Update: Fixed a code tag (<c> --> </c>)

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

      golly, this is a whole lot. i'll look it over more in-depth when i get home from uni. we've only just started going over subroutines today, actually, so i'm sure this will help me out, haha.

      hopefully your coffee woke you up! :) thank you for this!

        he he lunette

        you asked for wisdom and you got it back! ++roboticus for his tutorial/manual.

        Just a minor addition: ellipsis statement described in perlsyn does exactly what sub sub_name { die "NYI" } does: sub sub_name{...} dies with Unimplemented at .. if sub_name is called within the program.

        It is easy to remember and to spot and also saves some keystroke!

        My warmest welcome to the monastery and to the wonderful world of Perl!

        PS for sake of indentation perltidy program which comes within Perl::Tidy module can change your old dinosaur into a cute hamster (incidentally: Perl is taught in university? cool..)!

        L*

        There are no rules, there are no thumbs..
        Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
Re: help with user selected hash operations?
by jahero (Pilgrim) on Oct 30, 2017 at 15:32 UTC