Version 0 of More functional programming

Updated 2004-03-20 04:12:48

NEM 20Mar2004 - I've been thinking a bit recently about some of the ideas on this wiki for doing functional programming in Tcl, like at Lambda in Tcl, and thought I'd add yet another take on it. This method also combines some of the more radical ideas from Getting rid of the value/command dichotomy for Tcl 9 - namely unifying the variable/command namespaces, and slightly changing the command resolution procedure to allow auto-expanding of leading words (this makes functions as values easier to do, as shown below). The package below implements all of these ideas. It's not very efficient (lambdas aren't byte-compiled, relies on [unknown] etc), but it's quite fun to play with. Also provided are some common higher-order functions (map, filter, compose, foldl, foldr). Hope you like it!

To use, just package require and namespace import -force funtcl::* (you don't have to import, but it makes things a bit easier, and registers the unknown handler).


 # funtcl --
 #
 #   Morphs the current interpreter into one based on functional programming
 #   ideas. Does a number of things:
 #   - Unifies command and variable namespaces
 #   - Defines a [lambda] command for defining first-class function values
 #   - Redefines [proc] in terms of lambda
 #   - Supplies some common higher-order functions
 #

 package provide funtcl 0.1

 namespace eval funtcl {
     namespace export lambda proc map filter foldl foldr unknown compose
 }
 # First up - create the lambda command
 interp alias {} ::funtcl::lambda {} list ::funtcl::apply

 # Apply - used for the lambda. This and the next proc go to rather absurd
 # lengths to make this work just like a proc - no variables are visible to the
 # body of the lambda other than those which are defined by argument passing,
 # and any which are retreived through global/upvar etc. The lambda always
 # executes in the funtcl namespace though...
 proc funtcl::apply {arglist body args} {
     # This bit of magic ensures that only args defined in the arglist actually
     # get defined (as far as the body is concerned).
     ApplyArgs [K $body [unset body]] \
         [K $arglist [unset arglist]] [K $args [unset args]]
 }

 # Sets up variables in the callers scope according to the argument spec given,
 # and the actual arguments passed. This should behave exactly like Tcl's
 # argument passing to procedures.
 proc funtcl::ApplyArgs {body argspec arglist} {
     set index 0
     array set ret {}
     foreach item $argspec {
         if {$index < [llength $arglist]} {
             set ret([lindex $item 0]) [lindex $arglist $index]
         } elseif {[llength $item] == 2} {
             set ret([lindex $item 0]) [lindex $item 1]
         } else {
             # Possibly not enough args
             if {$item eq "args" && $index == ([llength $argspec]-1)} {
                 # ok
                 set ret(args) [list]
             } else {
                 # Build up a good error message
                 set err "wrong # args: should be \""
                 # Grab how we were invoked from [info level]
                 set cmdname [lindex [info level -2] 0]
                 append err $cmdname
                 foreach item $argspec {
                     if {[llength $item] == 1} {
                         append err " $item"
                     } else {
                         append err " ?${item}?"
                     }
                 }
                 append err "\""
                 error $err
             }
         }
         incr index
     }
     # Any left over?
     if {$index < [llength $arglist]} {
         if {[info exists ret(args)]} {
             for {set i $index} {$i < [llength $arglist]} {incr i} {
                 lappend ret(args) [lindex $arglist $i]
             }
         } else {
             error "too many args to lambda"
         }
     }
     # Add these all to caller's scope
     foreach {name value} [array get ret] {
         upvar 1 $name n
         set n $value
     }
     # Finally, uplevel the body
     uplevel 1 $body
 }

 # Redefine proc in terms of set and lambda
 proc funtcl::proc {name arglist body} {
     uplevel 1 [list set $name [lambda $arglist $body]]
 }

 # Maps a function to each element of a list, and returns a list of the
 # results.
 proc funtcl::map {func list} {
     set ret [list]
     foreach item $list {
         lappend ret [eval $func [list $item]]
     }
     return $ret
 }

 # Filters a list, returning only those items which pass the filter.
 proc funtcl::filter {func list} {
     set ret [list]
     foreach item $list {
         if {[eval $func [list $item]]} {
             lappend ret $item
         }
     }
     return $ret
 }

 # Useful higher-order functions which replace common uses of recursion
 # foldl (fold left)
 # foldl - 0 {1 2 3} -> ((0-1)-2)-3
 proc funtcl::foldl {func default list} {
     set res $default
     foreach item $list {
         set res [eval $func [list $item $res]]
     }
     return $res
 }

 # foldr (fold right)
 # foldr + 0 {1 2 3} -> 1+(2+(3+0))
 proc funtcl::foldr {func default list} {
     set tot $default
     # Go in reverse
     for {set i [llength $list]} {$i > 0} {incr i -1} {
         set tot [eval $func [list [lindex $list [expr {$i-1}]] $tot]]
     }
     return $tot
 }

 # compose - compose two functions together
 # [compose f g] $args -> f [g $args]
 proc funtcl::compose {f g} {
     return [lambda {args} "$f \[$g \$args\]"]
 }
 # The K combinator - obscure, but very useful.
 proc funtcl::K {a b} { set a }

 # The unknown handler which makes the magic work...
 proc funtcl::unknown {cmd args} {
     if {[lindex $cmd 0] eq "::funtcl::apply"} {
         uplevel 1 $cmd $args
     } elseif {[uplevel 1 [list info exists $cmd]]} {
         # It's a var
         uplevel 1 [uplevel 1 [list set $cmd]] $args
     } elseif {[uplevel #0 [list info exists $cmd]]} {
         # Global var (we keep same command resolution scheme as Tcl)
         uplevel 1 [uplevel #0 [list set $cmd]] $args
     } else {
         # Call Tcl's usual unknown handler
         uplevel 1 [list funtcl::orig_unknown $cmd] $args
     }
 }

 # Save the original unknown command for later use
 rename ::unknown funtcl::orig_unknown
 # And alias it back for now
 interp alias {} ::unknown {} ::funtcl::orig_unknown

A few examples of use:

 % package require funtcl
 0.1
 % namespace import -force funtcl::*
 % set + [lambda {a b} { expr {$a + $b} }]
 ::funtcl::apply {a b} {expr {$a + $b}}
 % # Use the foldr function to make a sum command
 % set sum {foldr + 0}
 foldr + 0
 % sum {1 2 3}
 6
 % # Use compose, to create a command which sums a sorted list (!)
 % set sum_sorted [compose sum lsort]
 ::funtcl::apply args {sum [lsort $args]}
 % sum_sorted {3 4 1}
 8
 % # Demonstrate how [proc] is now defined in terms of lambda
 % proc foo {} { puts "Hello, World!" }
 ::functcl::apply {} { puts "Hello, World!" }

Well, you get the picture. Everything mostly works as you would expect. There may be some edge cases left still - but this is just a toy!