Why I like Tcl
In this blurb, I'll try to convince you that Tcl isn't just an old clunky language for contrarian weirdos and that it is in fact (well, has become) a hidden gem for the power-hungry hacker who wants a simple (but not barren!) glue language, like a mix of sh and Scheme.
Pros §
- Extremely consistent and elegant syntax described in 12 rules fitting in a short man page and no reserved keyword, nearing a Lisp/Forth level of ascetic purity.
- Homoiconic through strings (like every language with
eval
) but most importantly, through "list-like" strings, allowing for transparent code serialization and manipulation. - Official man pages! No web-only and spec-like doc such as cppreference nor example-less "minimalist" stuff like pydoc.
- One of the simplest if not the simplest interaction with C, letting you write plugins very easily (with critcl and swig to help).
- Not slow, not fast, in the same ballpark as cpython or Perl. Bytecode compiled with a stack VM and non-atomic (threads don't normally share memory) refcounting as sole GC (cycles are impossible by design); also "stackless" since 8.6.
- Language-wide copy-on-write semantics for all values (a bit like Qt's implicit sharing) makes for very clean code where you don't have to worry about pass-by-value/reference, but brings its own difficulties when trying to optimize.
- Fun type system that is seemingly "everything is a string" (like sh) but in reality "everything is an auto-mutating and cached tagged union + string", to keep the same flexibility but with decent performance.
- Powerful introspection through
info
to get the name/body/arglist of a procedure, all registered procedures, test variable existence, inspect stack frames, etc... Together withtrace
, you can even write an internal debugger in few lines. - Modifying the procedure arguments is done via
upvar
: in Tcl, a variable reference is just a name (string) attached to another stack frame number, quite elegant considering the language's core concepts. - If you use at least the builtin extensions (thread, http, tdbc, tcltest, msgcat) together with the de facto standard Tcllib, TclX and Tklib packages, you're almost set for life. I also recommend the very convenient tclreadline, tDOM and TclCurl.
- Channels are one of the cleanest I/O implementation I've ever used with some cool features:
- Transformations allowing filters like zlib or TLS to be put on a channel (
transchan
). - Reflected aka virtual channels (
refchan
), to make your own channel types. Basically like glibc/BSD's fopencookie/funopen or CL gray streams. - Centralize a lot of ioctl/fcntl mess and even more in
chan configure
and others (pending
,blocked
,eof
, etc...). - Integration with the event loop via
chan event
for a nice callback oriented approach to sockets and pipes. - Other third-party channel types include pty (expect), concat, random, memory or fifo (tcllib).
- Builtin event loop for seamless concurrency and event-based I/O. Much simpler than Python's very "AbstractBeanFactory" asyncio, in my eyes.
- Coroutines that integrate nicely with the aforementioned event loop.
- Versatile and terse (ba)sh-like subprocess creation through
exec
. Especially liking the popen-style channel redirections>@ chan
and<@ chan
. - An elegant thread model consisting of an interpreter per thread and no raw access to other threads' memory. Comes with a thread pool, and both simple and performant synchronization/communication facilities.
- Finally a sane, light and portable (even more with Tile) GUI toolkit: Tk. Not my area of expertise, so I won't go into detail here.
- One of the fastest Unicode aware regex implementations, written by Henry Spencer himself. Has its own greater-than-POSIX-ERE syntax called ARE not as complete as PCRE (lacking lookbehind constraints, most importantly) but still great for an hybrid NFA/DFA engine.
uplevel
(eval in a different stack frame) andtailcall
(replace the current procedure with another) let you augment the language by implementing control structures and keywords yourself. Inferior to CL's synergy between unhygienic macros, "naked AST" style homoiconicity, gensym and quasi-quoting, but still quite powerful.- Safe interpreters as a very granular jail for untrusted Tcl script evaluation (e.g. config files).
- Recent versions (>= 8.5) really embraced FP with:
- Real lambdas (but not closures, these have to be emulated) through
apply
(8.5). - Purer hash maps (
dict
) than ugly sticking-out-like-a-sore-thumb arrays (8.5). - Opt-in Lisp style prefix arithmetic -
* 3 [+ 1 2]
instead ofexpr {3 * (1 + 2)}
- including the same sane behaviour for more than two (reduce) or zero (neutral element) arguments (8.5). - Builtin map/filter with
lmap
(8.6). - No idiotic statement/expression divide, everything has a value and evaluated blocks have the
value of their last expression so no need for a ternary operator other than
if
or mandatory trailingreturn
. - Multiple more-or-less powerful OO systems (now based on the builtin TclOO): [incr Tcl] for C++ style OO, XoTcl for a take on CLOS or Snit for something Tk oriented.
- Made with embedding in mind, only beaten by Lua and some specialized Schemes in this regard.
See also
Here are some of the "hype" posts that sparked my own curiosity, years ago:
- http://antirez.com/articoli/tclmisunderstood.html
- https://colin-macleod.blogspot.com/2020/10/why-im-tcl-ish.html
- https://yosefk.com/blog/i-cant-believe-im-praising-tcl.html
Cons §
- Let's be honest, even with the release of 9.0, Tcl is almost dead as far as general interest
and manpower goes. Very few dogged people are saving it from going the way of the dodo like its
spiritual brother Rebol (inb4 Red). Main consequences are:
- No LSP/SLIME equivalent, which is as painful as it sounds.
- No package manager that can be considered alive.
- Few libraries to interact with the modern computing world or fill some of the following holes.
- The warts of the weak type system:
"foo bar"
is simultaneously a two-word string, at list of length 2 and a dict with one key/value pair. No way to differentiate between them when needed (want to serialize to JSON?). - Missing metaprogramming facilities like code walking and quasi-quoting.
- No pattern matching/unification.
foreach
, while sporting builtin parallel list iteration (no need to zip them), is noiterate
/loop
. I know this looks like the typical spoiled Lisp weenie tantrum, but powerful imperative iteration is a very important and fundamental feature, in my view.- No builtin keyword parameters/options in
proc
even though the latters are used in the stdlib itself. - Truly a scripting language (as its creator wanted) and all the associated limitations (lack of performance, no way to define user types, etc...).
- No official support for UDP and UNIX domain sockets (and too much reliance on TclX to fill POSIX-shaped holes in general).
- No GC for class instances (cf TIP 550).
- No FFI in core; CFFI is getting pretty good, mind you.
- Subtly broken
exec
(cf TIP 424 and 259).
Conclusion §
So, while Common Lisp is my true love and I nowadays disagree with Ousterhout's idea of hard separation between scripting and programming languages (I mean, look at Guile!), I still think that modern Tcl is a very lean, well thought-out and practical tool that can also bring joy to the metaprogrammers and aesthetes where something like Python or Perl can easily induce that "death by a thousand cuts" frustration due to numerous and disappointing design faults.