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 800x600button .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.