World Playground Deceit.net

Why I like Tcl

Published on
Tags: tcl, programming

In this article, I'll try to convince you that Tcl isn't just an old clunky language for contrarian fools and that it is in fact one of the best scripting languages for the power-hungry hacker who wants a simple (but not too simple!) 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.
  • Official man pages! No web-only and spec-like doc like cppreference nor lackluster "minimalist" stuff like pydoc (compare pydoc print with man n puts, for example).
  • 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 GC (cycles are impossible by design and CoW semantics go well with it); 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/ref, but brings its own difficulties when trying to optimize.
  • Fun type system that is superficially "everything is a string" (like sh) but in reality "everything is a tagged union with on-the-fly conversion when needed and a guaranteed string representation". Allows for transparent serialization (puts $mydict stdout then set mydict [read stdin]), like CL's read/write.
  • Powerful introspection through info to get the name/body/arglist of a procedure, all registered procedures, test variable existence, inspect stack frames, etc... Together with trace, 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) and the very basic tcllib/tclX/tklib packages, you're almost set for life. Personally, I also recomment the very convenient tclreadline, tdom and tclcurl.
  • Channels is 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 (cf transchan).
    • Reflected aka virtual channels (cf 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). Very reminiscent of CL's various standard streams.
  • 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 interaction 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 got 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. Performance comparison with Perl: https://github.com/mariomka/regex-benchmark/pull/44.
  • uplevel (eval in a different stack frame) and tailcall (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 (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 (cf EXAMPLES) Lisp style prefix arithmetic * 3 [+ 1 2] instead of expr {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 trailing return.
  • 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:

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: no way to differentiate between atoms and single element lists or evenly sized lists and dicts.
  • Missing metaprogramming facilities like tree walking and quasi-quoting.
  • No pattern matching/unification.
  • foreach, while sporting builtin parallel list iteration (no need to zip them), is no iterate/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 his creator wanted) and all the associated limitations (lack of performance, "scalability" in the form of custom data structures, 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.

Your host's Tcl wiki page