Commands as Values in Jim

gchung Regarding "Getting rid of the value/command dichotomy for Tcl 9", here's a mock up of what that might look like in Jim.

Jim provides lambdas, which gives us a way to store commands in variables, like so:

set square [lambda {x} {expr $x*$x}]
set cmd $square
$square 6 ;# => 36
$cmd 7    ;# => 49

I'd like the language to work as follows:

proc square {x} {expr $x*$x}
set cmd $square
square 6 ;# => 36
cmd 7    ;# => 49

One way to mock up "command as values" in Jim is to:

  1. Redefine proc so that it stores a lambda into a variable.
  2. Have unknown auto-dereference the the first argument.

That's not too complicated. Here's the code:

# We will redefine [proc] so move the original command to [builtin::proc].
rename proc builtin::proc

# Redefine [lambda] to use [builtin::proc] instead of [proc].
builtin::proc lambda {arglist args} {
  tailcall builtin::proc [ref {} function lambda.finalizer] $arglist {*}$args
}

# Use [unknown] to auto-dereference the first argument of a command list.
builtin::proc unknown {cmdname args} {
  set depth [info level]
  
  # Look through the call chain for the command reference.
  for {set level 1} {$level <= $depth} {incr level} {
    upvar $level $cmdname cmdref
    if {[exists -var cmdref]} break
  }

  if {$level > $depth} {error "can't read \"$cmdname\": no such variable"}
  if {![exists -command $cmdref]} {error "\"$cmdname\" is not callable"}
  tailcall [set cmdref] {*}$args 
}

# Define [proc] - Store a lambda in a variable whose name is the command name.
set ::proc [lambda {name args} {
  # [uplevel] lets us capture static variables in the caller's context.
  tailcall set $name [uplevel 1 [list lambda {*}$args]]
}]

# test
proc square {x} {expr $x*$x}
set cmd $square
cmd 7 ;# => 49

Here's another way to define proc which tags the ref with the command name (which I prefer):

set ::proc [lambda {name args} {
  # [uplevel] lets us capture static variables in the caller's context.
  # I want to tag the ref with the proc name, so I'm not using [lambda].
  tailcall set $name [uplevel 1 [list builtin::proc [ref {} $name lambda.finalizer] {*}$args]]
}]

# test
set x 1
proc x {} {return 2}
list [x] $x ;# => 2 <reference.<x______>.00000000000000000002>

Notice that the new proc creates local (not global) commands. Consider the example of a command that generates accumulators:

proc Accumulator {{value 0}} {
   proc counter {{i 1}} value {incr value $i}
   return $counter
}
set c0 [Accumulator]
c0 3     ;# => 3
set c1 [Accumulator 10]
c1 4     ;# => 14
c0 5     ;# => 8
counter  ;# => error: can't read "counter": no such variable

To create global commands anywhere, you'll need to prefix the command name with ::.

Having commands created locally will break a lot of scripts (e.g. Jim's oo package will definitely break).

Now, we can add a bit of power by modifying unknown such that, when the command name is a variable but not a command, we should treat it like a dict and use the second arg as the key to that dict:

# [unknown] version 2:
builtin::proc unknown {cmdname args} {
  set depth [info level]
  
  # Look through the call chain for the command reference.
  for {set level 1} {$level <= $depth} {incr level} {
    upvar $level $cmdname cmdref
    if {[exists -var cmdref]} break
  }

  if {$level > $depth} {error "can't read \"$cmdname\": no such variable"}
  
  # ---code change---
  #if {![exists -command $cmdref]} {error "\"$cmdname\" is not callable"}
  # If cmdref is not callable, maybe it's a dict, search deep into the dict
  if {![exists -command $cmdref]} {
    set head $cmdref
    while {![catch {dict size $head}] && [llength $args]} {
      set args [lassign $args key]
      set head [dict get $head $key]
      if {[exists -command $head]} {tailcall [set head] {*}$args}
      lappend cmdname $key ;# update the command name for debugging purposes
    }   
    error "\"$cmdname\" is not callable"
  }
  # ---end code change---

  tailcall [set cmdref] {*}$args 
}

Now we can create dispatchers using dicts:

set math [dict create \
  square [lambda {x} {expr $x*$x}] \
  cube   [lambda {x} {expr $x**3}] \
  times  [dict create \
    two   [lambda {x} {expr 2*$x}] \
    three [lambda {x} {expr 3*$x}] \
  ] \
  pi $(acos(-1))
]

# test
math cube 7 ;# =>343
math times three 7 ;# => 21
math pi ;# => error: "math pi" is not callable

I believe this takes us pretty close to having namespaces and ultimately objects. However, syntactic sugar (such as :: in variable names) needs to be redefined internally to make it usable.