Version 10 of Map in functional programming

Updated 2006-08-07 13:17:46 by NEM

map is function mapping, a classic primitive in functional programming - apply a function to all elements of a list, and return the resulting list:

 proc map {fun list} {
    set res {}
    foreach element $list {lappend res [$fun $element]}
    set res
 } ;# RS

 proc addTax x {expr {$x*1.16}}   

 % map addTax {1 10 5 2 1.95}
 1.16 11.6 5.8 2.32 2.262

IL Fun implementation I just threw together to demo...

 ### the map function and test proc. (yes i know eval is evil)
 proc map { fn list } {
    if { [llength $fn] > 1 } {
        foreach value $list { lappend tcl "[subst $fn];" }
        eval [join $tcl]; return
     }

    foreach value $list { $fn $value }
 }

 proc cap { value } {
        puts [string totitle $value]
 }

 ### test code
 set items { a b c "this is a long var" 1 2 3 }
 map { puts "$value" } $items
 map cap { "john" "mary" }
 map { puts [expr ($value + 5)] } { 1 2 3 4 5 }

Lars H: Since IL's map doesn't return anything (other than the empty string), it doesn't really qualify as a map in the functional programming sense. Rather, the above procedure is a contorted subequivalent of foreach that sends any user straight to quoting hell.


SS 29Oct2004. I'm going to submit a TIP for the inclusion of a map command in Tcl. The semantic is very similar to foreach, with three main differences:

 map varname list body
 map varlist1 list1 ?varlist2 list2 ...? body
  • map raises an error if the length of every list is not multiple of the number of variables passed.
  • map raises an error if the number of iterations needed to consume all the lists passed is not the same.
  • map returns a list composed appending to every iteration the result of the script passed.

Some usage example:

 % map x {1 2 3 4} {expr $x*$x}
 1 4 9 16
 % map {x y} {1 2 3 4} {expr $x+$y}
 3 7
 % map x {1 2 3 4} y {5 6 7 8} {expr $x+$y}
 6 8 10 12
 % map x {pela mera arancia limone} {string toupper $x}
 PELA MERA ARANCIA LIMONE

Rationale

I've a strong feeling that the semantic I proposed for map is the best we can have in relation to the rest of Tcl, and the following are the reasons.

  • The formal command structure is very similar to foreach. map and foreach are also very similar in the Scheme Lisp dialect. Every Tcl programmer that can understand foreach will be able to use map in 1 minute.
  • There is no need for lambda, that's missing in Tcl. A more idiomatic way in Tcl is to pass a function direcly as a string.
  • It shares the power of foreach about the ability to map multiple lists in different ways, with commands having different arity (see examples).

Do you need lambda? If you can see there is the following pattern in your programs, you need it:

 set res {}
 foreach x $mylist {
   lappend res [foobar $x]
 }
 use $res

(You probably also need Filter in functional programming, that will be probably covered in the same TIP).

Implementation

I wrote the following reference implementation, that's released under the BSD license:

 proc map args {
     set argc [llength $args]
     # Check arity
     if {$argc < 3 || ($argc % 2) != 1} {
         error {wrong # args: should be "map varList list ?varList list? script"}
     }
     set listNum [expr {($argc-1)/2}]
     set numIter -1
     # Check if number of vars matches the length of the lists
     # and if the number of iterations is the same for all the lists.
     for {set i 0} {$i < $listNum} {incr i} {
         set varList [lindex $args [expr {$i*2}]]
         set curList [lindex $args [expr {$i*2+1}]]
         if {[llength $curList] % [llength $varList]} {
             error "list length doesn't match varList in arg # [expr {$i*2+1}]"
         }
         set curNumIter [expr {[llength $curList]/[llength $varList]}]
         if {$numIter == -1} {
             set numIter $curNumIter
         } elseif {$numIter != $curNumIter} {
             error "different number of iterations for varList/list pairs"
         }
     }
     # Performs the actual mapping.
     set script [lindex $args end]
     set res {}
     for {set iter 0} {$iter < $numIter} {incr iter} {
         for {set i 0} {$i < $listNum} {incr i} {
             set varList [lindex $args [expr {$i*2}]]
             set curList [lindex $args [expr {$i*2+1}]]
             set numVars [llength $varList]
             set listSlice [lrange $curList [expr {$numVars*$iter}] \
                                             [expr {$numVars*$iter+$numVars-1}]]
             uplevel 1 [list foreach $varList $listSlice break]
         }
         lappend res [uplevel 1 $script]
     }
     return $res
 }

DKF: All the above is over elaborate (and classic map doesn't do striding). Here's a simple version that leverage's Tcl 8.5's apply command:

 proc map {lambdaExpression list} {
    set res {}
    foreach element $list {lappend res [apply $lambdaExpression $element]}
    return $res
 }

Demonstrating...

 % map {x {expr {$x*1.16}}} {1 10 5 2 1.95}
 1.16 11.6 5.8 2.32 2.262
 % map {x {expr {$x*1.16}}}
 wrong # args: should be "map lambdaExpression list"

See? Even the error message is informative!

Given the simplicity of coding involved, writing a fancy version isn't really justified.


See also in tcllib: struct::list map, struct::list filter and struct::list fold.

NEM notes that the tcllib versions take their arguments in an inconvenient order: taking the sequence (list) before the command. This makes it awkward to use them with interp alias. My own map is this:

 proc map {func list} {
     set result [list]
     foreach elem $list {
         lappend result [uplevel #0 [linsert $func end $elem]]
     }
     return $result
 }

Demo:

 % proc + {a b} { expr {$a + $b} }
 % map {+ 1} {1 2 3 4 5}
 2 3 4 5 6
 % interp alias {} succs {} map {+ 1}
 succs
 % succs {1 2 3 4 5}
 2 3 4 5 6

Category Algorithm|Category Functional Programming