Tcl/Tk
Un lenguaje que aparece recursivamente en mi línea de trabajo y al que
normalmente no he hecho mucho caso es Tcl
y su toolkit Tk
. Es
posible que a la mayoría os suene el Tk
, por la librería de Python
o cualquier otro lenguaje de script que lo utilice para añadir
widgets de una manera sencilla. Es posible que incluso lo tengas
instalado en tu ordenador sin saberlo, especialmente si utilizas
GNU/Linux como sistema operativo. Lo usa sqlite
, lo usa fossil
, lo
que no es de extrañar porque ambas herramientas vienen del mismo
programador, pero lo utilizan otras herramientas también. La
explicación para hacerle un poco de casito de nuevo a este lenguaje
es que últimamente estoy dándole más a fossil
como herramienta de
control de versiones y me he visto utilizando un subconjunto de Tcl
que trae embebido... y ¡oye, no está tan mal y lo dejaba siempre
olvidado! Vaya una pequeña introducción al lenguaje y de sus
principales características, sin ánimo de ser exhaustivo.
Mi primer contacto con este lenguaje, curiosamente, fue en windows.
En un trabajo donde estuve, trabajábamos con varias «lectoras de
marcas» que se conectaban a través del COM1
. El software que
manejaba (absolutamente propietario) buscaba un conector de seguridad
por hardware que se enchufaba en el puerto paralelo de la
impresora. El caso es que me fui de vacaciones y cuando volví, me
habían cambiado el ordenador, habíamos pasado de Windows 98 a
Windows XP y me hicieron el cambio a traición. Me habían dejado
encima de la mesa un disco Zip con la copia de seguridad y todo
conectado; habían instalado todos los programas declarados, incluido
el de la lectora de marcas. Sin embargo, no comprobaron que
funcionara: en el ordenador viejo se habían llevado la garrapata de
seguridad, enchufada. Cuando volví ya se habían desecho de los
ordenadores viejos y fue imposible recuperar el chismático de
seguridad anticopia. Recuerdo que se pidió uno a la casa, pero
mientras tanto, tuvimos que hacer una lectura de exámenes y tuvimos
que escribir y leer del COM1 un poco «a pelo». Lo intenté hacer como
pude con C/C++
, pero los puertos COM
estaban cerrados y era un
cristo abrirlos. Tuvo que venir a ayudarme uno de los informáticos de
la casa. Él instaló una versión de Tcl/Tk y haciendo unas pruebas,
entre los dos conseguimos hacer un proceso simple de lectura/escritura
pudiendo corregir los exámenes que necesitábamos. Los scripts los
guardé y los utilicé más de una vez... el software oficial
caducaba y te sugería que compraras otra máquina (caras de la
muerte). Incluso con esos scripts pude hacer funcionar máquinas
viejas, de tipo manual, de las que tenías que ir metiendo las hojas de
una en una, que estaban guardadas en un armario pero que en alguna
ocasión nos salvó la situación.
No voy a ser exhaustivo, está la web del lenguaje si alguien quiere profundizar más y también puede encontrar libros y tutoriales dedicados a este ignorado lenguaje. Y me refiero a ignorado no porque no lo conozcamos sino a que tiendo a pasar de largo sin hacerle demasiado caso. Por tanto, como abogado de las causas perdidas, para intentar que no se sienta tan excluido, voy con una introducción al mismo.
Introducción a Tcl/Tk
El Tcl/Tk
, de Tool Command Language, es uno de esos lenguajes
llamados de script. Pensado para automatizar procesos, no para
programar sistemas. No he comprobado si es rápido o lento, tampoco me
importa demasiado, he hecho algún script para sqlite3
en una
(copia de) base de datos que tengo para mi trabajo y me ha parecido
bastante útil. Principalmente, porque es un lenguaje con unas bases
muy simples y, por tanto, le he cogido enseguida el
gustillo/tranquillo. Por lo que he visto, hay complejidades a las que
no he llegado, porque (aún) no las he necesitado, pero tiene librerías
también para casi cualquier asunto. Puedes abrir canales que pueden
ser ficheros, streams, sockets, tratándolos todos de la misma
manera.
Pero voy a dejarme ya de introducción: vamos con las bases del lenguaje.
Todo son comandos
Para Tcl/Tk
los scripts son una ristra de comandos uno tras otro.
Cada comando comienza con una instrucción o comando, a la que le sigue
una lista de parámetros y termina con un salto de línea o con un punto
y coma. Lo que hay tras el ;
antes del salto de línea es ignorado
por el intérprete. Esto es invariable y, por ejemplo, no hay operador de
asignación, existe el comando set
:
set etiqueta "Hola Mundo!" puts $etiqueta
Esta característica hace que, viniendo de lisp
, casi sienta la
tentación de meter cada comando entre paréntesis.
No hay tipos, todo son cadenas
No necesitas convertir unos tipos en otros para operar con ellos,
porque todos son cadenas. Además las cadenas internamente se guardan en
UTF-8 y por tanto no hay problemas con la conversión de códigos.
Alguno se estará preguntando, que si todo es una cadena, por qué en el
ejemplo anterior se escribió la cadena Hola Mundo!
rodeada de
comillas... veamos el siguiente ejemplo en la shell:
% puts Hola Hola % puts Mundo! Mundo! % puts Hola Mundo! can not find channel named "Hola" %
Al haber espacios en blanco, Tcl
piensa que hay una lista de
parámetros para puts
. En ese caso, puts
espera que el primer
parámetro sea lo que llama channel, que puede ser un fichero, un
stream, un socket, etc. Cuando sólo tiene un parámetro utiliza la
salida por defecto. Para agrupar esas cadenas se utilizan las
comillas y así se convierten en un solo argumento. También se pueden
utilizar las llaves, con una sutil diferencia. Veamos un ejemplo:
% puts "\tHola Mundo!\n" Hola Mundo! % puts {\tHola Mundo!\n} \tHola Mundo!\n
Como se puede apreciar, cuando los caracteres aglutinadores son las comillas, interpreta los caracteres especiales de la cadena, mientras que cuando utilizamos las llaves no interpreta nada. Y cuando digo que todo es una cadena, es que todo es una cadena:
% set a pu pu % set b ts ts % $a$b "Hola Mundo!" Hola Mundo!
Como se puede apreciar se ha formado el comando puts
dividiendo la
cadena en dos y guardando cada mitad en una variable diferente. Al
poner ambas variables juntas (concatenadas) se interpretan
sustituyéndolas por su contenido y conformando la sentencia completa
puts "Hola Mundo!"
.
A estas alturas, algunos estarán pensando: y si todo son cadenas
¿cómo hago cálculos?. La respuesta corta es con el comando expr
.
% set a 5 5 % set b 10 10 % puts [expr $a+$b] 15
¡Eh! Un momento, habíamos quedado que se agrupaban las cadenas con
las comillas o con las llaves... ¿qué son esos corchetes? Cuando
queremos sustituir un argumento con lo que devuelva otro comando
utilizamos los corchetes. En el caso anterior queremos que muestre el
resultado del comando expr $a+$b
.
% expr $a + $b 15 % [expr $a + $b] invalid command name "15"
Se puede apreciar, que cuando utilizamos los corchetes, el sistema al
recibir la cadena "15"
, interpreta que debe ser un comando e intenta
ejecutarlo.
Atención a las operaciones, porque hay que distinguir la aritmética entera de la flotante. Por ejemplo:
% expr 1 / 10 0 % expr 1.0 / 10 0.1 % expr 1. / 10 0.1
Listas
Uno de los objetos... no, mejor otro nombre, no sea que nos equivoquemos con la POO... Uno de los tipos... no, tampoco, habíamos quedado que no hay tipos en este lenguaje... Uno de los... pichorros, que más utilizo para agrupar la información en los programas son las listas.
% set l [list a b foo "hello world"] a b foo {hello world} % puts [llength $l] 4
Sencillo de entender: una lista se crea con el comando list
. También
podemos apreciar cómo internamente la cadena hello word
ha pasado de
representarse con comillas a su forma estática con llaves. Por
ejemplo, si hubiéramos escrito el siguiente código:
% set a maldito maldito % set l [list a b foo "hola $a mundo!"] a b foo {hola maldito mundo!}
Vemos cómo se almacena la forma estática después de interpretar el contenido de la cadena.
Tcl
además viene con una gran cantidad de procedimientos para
trabajar con listas, cortarlas, anexarlas, ver el largo, sustituir
elementos, etc. Pero eso ya lo consultáis en la documentación.
Procedimientos
Lo de un comando detrás de otro está bien, es muy sencillo de
entender pero ¿cómo hago mis propios comandos? Para esto existe un
comando proc
que recibe tres parámetros: 1) el nombre del
procedimiento, 2) la lista de parámetros que recibe, 3) el bloque de
código que debe ejecutar. Por ejemplo, imaginad que queremos tener un
procedimiento de suma equivalente al de lisp
, sin necesidad de
escribir expr
cada vez que quiera hacer una simple suma. El código
sería así:
proc + {a b} { expr $a+$b }
En este caso, proc
recibe el nombre de procedimiento +
, con la
lista de parámetros {a b}
y un bloque de código que hace expr
$a+$b
. Vamos a hacerlo funcionar:
% proc + {a b} { expr $a+$b } % puts [+ 5 12] 17
Voy a complicarlo un poco: quiero hacer un procedimiento para tener todos los operadores aritméticos en forma de prefijo y, de paso, vemos por encima un poco sobre bucles:
set operadores [list + - * /] foreach o $operadores { proc $o {a b} [list expr "\$a $o \$b"] }
Se puede apreciar, que en el caso de los parámetros, el carácter $
debe ir escapado para que sea evaluado al devolver la lista de
comandos y no al evaluar la expresión, mientras que el operador se
sustituye desde el inicio. Si no lo hacemos así, el intérprete buscará
y no encontrará las variables a
y b
, fuera de la función.
El funcionamiento lo podemos ver en el siguiente ejemplo:
% set operadores [list + - * /] + - * / % foreach o $operadores { proc $o {a b} [list expr "\$a $o \$b"] } % puts [/ 10. 3] 3.3333333333333335
Quizá hubiera sido mejor definir esos procedimientos para que
aceptaran una lista de números, en lugar de tan sólo dos parámetros,
como en lisp
o scheme
. Lo haré en un momento, pero dejadme que
primero consideremos la lista de argumentos de un procedimiento. Si es
variable, es decir, si queremos que pueda haber llamadas con distinto
número de parámetros podemos hacer dos cosas: definir algunos valores
por defecto o analizar cada argumento para actuar en consecuencia.
En cuanto a los parámetros por defecto, veamos un sencillo ejemplo fabricándonos nuestra función de incremento:
proc inc {valor {incremento 1}} { expr $valor+$incremento }
La lista de parámetros se ha convertido en una lista de listas y el
parámetro incremento
devolverá 1
en el caso de que no esté
definido. Lo vemos en funcionamiento:
% proc inc {valor {incremento 1}} { expr $valor+$incremento } % inc 13 2 15 % inc 13 14 % set a 5 5 % inc $a 6 % puts $a 5
En el ejemplo podemos apreciar que en los procedimientos, la
instrucción return
es opcional. Como en muchos otros lenguajes,
Tcl
devuelve el valor de la última instrucción evaluada si no existe
un return
específico. También podemos ver, que nuestro código nos
devuelve un valor, pero no modifica los parámetros recibidos como hace
la función original incr
. Se pueden hacer llamadas por referencia
con upvar
para poder modificarlos, pero creo que sería liar mucho la
perdiz para una introducción. Que sepáis que se puede y si alguien lo
necesita y/o tiene curiosidad que lo mire en la documentación, de
todas formas pondré un ejemplo más adelante.
La otra forma de tener argumentos opcionales es analizar la lista de
parámetros y hacer algo con ella. Por ejemplo, para crear un comando
suma
que acepte una lista de números que sumar, podríamos hacerlo de
la siguiente manera:
proc suma args { set s 0 foreach i $args { set s [expr $s + $i] } return $s }
Veamos el código en acción:
% proc suma args { set s 0 foreach i $args { set s [expr $s + $i] } return $s } % suma 1 2 3 4 5 15 % suma 1.1 2.2 3.3 4.4 5.5 16.5 % suma 0 %
Por último, con respecto a los procedimientos, nada nos impide hacer nuestros propios bloques de control utilizando los ya existentes o incluso nuestro propio sistema de macros. Aunque no lo analizaré en profundidad, dejo el ejemplo para que lo desentrañéis:
proc do {variable primero ultimo cuerpo} { upvar $variable v for {set v $primero} {$v <= $ultimo} {incr v} { uplevel $cuerpo } }
Quizá verlo en acción nos puede servir mejor para entender cómo funciona.
% proc do {variable primero ultimo cuerpo} { upvar $variable v for {set v $primero} {$v <= $ultimo} {incr v} { uplevel $cuerpo } } % set a {} % do i 1 5 { lappend a [expr $i*$i] } % puts $a 1 4 9 16 25 % puts "Contador: $i -- lista: $a" Contador: 6 -- lista: 1 4 9 16 25 %
No hace falta explicar el comando for
, es equivalente al de otros
lenguajes como C/C++
; lo único es observar cómo es un procedimiento
que acepta cuatro parámetros. Después de definirlo podemos utilizar
nuestro nuevo procedimiento de control do
, creamos una lista vacía
a
y la llenamos con los cuadrados de los primeros cinco enteros.
Además vemos que los comandos upvar
y uplevel
sirven para trabajar
con variables y bloques de códigos por referencia, pero los detalles
los tendréis que buscar en la documentación.
Evaluación del código
Como todo es una cadena, al final todo es evaluar cadenas como código. Por ejemplo:
% set cadena "puts hola" puts hola % eval $cadena hola
El comando eval
hace ese trabajo, pero no es el único. También
existe, como hemos visto el comando uplevel
, para trabajar por
referencia con el código embebido dentro de otra función. Por
ejemplo:
proc repetir {n cuerpo} { set res "" while {$n} { incr n -1 set res [uplevel $cuerpo] puts "$n -- $res" } return $res }
El comando incr
incrementa una variable de tipo entero. Lo hace en
una unidad si sólo recibe el parámetro de la variable, pero podemos
indicar también un segundo parámetro con el valor del incremento. En
el ejemplo, ese valor de incremento es -1
.
En la condición del comando while
vemos que, como ocurre en C/C++
,
el valor de 0
es equivalente al valor lógico false
.
El ejemplo en funcionamiento:
% proc repetir {n cuerpo} { set res "" while {$n} { incr n -1 set res [uplevel $cuerpo] puts "$n -- $res" } return $res } % set x 5 5 % repetir 5 { incr x } 4 -- 6 3 -- 7 2 -- 8 1 -- 9 0 -- 10 10 % repetir 5 { puts "Hola cinco veces" } Hola cinco veces 4 -- Hola cinco veces 3 -- Hola cinco veces 2 -- Hola cinco veces 1 -- Hola cinco veces 0 -- %
En el ejemplo de incrementar la variable x
vemos cómo se va
incrementando en cada paso, porque cuando hace el incremento también
devuelve el valor. Sin embargo, el comando puts
no devuelve
nada, sólo imprime la cadena, por lo que res
se mantiene vacío
durante toda la ejecución.
Tk
El toolkit gráfico es sencillo y potente. Su diseño es quizá lo que podríamos definir como viejuno o vintage, sin embargo, más allá de la estética es sencillo de utilizar. Por no alargar innecesariamente el artículo, vamos a hacer de manera interactiva un «Hola mundo!» gráfico con él y si os interesa ya lo ampliaréis.
Lo primero será lanzar el intérprete gráfico es wish
, igual que el
de Tcl
es tclsh
. Vemos que comparten el mismo prompt:
> wish %
Al pulsar enter tras el wish
nos aparecerá una ventana muy
sencilla:
Añadimos la etiqueta de «Hola mundo!»
% label .l -text "Hola, Mundo!" .l % pack .l %
El comando label
tiene un nombre de widget: .l
y un texto. El
comando pack
lo dibuja en la ventana.
Y también le vamos a añadir un botón para cerrar la ventana:
% button .b -text "Salir" -command exit .b % pack .b %
Vemos que el comando button
es muy similar a label
pero se ha
añadido un parámetro -command exit
. El resultado gráfico es:
Al pulsar el botón, la ventana se cerrará.
Todo lo podemos empaquetar en un archivo con el siguiente aspecto:
#!/usr/bin/wish label .l -text "Hola, Mundo!" pack .l button .b -text "Salir" -command exit pack .b
Conclusiones
A mí me parece que Tcl
, junto con su extensión gráfica Tk
, es un
lenguaje bastante expresivo y con un funcionamiento fácil de
comprender, a la par que potente, especialmente la capacidad de tener
widgets, evaluando cadenas de texto que son interpretadas como
código. Tan sencillo que afirman que programar un intérprete para él
desde cero no es una tarea demasiado ardua y que con poco código se
puede hacer perfectamente.
Además, se considera que es multiparadigma, puesto que dispone de unas librerías que lo convierten en un lenguaje orientado objetos, o en un lenguaje funcional.
Si lo tienes instalado en tu sistema, y no lo has utilizado nunca, es el momento de hacerlo. Usa los ejemplos que hay en este artículo, juega con ellos y (re)descubre este magnífico lenguaje.
Comentarios