Beefy Boxes and Bandwidth Generously Provided by pair Networks
good chemistry is complicated,
and a little bit messy -LW
 
PerlMonks  

Using perl to teach programming

by Falkkin (Chaplain)
on Jan 30, 2001 at 20:59 UTC ( #55265=CUFP: print w/ replies, xml ) Need Help??

My CUFP: Using perl to write an interactive perl tutorial.

Quite a while ago, in this node, I talked about the possibility of teaching perl to a couple of novice programmers.

ichimunki's response to my post was very enlightening... I agree that it'd be very useful to have a more interactive way for newbie programmers to just type stuff in and have it evaluated immediately. Here's my solution... a simple perl "shell" of sorts that accepts commands and evals them. The typed-in code is evaluated in its own separate package (WorkSpace), which allows me to do a couple neat tricks:

- Dumping of variables and subs. The users of my program can use the vars command to get a listing of the present states of all variables.

- An undo command. I save the state of the WorkSpace package's symbol table at the beginning of each call to my evaluate sub. These undo command pops a state off of the @undo_stack array and uses that to restore the state of the symbol table.

Eventually, I'd like to make the tutor more interactive by providing it with the ability to load and run tutorial scripts that interactively walk the user through the perl basics.

Most recent updates:
- The tutor now allows loading from and saving to files.
- The new display command displays the entire contents of what you've typed so far.
- The new edit command allows for editing your code in an editor of your choice. Defaults to notepad on a Win32 machine and your EDITOR environment variable (or vi) everywhere else.
- Fixed the undo/redo bug and allowed for essentially unlimited undo levels.
- Improved block mode that reads from Term::ReadLine instead of pure STDIN. Perhaps block mode isn't necessary anymore, though, since the edit command is now available.
- Fixed a bug whereby an empty line was interpreted as an error. Now, entering in nothing at a prompt simply spits out another prompt.

A couple minor bugs:
- edit has a race condition, which I am ignoring for now. Will fix it eventually. ;)
- save might produce non-compiling code, if the user has been entering in lines that don't end with semi-colons but should.

A sample run through the tutor:

Welcome to the Perl Tutor! Special commands are: (H)elp Get online help. (L)oad Load and evaluate the contents of the file. (V)ars Look at the present values of variables. (C)lear Clears the values of all variables and removes all undo da +ta. (B)lock Type in more than one line of code at a time. (U)ndo Undo last Perl command. (Q)uit Quit the Perl Tutor. Anything which is not a special command will be evaluated as Perl code +. > sub foo { $_ = shift; return $_ x 3 } > $bar = foo("42 "); > @baz = (1, 2, 3, 4); > %quux = @baz > vars $bar = "42 42 42 " @baz = (1, 2, 3, 4) sub foo { ... } %quux = (1 => 2, 3 => 4) > undo Successfully executed `undo' command. > vars $bar = "42 42 42 " @baz = (1, 2, 3, 4) sub foo { ... } > quit

And here, finally, is the code for the tutor:

#!/usr/bin/perl -w use strict; use Term::ReadLine; my $term = new Term::ReadLine 'Perl Tutor'; my $OUT = $term->OUT || *STDOUT; print "Welcome to the Perl Tutor!\n\n"; print_commands(); my @undo_stack; my @history; LINE: while (1) { $_ = $term->readline("> "); chomp; next LINE unless /./; help(), next LINE if /^h$/i || /^help/i; edit(), next LINE if /^e$/i || /^edit/i; vars(), next LINE if /^v$/i || /^vars/i; block(), next LINE if /^b$/i || /^block/i; load(), next LINE if /^l$/i || /^load/i; save(), next LINE if /^s$/i || /^save/i; clear(), next LINE if /^c$/i || /^clear/i; undo(), next LINE if /^u$/i || /^undo/i; re_do(), next LINE if /^r$/i || /^redo/i; display(), next LINE if /^d$/i || /^display/i; exit() if /^q$/i || /^quit/i; evaluate($_); } sub print_commands { print <<'DONE'; Special commands are: (H)elp Get online help. (V)ars Look at the present values of variables. (C)lear Clears the values of all variables and removes all undo da +ta. (B)lock Type in more than one line of code at a time. (U)ndo Undo last Perl command. (R)edo Redo the last `undo' command. (D)isplay Display the code you've entered so far. (L)oad Load and evaluate the contents of a file. (S)ave Save the code you've entered so far into a file. (E)dit Edit your code in the editor of your choice. (Q)uit Quit the Perl Tutor. Anything which is not a special command will be evaluated as Perl code +. DONE } sub load { my $filename = $term->readline("Please enter a filename to load: "); load_from_file($filename); } sub load_from_file { my $filename = shift; open (FILE, $filename) or return print "Could not load file $filenam +e: $!\n"; my @data = <FILE>; close(FILE) or return print "Could not close file $filename: $!\n"; my $data = join "", @data; chomp $data; clear(); print "File $filename loaded successfully.\n"; evaluate($data); } # This bit is insecure to race conditions. I'll try to fix that later, # but it shouldn't be a huge problem. sub edit { my $loaded = 0; my $filename; until ($loaded) { $filename = ".perl-tutor-$$-" . int(rand(2**16)) . ".tmp"; $loaded = 1 unless -e $filename; } my $default_editor = $ENV{EDITOR}; unless ($default_editor) { $default_editor = "vi"; $default_editor = "notepad.exe" if $^O =~ /Win32/i; } my $editor = $term->readline("Enter the name of an editor you would +like to use.\n[Default: $default_editor]: "); $editor = $editor || $default_editor; save_to_file($filename); system($editor, $filename); load_from_file($filename); unlink($filename) or return print "Could not remove temporary file $ +filename: $!\n"; } sub save { my $filename = $term->readline("Please enter a filename to save to: +"); save_to_file($filename); } sub save_to_file { my $filename = shift; open (FILE, ">$filename") or return print "Could not save to file $f +ilename: $!\n"; print FILE join("\n", @history); close(FILE) or return print "Could not close file $filename: $!\n"; } sub display { print join("\n", @history); print "\n"; } sub block { my @block = (); print "Entering Block Mode.\nEnter in as many lines of code as you w +ant; type `done' on a blank line when done.\n\n"; while (1) { $_ = $term->readline("block> "); chomp; last if /^done$/; push @block, $_; } evaluate(@block); } sub clear { %WorkSpace:: = (); @undo_stack = (); @history = (); print "Successfully cleared all variables and removed all undo data. +\n" } sub help { print "Eventually, a nice help message will go here.\n\n"; print_commands(); } sub quote_strings { my @return_val; foreach (@_) { my $scalar = $_; $scalar = "\"$_\"" unless $scalar =~ /^[0-9.]+$/; push @return_val, $scalar; } return @return_val if wantarray; return $return_val[0]; } sub undo { my @undo_data = @history; return print "Could not undo, because there is no undo data availabl +e.\n" unless @undo_data; push @undo_stack, \@undo_data; my @old_history = @history; pop @old_history; @history = (); %WorkSpace:: = (); evaluate(@old_history); } sub re_do { my $history_ref = pop @undo_stack; return print "Could not redo, because there is no redo data availabl +e.\n" unless $history_ref; my @new_history = @$history_ref; @history = (); %WorkSpace:: = (); evaluate(@new_history); } no strict; sub evaluate { package WorkSpace; eval(join ("", @_)); print "Unrecognized command or syntax error. (Type `help' for help.) +" if $@; push @history, @_ unless $@; print "\n"; package main; } sub vars { package WorkSpace; my $varcount = 0; foreach my $symname (sort keys %WorkSpace::) { local *sym = $WorkSpace::{$symname}; if (defined $sym) { $varcount++; my $scalar = main::quote_strings($sym); print "\$$symname = $scalar\n"; } if (defined @sym) { $varcount++; my @array = main::quote_strings(@sym); print "\@$symname = ("; print join(", ", @array); print ")\n"; } if (defined %sym) { $varcount++; print "\%$symname = ("; my $output; foreach my $key (sort keys %sym) { my $value = main::quote_strings($sym{$key}); $output .= "$key => $value, "; } chop $output; chop $output; print "$output)\n"; } if (defined &sym) { $varcount++; print "sub $symname { ... }\n"; } } print "No variables are currently defined.\n" unless $varcount; package main; }

Comment on Using perl to teach programming
Select or Download Code
Re: Using perl to teach programming
by japhy (Canon) on Jan 30, 2001 at 21:18 UTC
    Since you're eval()ing the code they enter to set the variables and such, you can directly reproduce the code of the subroutine, as long as you request that they enter ONE subroutine at a time, and that subroutines are entered by themselves. This also does not work for code references. But it is a start.
    # in evaluate() if (/^\s*sub\s+(\S+)/) { $WorkSpace::functions{$1} = $_; }


    japhy -- Perl and Regex Hacker
      Thanks... that is a good idea. I think what I might do is this:

      Have a @code_lines array keep track of each line of code as the user enters it. This would allow me to implement a save command where the user can save their code to a file for later use. I'd need to pop values off of @code_lines for every undo command.

      Have a new subs command that retrieves everything that looks like a subroutine definition from @code_lines and prints out their contents.

Re: Using perl to teach programming
by stefan k (Curate) on Jan 30, 2001 at 22:25 UTC
    Respect!!
    This seems to be a maximum of cool use for a minimum of coding.

    As a matter of fact I have just three weeks ago taught a school girl (some 16 years old) the beginnings of programming and had chosen Perl for that because of the easyness (no declarations, variable types and the like). One of the results was a genetic algorithm within a week (Hello World GA, OK, I cleaned the code a bit ;-)
    I could well imagine that the playing stage will be improved by your tutor because you can skip XEmacs and Bash for the beginning :-)

    Well done, I think!
    Regards
    Stefan K

    $dom = "skamphausen.de"; ## May The Open Source Be With You! $Mail = "mail@$dom; $Url = "http://www.$dom";
Re: Using perl to teach programming
by t'mo (Pilgrim) on Jan 30, 2001 at 22:53 UTC

    Why only 32 levels of undo, especially if you're using an array as a stack? Zero, one, or infinite levels of undo is probably the best way to go. Check out the "command" design pattern for ideas. You could also easily add redo.

      I intended for the levels of undo to be configurable. Upon further thought, I think I'll make the default be 256 levels.

      Thanks for the suggestion on redo... I'll definitely want to implement that.

Re: Using perl to teach programming
by ichimunki (Priest) on Jan 31, 2001 at 02:21 UTC
    Great start!

    One nit, ctrl-d is ctrl-z on windows, if memory serves-- and you have to watch handling input done this way. It's a tad idiosyncratic, but consistent.

      Thanks much for the suggestion... I'd forgotten about that, having not tested the program under windows recently. Again, I'd like to eliminate reading from normal STDIN entirely since it doesn't provide a good way of going back and altering what you've previously entered (aside from undoing the entire block afterwards.)
        You could store the script lines in a list instead of a single scalar, then using (ack!) line numbers, provide a way to list, modify, add (remember BASIC?), or delete lines. This way a tutorial can build up a routine. Then at the end, you can save the whole thing as a real Perl ".pl" script in an external file.
Re: Using perl to teach programming
by $code or die (Deacon) on Jan 31, 2001 at 13:59 UTC
Re: Using perl to teach programming
by raptor (Sexton) on Feb 05, 2001 at 13:03 UTC
    hi Falkkin, all,
    I read your script ...cool :") and here are some extentioon I've written, plus some docs (perldoc tutorial.pl).
    And here is the rewriten code ... enjoy...
    All changes are marked with comment #*changed AND all new sub's with #*added
    #!/usr/bin/perl use strict; use Term::ReadLine; my $term = new Term::ReadLine 'Perl Tutor'; my $OUT = $term->OUT || *STDOUT; print "Welcome to the Perl Tutor!\n\n"; print_commands(); my $undo_level = 250; my @undo_data; my @redo_data; # It will be cool if someone extend this so we can create interactive # tutorials...:") ..I don't have much time at the moment and I'm lazy + :") #... f.e. www.perl.com had 6 articles for the beginers... # before a couple of weeks .. # if tutorial mode extract next item from .tut file # and do what the teacher say.. ;') # expect user to do something, correct him if wrong...etc.. # IDEAS ?!?! what about including MS Office wizards :") he he LINE: while (1) { $_ = $term->readline("> "); chomp; help(), next LINE if /^h$/i || /^help/i; vars(), next LINE if /^v$/i || /^vars/i; block(), next LINE if /^b$/i || /^block/i; load(), next LINE if /^l$/i || /^load/i; clear(), next LINE if /^c$/i || /^clear/i; undo(), next LINE if /^u$/i || /^undo/i; re_do(), next LINE if /^r$/i || /^redo/i; #*added printBlock($1), next LINE if /^bp.*$/i; evalBlock($1), next LINE if /^be.*$/i; editLineInBlock($1), next LINE if /^eb.*$/i; delBlock($1), next LINE if /^bd.*$/i; loadBlock($1), next LINE if /^bl.*$/i; execOnFile($1), next LINE if /^e\|.*$/i; exit() if /^q$/i || /^quit/i; evaluate($_); } sub print_commands { print <<'DONE'; Special commands are: (H)elp Get online help. (L)oad Load and evaluate the contents of the file. (V)ars Look at the present values of variables. (C)lear Clears the values of all variables and removes all undo dat +a. (B)lock Type in more than one line of code at a time. (bp$ID)Block print -=> print block with ID OR all (be$ID)Block evaluate -=> evaluate block with ID (bd$ID)Block delete -=> delete block with ID (bl$ID)Block load -=> load file into block (eb$ID|$line)Edit Block -=> Edit block ID - line $line (E|$fname)xec on File -=> execute the commands that follow on file - +$fname (U)ndo Undo last Perl command. (R)edo Redo the last `undo' command. (Q)uit Quit the Perl Tutor. Anything which is not a special command will be evaluated as Perl code +. DONE } #*chnaged sub load { my $inBlock = shift; my $filename = $term->readline("Enter a filename: "); open (FILE, $filename) || return print "Could not load file $filenam +e: $!\n"; my @data = <FILE>; close(FILE); my $data = join "\n", @data; print "File $filename loaded successfully.\n"; if ($inBlock) { $Blocks::{"id".$inBlock} = $data } else { evaluate($ +data) }; } #*changed sub block { print "Entering Block Mode.\nEnter in as many lines of code as you w +ant; press Ctrl-D or Ctrl-Z when done.\n\n"; my @input = <STDIN>; my $input = join "\n", @input; $Blocks::count++; $Blocks::{"id".$Blocks::count} = join "", @input;; evaluate($input); } #*added sub printBlock { (my $id) = $_ =~ /(\d+)/; if ($id) { if ($Blocks::{"id".$id}) { print qq{$Blocks::{"id".$id}\n} } else { print "Block with ID : $id doesn't exists\n" }; return }; for my $k (sort grep {/^id/} keys %Blocks:: ) { print qq{Block ID : $k\n$Blocks::{$k}\n#=======----------|\n +} }; }; #*added sub evalBlock { (my $id) = $_ =~ /(\d+)/; if ( $id && $Blocks::{"id".$id}) { evaluate($Blocks::{"id".$id}) ; print "Block $id evaluated successfully\n" } else { print "Nothing evaluated, may be block with that ID doe +sn't exists\n" }; }; #*added sub delBlock { (my $id) = $_ =~ /(\d+)/; delete $Blocks::{"id".$id} if $id; }; #*added sub loadBlock { (my $id) = $_ =~ /(\d+)/; load($id) if $id; }; #*added, someone to write the EDITOR :") or we have to use temporary f +ile !!! # think also for Windows :"( sub editLineInBlock { (my ($id,$line)) = $_ =~ /eb(\d+?)\|(\d+)/; return unless $id; my @block = split "\n", $Blocks::{"id".$id}; print "Block $id old Line number $line:\n$block[$line]\n"; my $newline = $term->readline("New line content : "); $block[$line] = $newline; $Blocks::{"id".$id} = join "\n", @block; print "Line updated...\n"; }; #*added sub execOnFile() { no strict; (my ($fname,$rest)) = $_ =~ /^e\|([^\s]+?)\s+?(.*)$/; unless ($fname || $rest ) {print "No filename/action specified !!! +\n"; return}; # I'm not sure where is better to store the filename in MAIN or i +n WORKSPACE ?!! # 'cause WorkSpace is cleared... but until U deicde let it be ... +. if ($fname =~ /^\$(.*)/) { $fname = ${$WorkSpace::{$1}}; unless ($fname) {print "Variable \$$1 which is supossed to con +tain the filename is not defined\n"; return} }; my $action = eval "sub { $rest }"; save_state(); package WorkSpace; open FILE, "<$fname"; while (<FILE>) { &$action($_) }; close FILE; print "\n"; package main; }; sub clear { %WorkSpace:: = (); @undo_data = (); print "Successfully cleared all variables and removed all undo data. +\n" } sub help { print "Eventually, a nice help message will go here.\n\n"; print_commands(); } sub quote_strings { my @return_val; foreach (@_) { my $scalar = $_; $scalar = "\"$_\"" unless $scalar =~ /^[0-9.]+$/; push @return_val, $scalar; } return @return_val if wantarray; return $return_val[0]; } sub undo { save_state(); my $current_state = pop @undo_data; my $undo_state = pop @undo_data; return print "Could not undo, because there is no undo data availabl +e.\n" unless $undo_state; push @redo_data, $current_state; my %state = %{ $undo_state }; %WorkSpace:: = %state; print "Successfully executed `undo' command.\n"; } sub re_do { my $state_ref = pop @redo_data; return print "Could not redo, because there is no redo data availabl +e.\n" unless $state_ref; push @undo_data, $state_ref; shift @undo_data if @undo_data > $undo_level; my %state = %{ $state_ref }; %WorkSpace:: = %state; print "Successfully executed `redo' command.\n"; } sub save_state { my %saved_data; foreach my $key (keys %WorkSpace::) { $saved_data{$key} = $WorkSpace::{$key}; } push @undo_data, \%saved_data; shift @undo_data if @undo_data > $undo_level; } no strict; sub evaluate { save_state(); package WorkSpace; $_ = shift; eval($_); print "Unrecognized command or syntax error. (Type `help' for help.) +" if $@; print "\n"; package main; } sub vars { package WorkSpace; my $varcount = 0; foreach my $symname (sort keys %WorkSpace::) { local *sym = $WorkSpace::{$symname}; if (defined $sym) { $varcount++; my $scalar = main::quote_strings($sym); print "\$$symname = $scalar\n"; } if (defined @sym) { $varcount++; my @array = main::quote_strings(@sym); print "\@$symname = ("; print join(", ", @array); print ")\n"; } if (defined %sym) { $varcount++; print "\%$symname = ("; my $output; foreach my $key (sort keys %sym) { my $value = main::quote_strings($sym{$key}); $output .= "$key => $value, "; } chop $output; chop $output; print "$output)\n"; } if (defined &sym) { $varcount++; print "sub $symname { ... }\n"; } } print "No variables are currently defined.\n" unless $varcount; package main; } #*added to remembering U that it exists :") BEGIN { $WorkSpace::f = "block.html"; }; __END__ =head1 DESCRIPTION ... very cool idea briliant realization (that's why Perl ROCKS!! :"), I have also to teach some people on PERL, so ....blah, blah .... write something here :") .... Hey, lazy people use BEGIN{} block to add code that you want to be executed every time, config data etc... =head2 EXAMPLES e|c:\test.txt print -=> print c:\test.txt line by line. The "e|filename actions" command excutes actions U specified on every line of the file. [ the same like : while(<FILE>) {...actions.. }. In the $_ as U may expect :") is current line ] e|$filename s/<.*?>/g;print -=> strip HTML tags from the file pointed by the var $filename (if U are lazy like me, don't type the filename every time :") ) bp -=> every time when U execute "b"/"block" command or load file into block with "bl" this block of code is stored in Blocks package as string. So with "bp" all blocks are printed to STDOUT. bp3 -> print block number 3. The block numbers begin with 1 and are automaticaly incremented after creating new block. bl -=> load file into block. The file is not evaluated as this happen when U load file with "l"/"load" command. -> bl5 - load the file into block number 5 bd7 -=> delete block number 7. be2 -=> once U have block loaded/created U can evaluate it. eb2|4 -=> replace the line 4 from block 2 with the text U enter. =cut
Re: Using perl to teach programming
by raptor (Sexton) on Feb 05, 2001 at 16:57 UTC
Re: Using perl to teach programming
by Anonymous Monk on Feb 13, 2001 at 03:09 UTC
    Falkkin

    Your perl tutor is great, just what I needed to teach a simple Perl 101 class. I had one additional requirement though, I wanted to allow anyone to do simple Perl evals without installing Perl or having a logon to a box with Perl installed. I also wanted to be able to demo simple Perl constructs real time for people without having to log into a shell.

    What I came up with, after being inspired by your post, was a simple CGI that uses evel to do just that. Because the CGI is running in a trusted environment, we don't have to worry about people running malicous code ('cos if they do, I know where they live ;-)

    So without further ado, here's my own simple solution (note that the name of the textarea element on the HTML form that sits in front of this code should be "code"):

    #!/bin/perl ################################################################## # Execute perl statements passed in via a CGI form # ################################################################## # This script uses the cgi-lib.pl CGI library # available at http://cgi-lib.berkeley.edu/ require 'cgi-lib.pl'; ############ MAIN ############### # Print HTTP Header print &PrintHeader; # Point STDERR to STDOUT open (STDERR, ">&STDOUT"); # Flush STDERR and STDOUT select STDERR; $| = 1; select STDOUT; $| = 1; # Parse CGI arguments &ReadParse(*in); printTop(); evaluate($in{'code'}); printFoot(); exit 0; ############ SUBS ############### # converts any HTML special characters in the # code to the equivelant HTML escape characters # also adds line numbers to the code so that you can see # what's going on sub HTMLize { my $code = shift; # conversion $code =~ s/</&lt;/g; $code =~ s/>/&gt;/g; # add line numbers # make sure we have a \r\n at the end of the code $code .= "\r\n"; $code =~ s(.*\r\n)("<li>$&")ge; return $code; } # Print the top portion of the HTML page sub printTop { my $code = HTMLize($in{'code'}); print <<"END_OF_HTML" <html> <head> <title>WebQA TNT: Perl 101 - PerlRun</title> <style> OL { margin-top : 0%; margin-bottom : 0%; } CODE { margin-top : 0%; margin-bottom : 0%; } PRE { margin-top : 0%; margin-bottom : 0%; } </style> </head> <body bgcolor=white> <h1>WebQA TNT: Perl 101 - PerlRun</h1> <hr> <b>Evaluating code:</b> <code> <pre> <ol> $code </ol> </pre> &lt;/code> <hr> <b>Results of the evaluation are:</b> <p> <font color=blue> <pre> END_OF_HTML } # print the bottom portion of the html code sub printFoot { print <<'END_OF_HTML' </pre> </font> <hr> </body> </html> END_OF_HTML } # use an eval statement to actually execute the # statement passed in sub evaluate { package WorkSpace; $_ = shift; eval($_); print "<font color=red>Unrecognized command or syntax error.<br>$@</ +font><br>\n" if $@; print "<br>\n"; package main; }

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: CUFP [id://55265]
Approved by root
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others romping around the Monastery: (7)
As of 2014-09-23 09:15 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    How do you remember the number of days in each month?











    Results (216 votes), past polls