100 Languages Speedrun: Episode 06: Tcl/Tk

100 Languages Speedrun: Episode 06: Tcl/Tk

Time for some software archeology! Tcl/Tk is a language you rarely see anymore, but it was somewhat popular back in the days. It was very embedding-friendly - in fact it started as a language for scripting existing applications, not for creating standalone programs. It also came with builtin graphics toolkit (the "Tk" part), in times when it was extremely uncommon.

Tcl/Tk is a massive pain to install on new operating systems. OSX comes bundled with an obsolete version that prints a warning whenever you run a hello world. brew install tcl-tk install a proper version, but it won't link it in $PATH saying Warning: Refusing to link macOS provided/shadowed software: tcl-tk. So to use brew version we'll have to use full path to Tcl/Tk executables (or mess with $PATH).

Unix shell scripting

It's easier to make sense of Tcl/Tk if you're familiar with Unix shell scripting. If we put languages on unix-shell-likeness scale, it would go something like this:

  • traditional Unix shell - barely usable for writing code
  • modern Unix shell - some nasty duct taped control structures, not suitable for real programming, but some people force it anyway
  • Tcl/Tk - it qualifies as a real programming language, but it looks like shell, and has many shell-like semantics
  • Perl - syntactically it still looks like Unix shell, but it behaves mostly like a real programming language
  • PHP - still uses $ sigils, but that's about it
  • Ruby - occasional shell-like features if you look for them (like -nle, $.)
  • Python - pretty much nothing, unless you count # for comments

The way Unix shell scripting works is that every line is a command - the first word of the line is a command name, and the rest are string arguments. Variables all contain strings only - and there's no real distinction between number 42 and string "42". If line contains any $x, it is replaced by string contents of variable x before being ran. Tcl/Tk is a bit more complicated, but that's a good starting point.

Hello world

#!/usr/local/opt/tcl-tk/bin/tclsh

puts "Hello, world!"

Did I accidentally put Ruby code? I assure you, I did not, the syntax is going to get quite weird very soon. The #! line pointing at full path is due to OSX brew issues, and if you run it on a different system you'll need a different one. # is also used for comments.

Variables

#!/usr/local/opt/tcl-tk/bin/tclsh

set who "world"
puts "Hello, $who!"

Variables are all strings. Inside double quotes strings are interpolated.

One thing to note is that $x refers to contents of variable x.

This is a distinction which most languages don't make. Even in Perl or PHP which use sigils, $x refers to both the variable (when on left of = sign), or its contents (when on right of = sign). Shell and Tcl make distinction between these two cases - and they don't have x=y style variable assignment.

Types

#!/usr/local/opt/tcl-tk/bin/tclsh

set x 2
set y "4"
set z [expr $x+$y]
puts [string toupper Hello]
puts [string tolower "World"]
puts "$x + $y = $z"
puts {$x + $y = $z}
puts stdout hello

This prints:

HELLO
world
2 + 4 = 6
$x + $y = $z
hello

Variables are all strings, so 2 and "2" are the same thing. You generally don't need to quote them, so hello and "hello" are in most contexts the same thing.

You can use [function arguments] to call a function. [string action argument] is a weird function that does many actions based on its argument, applied to the second. As you can see, it doesn't matter if you pass Hello or "Hello".

To do math you need to call [expr ...] function. As all variables are strings, it wouldn't really make sense for $x+$y to do anything on its own.

{...} is also a string, but unlike "..." it doesn't interpolate anything. Tcl has many things that look like control structures, but in a way they just pass such strings containing code around.

And for the last one, puts hello on its own should work, but puts has optional argument where to print it and when you type puts hello Tcl is confused if you meant to puts hello string to standard output, or puts whatever's default into hello stream. Maybe let's not think about this too much, I just wanted to mention that hello and "hello" are almost the same thing in most context, but not always so.

Fibonacci

In most languages we can get to Fibonacci and FizzBuzz right away, but for Tcl we had to take a few extra steps before that.

#!/usr/local/opt/tcl-tk/bin/tclsh

proc fib n {
  if { $n <= 2 } {
    return 1
  } else {
    return [expr [fib [expr $n-1]] + [fib [expr $n-2]]]
  }
}

for {set i 1} {$i <= 30} {incr i} {
  puts [fib $i]
}

Let's go through it step by step:

  • proc name arguments { ... } defines a function.
  • for {set i 0} {$i < 30} {incr i} { ... } loops over a range, with C style 4-argument for.
  • incr i increments i, which can also be achieved by set i [expr $i + 1].
  • if { condition } { ... } else { ... } is a conditional - conditionals are automatically evaluated without need for extra [expr ...]
  • return value returns from a function

OK, that looks fine. Except that's mostly lies. { } doesn't define a block, it's just a string we're passing. if, else, proc, return and not keywords - they're just commands.

So this awful code does exactly the same thing:

#!/usr/local/opt/tcl-tk/bin/tclsh

"proc" "fib" "n" {
  "if" { $n <= 2 } "return 1" "else" { "return" [expr ["fib" [expr $n-1]] + [fib ["expr" $n-2]]] }
}

"for" "set i 1" {$i <= 30} "incr i" { puts [fib $i] }

FizzBuzz

#!/usr/local/opt/tcl-tk/bin/tclsh

proc fizzbuzz n {
  if { $n % 15 == 0 } {
    return "FizzBuzz"
  } elseif { $n % 3 == 0 } {
    return "Fizz"
  } elseif { $n % 5 == 0 } {
    return "Buzz"
  } else {
    return $n
  }
}

for {set i 1} {$i <= 100} {incr i} {
  puts [fizzbuzz $i]
}

At least we didn't need to introduce any new syntax for FizzBuzz.

Tk Hello World

Here's the GUI hello world:

#!/usr/local/opt/tcl-tk/bin/wish

wm geometry . 800x600
button .hello -text "Hello, World!" -command { exit }
pack .hello

Here's what it looks like: hello_button.png

Notice the executable changed from tclsh to wish.

This works very differently from browsers. We don't define structure of the app in some markup, and have code to control it - we're just issuing commands to control the GUI directly:

  • wm geometry . 800x600 - set window size to 800x600
  • button .name -text "..." -command {...} - create button with given text, and with given onclick command, and save it to variable name
  • pack .name - put widget in name in the window (by default centered horizontally, on top)

Tk Counter

So let's implement the click counter:

#!/usr/local/opt/tcl-tk/bin/wish

set counter 0
proc plus_one args {
  global counter
  incr counter
}
proc minus_one args {
  global counter
  set counter [expr $counter-1]
}

wm geometry . 800x600
label .counter -textvariable counter -font "Helvetica -64"
button .plus -text "+1" -command plus_one -font "Helvetica -48"
button .minus -text "-1" -command minus_one -font "Helvetica -48"

place .counter -x 400 -y 200 -anchor s
place .minus -x 400 -y 300 -anchor e
place .plus -x 400 -y 300 -anchor w

Here's what it looks like:

counter.png

Let's walk over it all:

  • we keep the counter in a global variable counter
  • we have procedures plus_one and minus_one that increment and decrement the counter, as variables are local by default we need to explicitly tell it with global counter that they are meant to modify the global variable - even incr would create a new local variable otherwise
  • we create a label - -textvariable argument makes it update when specified global variable changes
  • we create a pair of buttons calling our functions - we could put the whole function inside with -command { ... } as well
  • styling for all of that is passed as just some extra arguments like -font, there's nothing like CSS
  • we place them at specific points of the window with place command - it takes -x -y arguments specifying where to place something, and -anchor to specify which side of the anchor point to put the widget on - there doesn't seem to be any centering

Should you use Tcl/Tk?

In 2021 not really. For regular programming there's literally hundreds of much better programming languages. For embedded uses, I think pretty much everyone moved on to JavaScript or Lua or Python or such, or basically anything else than Tcl/Tk.

As for quick GUIs for your shell scripts, Tk is a fairly bad toolkit, and I covered many better ones in my Electron Adventures series. But even if you really want to use Tk, somehow many modern languages like Ruby and Python still include some kind of Tk code in their standard library for historical reasons.

Tcl/Tk is really only of interest as a historical artifact, not as a language anyone might seriously use for new software.

I find it difficult to even say how much influence it had on other languages and GUI systems. Most Tcl features are also found in Unix shell scripts, and in Perl which released a few months before Tcl. So any similarities could be explained much better by Unix shell's or Perl's influence. Old style GUIs have been nearly obliterated by browser style GUIs, so I can't tell if Tk influenced those other GUI toolkits much. It seems to me that it basically expired without any real impact. Some languages pass away, but leave big legacy behind - like most of ES6+ JavaScript features come from CoffeeScript; and Perl had a huge direct or indirect impact on almost every post-Perl language. For Tcl/Tk, I'm not really seeing anything like that. It did its thing, then it just died quietly, and now it's nearly forgotten.

Code

All code examples for the series will be in this repository.

Code for the Tcl/Tk episode is available here.