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-argumentfor
. -
incr i
incrementsi
, which can also be achieved byset 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:
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 variablename
-
pack .name
- put widget inname
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:
Let's walk over it all:
- we keep the counter in a global variable
counter
- we have procedures
plus_one
andminus_one
that increment and decrement the counter, as variables are local by default we need to explicitly tell it withglobal counter
that they are meant to modify the global variable - evenincr
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.
Top comments (5)
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:
(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:
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...
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 🤔
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.
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 :-)
Cooool