DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

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!"
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This prints:

HELLO
world
2 + 4 = 6
$x + $y = $z
hello
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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] }
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Here's what it looks like:

Hello World button screenshot

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
Enter fullscreen mode Exit fullscreen mode

Here's what it looks like:

Counter screenshot

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.

Top comments (5)

Collapse
 
cben profile image
Beni Cherniavsky-Paskin • Edited

One string TCL perfected, and hasn't been copied enough, is string quoting.
In shell, or almost any language, if you want to put quotes inside quotes you start getting into escaping hell:

watch "ls -l \"filen1 with space\" file2 file3-with-\\"quote"
Enter fullscreen mode Exit fullscreen mode

(well you can use ' vs " but that only helps 1 level deep. by the time you need \\" you know you're cursed.)

TCL, by (1) using distinct paired delimiters {...} for literal quoting (2) skipping nested delimiters as long as they balance, needs almost no escaping.
In a hypothetical TCL-like-shell, you'd simply write something like:

watch {ls -l {filen1 with space} file2 file3-with-\}brace}
Enter fullscreen mode Exit fullscreen mode

Well you do need { and } but only when unbalanced, which is rare — and crucially doesn't pile up when nesting!

"watch" BTW is a unix command that takes a shell-command-to-execute, so is essentially a custom control structure for the shell — but implemented entirely outside the shell, whoa! So are "xargs" (yikes), gnu parallel (better) and some others...
They're a worthy comparison to TCL's control structures that are just regular commands taking strings ;-)
Alas, shells make these commands 2nd-class citizens, bad quoting syntax being one missing part. Other problems are non-unixish state inside the shell, like aliases being invisible to external tools like "watch" that exec(), and lack of closures over variables, cwd :-( One day I'll prototype a shell with TCLish quoting and all state in filesystem...

Collapse
 
cben profile image
Beni Cherniavsky-Paskin • Edited
  • There was prior art, e.g. m4 had paired `...' delimiters before. And maybe TRAC had both aspects(?). However TCL really put these together neatly—then used them for maximum effect as data notation / structures / code!

  • I suspect bash adding $(...) instead of `...`, a major win when nesting, did come from TCL showing the way?

  • Hmm, Perl & Ruby do support paired quotes e.g. %q{...}. en.wikibooks.org/wiki/Ruby_Program...
    I should find excuses to make some DSL with them 🤔

Collapse
 
taw profile image
Tomasz Wegrzanowski • Edited

It's fairly rare that you need more than two levels of nesting, so just single + double quotes (or Python style triple-quotes) is good enough for almost all cases.

Perl (q()), Ruby (%q()), Raku (Q<>) etc. all have balancing quotes, but they're rarely used, so I'm not surprised most languages didn't bother with such features.

Some other quotes like %W are a lot more interesting.

Collapse
 
kamaranis profile image
Anton Barrera • Edited

I discovered in a BookOff in Japan (they sell used things) the book for about 500yens, Reading the synopsis it seemed interesting.

dev-to-uploads.s3.amazonaws.com/up...

Thanks to your post, I've saved some time. Money is not much, but time is important. Now I'll use the book to bulk up my bookshelf :-)

Collapse
 
epsi profile image
E.R. Nurwijayadi

Cooool