Notxor tiene un blog

Defenestrando la vida

Tcl/Tk

2022-04-24

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:

Captura_wish.png

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.

Captura_etiqueta.png

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:

Captura_boton.png

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.

Categoría: tcl tk programación

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en Mastodon y en Diaspora con el nick de Notxor.

También se ha abierto un grupo en Telegram, para usuarios de esa red.

Disculpen las molestias.