INTRODUCCIÓN SNOBOL es una serie de lenguajes de programación creados en la década de 1960 en los laboratorios Bell de AT&T por D. J. Farber, R. E. Griswold y F. P. Polensky. Se trata de lenguajes de muy alto nivel orientados a la manipulación de cadenas de caracteres ("StriNg Oriented and symBOlic Language"). Después de la versión inicial, llamada simplemente SNOBOL, se desarrollaron otras añadiéndole al nombre un número: SNOBOL2, SNOBOL3 y SNOBOL4, que se puede considerar más o menos definitiva, aunque existen otras variantes como FASBOL, SPITBOL, Snocone, etc. SNOBOL4 destaca por sus capacidades de reconocimiento de patrones, pero algunas de sus características, como los mecanismos de control del flujo, se consideran arcaicas. Ralph Griswold, uno de los autores originales de SNOBOL, diseñó tiempo después un nuevo lenguaje, en parte inspirado en SNOBOL4, llamado Icon (que a su vez dio lugar a Unicon). SNOBOL4 tuvo cierto éxito y repercusión en su momento (por ejemplo, ayudó a fraguar el concepto de expresión regular), pero ya hace tiempo que no se usa profesionalmente. A pesar de ello existe un grupo de entusiastas que lo mantienen vivo, habiendo incluso versiones bastante recientes que se pueden instalar en Linux o Windows. La verdad es que a mí no se me ocurren más razones para estudiarlo que la mera curiosidad o el interés por la historia de los lenguajes de programación. ------------------------------------------------------------------------------ RECURSOS EN LÍNEA generales https://es.wikipedia.org/wiki/Snobol https://en.wikipedia.org/wiki/SNOBOL http://www.regressive.org/snobol4/ versiones - más antiguas http://www.101iq.com/snobol4.exe (Minnesota SNOBOL4 para MS-DOS) ftp://ftp.snobol4.com/snobol4p.zip ftp://ftp.snobol4.com/vanilla.zip - más recientes http://www.regressive.org/snobol4/csnobol4/ https://github.com/spitbol/x32 https://github.com/spitbol/x64 https://github.com/spitbol/windows-nt http://snobol5.org/ documentación http://www.snobol5.com/greenbook.pdf http://berstis.com/s4ref/snobol4.htm (Minnesota SNOBOL4 para MS-DOS) http://www.regressive.org/snobol4/docs/burks/manual/contents.htm (Vanilla) http://www.regressive.org/snobol4/docs/burks/tutorial/contents.htm (Vanilla) http://www.math.bas.bg/bantchev/place/snobol/vtrm.pdf (los dos anteriores) https://github.com/spitbol/spitbol-docs/blob/master/spitbol-manual-v3.7.pdf https://vulms.vu.edu.pk/Courses/CS508/Downloads/Modern%20Programming%20Languages%20-%2009-12%20-%20SNOBOL.pdf http://snobol5.org/snobol5.htm ------------------------------------------------------------------------------ CÓMO PROBARLO Una forma de probar SNOBOL4 es mediante SNOBOL4+, un producto antiguo de una empresa llamada Catspaw, originalmente comercial pero hoy en día distribuido gratuitamente. Se puede bajar mediante FTP de la dirección ftp://ftp.snobol4.com/snobol4p.zip. Esto se puede hacer mediante programas como FileZilla, pero también desde la línea de comandos. Por ejemplo, en Linux (creo que se puede hacer igual en Windows): $ ftp www.snobol4.com (...) Name (...): anonymous (...) Password: (Se puede escribir cualquier cosa o nada, pero supongo que lo educado es hacer caso de las instrucciones y poner el correo electrónico.) (...) ftp> get snobol4p.zip (...) ftp> bye $ También se puede bajar, en vez de SNOBOL4+, Vanilla SNOBOL4, una versión limitada pero que para iniciarse en el lenguaje vale igual. El archivo es vanilla.zip. SNOBOL4+ (y también Vanilla SNOBOL4) es para MS-DOS, así que tendremos que ejecutarlo con un emulador como DOSBox. Una vez tengamos instalado DOSBox y hayamos creado una carpeta para usarla como disco duro virtual, descomprimiremos snobol4p.zip dentro de una subcarpeta vacía de ese "disco duro". El archivo ZIP tiene 3 ficheros: SNOBOL4.EXE README.TXT CODE.SNO Ya podremos probar SNOBOL4+ ejecutando DOSBox. Por ejemplo, si la carpeta se llama SNOBOL4P: C:\>CD SNOBOL4P C:\SNOBOL4P>SNOBOL4 CON SNOBOL4+ (al igual que las demás versiones del lenguaje que yo he visto) no incluye un entorno de programación, sino simplemente un ejecutable que hay que llamar con el nombre del fichero con nuestro código como argumento. Pero si en vez de un nombre de fichero usamos CON, espera a que se escriba el programa en ese momento. Esto no es práctico salvo para un miniprograma como el típico "Hola, Raimundo" (El espacio antes de OUTPUT es importante.): (...) ? OUTPUT = "Hola, Raimundo" ?END No errors Hola, Raimundo C:\SNOBOL4P> Lo normal es crear el fichero con un editor, como el EDIT incluido en DOSBox (que es la versión para FreeDOS del de MS-DOS), guardarlo con un nombre adecuado (por ejemplo, HOLA.SNO), salir del editor y luego ejecutarlo: $ SNOBOL4 HOLA El resultado, por supuesto, es el mismo de antes. Como se ve, se puede omitir la extensión si es SNO. ------------------------------------------------------------------------------ PRIMERA LECCIÓN El miniprograma anterior ya nos da unas pistas acerca de algunas características del lenguaje. En la primera línea nos sorprende (a mí, al menos) el espacio antes de OUTPUT. En la segunda nos podría sorprender, cuando se escribe con la opción CON, la falta de espacio antes de END. Las dos cosas son importantes. También lo son los espacios antes y después del signo igual (*). * Según el "Green Boook" de SNOBOL4, fuente de documentación oficial sobre el lenguaje (THE SNOBOL4 PROGRAMMING LANGUAGE, second edition, pág. 1, apartado 1.1): "In an assignment statement, the equal sign must be separated from the variable on the left and the value on the right by at least one blank.". Sin embargo SNOBOL4+ no requiere el espacio a la derecha. Hay que tener en cuenta que nos encontramos con un lenguaje de los años 60, una época en que la fuente principal de entrada eran las tarjetas perforadas de 80 columnas, lo que hacía que hubiera una tendencia a pensar en términos de líneas de texto y no de un flujo continuo de caracteres. Un programa en SNOBOL4 es una sucesión de sentencias (statements), cada una de una línea (aunque si hace falta se pueden "pegar" líneas). Cada sentencia tiene tres secciones (no tienen que aparecer las tres) y se deben separar por espacios (uno o más espacios o tabuladores). La primera sección no puede tener espacios delante, de ahí la no separación de END (que pertenece a la primera sección) con el signo de interrogación (el prompt). En la primera línea hay una declaración de asignación (que pertenece a la segunda sección), de ahí los espacios iniciales. Podría ir un solo espacio, pero lo acostumbrado es poner más para que la diferenciación entre secciones sea más visible. Otra cosa llamativa es la forma de producir una salida, asignando lo que queremos mostrar a una variable especial, OUTPUT. Según el Green Book (véase la nota anterior; en adelante TGB) esta variable está asociada con la impresión en papel (con 132 caracteres por línea) y este manual ni siquiera menciona las pantallas. Pero en la época de SNOBOL4+ el método principal de salida ya era la pantalla (entonces CRT), así que OUTPUT está por defecto asociado con ella (también hay formas de imprimir en papel). SNOBOL4+ también incluye la variable especial SCREEN, específica para la pantalla (útil para casos en que se ha cambiado el comportamiento normal de OUTPUT). Tal vez también llame la atención el hecho de que OUTPUT y END estén escritos con letras mayúsculas. La tendencia actual en programación (y desde hace varias décadas) es usar más las minúsculas que las mayúsculas, pero en los años sesenta era al revés. Aunque los ordenadores en que se usó SNOBOL4 en su tiempo también manejaban letras minúsculas (por lo menos algunos de ellos), lo más corriente era usar mayúsculas para las variables y funciones predefinidas (seguramente porque sí había ordenadores con solo mayúsculas, así que se consideraba más seguro). De hecho, por defecto, SNOBOL4+ pasa los nombres de variable a mayúsculas (es lo que llaman "case folding"). Esto se puede cambiar invocando el intérprete con la opción /C: $ SNOBOL4 /C En este caso es obligatorio usar, por ejemplo, OUTPUT en mayúsculas y podemos tener variables A y a con valores distintos. Una última enseñanza (aunque esta no sea muy sorprendente) de nuestro miniprograma es que todos los programas en SNOBOL4 deben terminar con una sentencia END, consistente en esta palabra en la primera sección, es decir, al principio de la línea. ------------------------------------------------------------------------------ CODE.SNO SNOBOL4+ incluye un pequeño programa llamado CODE.SNO para probar sentencias de forma interactiva: C:\SNOBOL4P> SNOBOL4 CODE (...) Enter SNOBOL4 statements: ? OUTPUT = "Hola, Raimundo" Hola, Raimundo Success ?END C:\SNOBOL4P> La idea no es escribir un programa completo, sino probar sentencias individuales (principalmente la sección central), aunque los valores de las variables se conservan durante la sesión: ? SALUDO = "Hola, Tadeo" Success ? OUTPUT = SALUDO Hola, Tadeo Success ? Para terminar se puede introducir una sentencia END o teclear Ctrl-Z e Intro. Mientras no se diga otra cosa, los ejemplos que vengan a partir de ahora están pensados para ejecutarlos con CODE.SNO. ------------------------------------------------------------------------------ VARIABLES Y TIPOS DE DATOS Ya hemos echado un vistazo a las variables y a un tipo de datos: las cadenas de texto o strings. Ahora introduciremos los tipos numéricos. Las variables se identifican mediante un nombre que debe empezar por una letra seguida por cualquier cantidad de letras, dígitos, puntos o guiones bajos (*). * Más adelante veremos que esto es cierto para variables referenciadas directamente por su nombre, pero hay una forma indirecta de llamar a una variable que permite usar como identificador cualquier string no nulo. En esto es bastante estándar salvo por la posibilidad de usar puntos. Las letras son las del alfabeto inglés (26 letras). SNOBOL4 diferencia las mayúsculas de las minúsculas en los identificadores, pero tradicionalmente prefiere las mayúsculas. Como vimos antes, SNOBOL4+ pasa todos los nombres de variable a mayúsculas, a no ser que se le diga explícitamente que no lo haga con la opción /C o cambiando el valor de la variable especial &CASE (desde ahora, mientras no haga falta, omitiré la línea "Success" que muestra a menudo CODE.SNO: ? &CASE = 0 ? A = 5 ? a = 8 ? OUTPUT = A * a 40 ? &CASE = 1 ? OUTPUT = A * a 25 ? &CASE = 0 ? OUTPUT = a 8 &CASE debe valer 0 para que no se produzca el "case folding". Por defecto vale 1 (si se usa /C en la llamada del intérprete vale 0). Como se ve en el ejemplo, la variable a (como se declaró con &CASE igual a 0) conserva su valor, pero es inaccesible mientras &CASE vale 1: antes de evaluar A * a, el intérprete lo transforma en A * A. La forma de declarar una variable es simplemente dándole un valor y el mismo nombre de variable puede almacenar sucesivamente distintos valores del mismo o distinto tipo. Si se invoca un nombre de variable a la que no se le ha asignado ningún valor, el intérprete le asigna el valor "", de tipo string. Al menos en SNOBOL4+, los nombres de variable no pueden tener más caracteres que la longitud máxima de una línea (120 caracteres). Los strings son secuencias ordenadas de caracteres y pueden contener todos los que formen parte del conjunto usado en el entorno. En el SNOBOL4 original era el conjunto EBCDIC. En SNOBOL4+ es el llamado ASCII extendido. (*) * Ambos conjuntos codifican cada carácter con 8 bits y contienen por tanto 256 caracteres. Y ambos tienen muchas variantes, llamadas "páginas de código" ("codepages"). El código de página que uso yo en DOSBox incluye todos los caracteres necesarios para escribir en español (incluyendo la ñ, signos de exclamación y de interrogación invertidos y vocales con tilde), así como el signo de euro. [No sé por qué, no se puede escribir normalmente la O mayúscula con tilde: se puede hacer con Alt-224 {apretar alt, 224 en el teclado numérico, soltar alt}.] Se puede ver el conjunto de todos los caracteres mostrando el contenido de la variable especial &ALPHABET. Por supuesto, el "case folding" no afecta al contenido de los strings: independientemente del valor de &CASE, SNOBOL4+ no cambia las letras minúsculas de un string a mayúsculas. Los strings se especifican literalmente encerrando la secuencia de caracteres entre comillas simples o dobles. Se pueden incluir en el string comillas de un tipo usando como delimitadores las del otro tipo: ? OUTPUT = "Sir Tim O'Theo" Sir Tim O'Theo ? OUTPUT = '"Wonderful!", she said.' "Wonderful!", she said. El estándar no impone un tamaño máximo a los strings, pero según el manual de Vanilla (en adelante VTRM, de Vanilla Tutorial and Reference Manual), el límite habitual es de 5000 caracteres, aunque se puede cambiar. Un string también puede tener 0 caracteres y se puede escribir "" o ''. Se le llama string nulo. Otro tipo básico es el de número, que ya usamos antes en un ejemplo. En realidad hay dos tipos numéricos distintos: enteros (integers) y reales (reals). No sé si esto forma parte del estándar, pero al menos en SNOBOL4+ los enteros deben estar comprendidos entre -32768 y 32767. Vanilla, al ser un producto limitado, no incluye números reales, que sí forman parte del estándar (y de SNOBOL4+). Los reales se diferencian porque tienen punto decimal: ? OUTPUT = 8.0 8. La función estándar DATATYPE se puede usar para comprobar el tipo de dato de una variable o valor literal: ? OUTPUT = DATATYPE(6.5) REAL ? N = "Tadeo Jones" ? OUTPUT = DATATYPE(N) STRING ? OUTPUT = DATATYPE(7.0) REAL De acuerdo con TGB, SNOBOL4 incluye 10 tipos predefinidos (hasta ahora hemos visto tres) y el usuario puede definir tipos nuevos. ------------------------------------------------------------------------------ OPERACIONES Con los números se pueden hacer las operaciones aritméticas básicas: suma (+), resta (-), multiplicación (*), división (/) y exponenciación (** o !). El mismo signo, -, se usa para el operador binario resta y para el operador unario negación. El signo + también puede funcionar como unario, aunque es útil sobre todo con strings. IMPORTANTE: Los operadores binarios deben estar precedidos y seguidos de espacios, mientras que los operadores unarios deben ir pegados a su operando. Las expresiones numéricas se interpretan de la forma habitual y pueden contener paréntesis. El signo igual que se usa en las asignaciones también es un operador binario. El operando de la izquierda debe ser un nombre de variable, el de la derecha un valor literal u otra expresión. El valor devuelto es el mismo valor asignado a la variable: ? OUTPUT = A = 5 5 ? OUTPUT = A 5 OUTPUT = A = 5 es equivalente a OUTPUT = (A = 5). A = 5 le asigna a la variable A el valor 5 y devuelve el valor 5, que a su vez se le asigna como valor a la variable especial OUTPUT, que produce la salida. Este mismo método se puede usar para darle el mismo valor a varias variables (es lo que hemos hecho con OUTPUT y A): ? A = B = C = 10 ? OUTPUT = A + B + C 30 Un operador binario especial es el de concatenación, ya que no tiene signo asociado. Para concatenar dos strings simplemente se escriben juntos, con un espacio entre ellos: ? N = "Rodolfo" ? A = "Valeiras" ? OUTPUT = N " " A Rodolfo Valeiras Los operadores aritméticos solo funcionan con números y la concatenación solo funciona con strings (salvo una excepción relacionada con el string nulo que veremos pronto), pero SNOBOL4 (como JavaScript) incluye mecanismos de conversión automática de tipos. ? OUTPUT = "5" + "7" 12 Si se intenta sumar un número con un string o dos strings, el intérprete intenta convertir el o los strings en números. El string nulo se convierte a 0. (*) * Según VTRM no se admiten espacios antes ni después de los dígitos para que se realice la conversión, pero esto no es cierto en SNOBOL4+. El ejemplo OUTPUT = 14 + ' 54' no produce error. El + unario se puede usar para convertir un string "numérico" en un verdadero número. ? A = +"7" ? OUTPUT = DATATYPE(A) INTEGER ? OUTPUT = +'' 0 De forma análoga, si intentamos concatenar números el intérprete los convierte a strings: ? OUTPUT = 7 7 77 En ese caso, 77 no es un número, es un string. El string nulo tiene un comportamiento especial en la concatenación. Si se concatena un número con el string nulo, el resultado sigue siendo numérico. ? OUTPUT = DATATYPE(6 "") INTEGER ------------------------------------------------------------------------------ VALORES Y SEÑALES Hay que diferenciar los conceptos de valores y señales. Los valores son muy variados; las señales (que yo sepa) solo dos: Success y Failure. Las expresiones del apartado anterior producen un valor y una señal, que siempre es Success (S, para abreviar). Pero en determinadas situaciones una expresión puede no producir un valor, y sí la señal de Failure (F). Las señales no se pueden almacenar en variables, sino que sirven para controlar el flujo del programa. No hay que confundir una señal de Failure con un error del programa, que provoca su interrupción. La señal de Failure forma parte de su funcionamiento normal. Veamos un primer ejemplo de Failure. Estamos usando constantemente la variable especial OUTPUT. Una variable similar es INPUT. Cuando aparece, el programa se queda esperando que se teclee un valor. (*) * Vanilla y SNOBOL4+ son suficientemente "modernos" para que la entrada estándar venga del teclado. En TGB la entrada estándar venía del lector de tarjetas perforadas. ? A = INPUT 12 <--- Esto es tecleado por el usuario. Success ? OUTPUT = A 12 Success ? OUTPUT = DATATYPE(A) STRING Success Como se ve en el ejemplo, todo lo que escribamos se interpreta como string, aunque "tenga la forma" de número (se puede transformar en número, por ejemplo así: A = A + 0). Si al leer del input estándar se lee un carácter ASCII 26 (Control-Z), INPUT no toma ese valor, sino que se genera una señal Failure. Se puede hacer con CODE.SNO tecleando Ctrl-Z (a mí me aparece una flecha hacia la derecha): ? A = INPUT ^Z Failure ? OUTPUT = A 12 La variable A ha conservado el valor de antes. ------------------------------------------------------------------------------ CONTROL DEL FLUJO Ya hablamos antes de las tres secciones o campos de una sentencia. Las tres son opcionales. La primera, de estar presente debe empezar en la columna 1 y es una etiqueta (label). Las etiquetas permiten nombrar la sentencia para poder desviar hacia ella el flujo de ejecución del programa. Deben empezar por una letra (*) y después cualquier serie de caracteres excepto blancos o tabuladores (*). * Según toda la documentación que he leído también pueden empezar por un dígito, pero a mí me sale un error al intentar usar una etiqueta de ese tipo en el Goto (lo he probado también con una versión de SNOBOL4 para Linux) y no he encontrado ningún programa de ejemplo que use una etiqueta que empiece por un dígito. * Eso es lo que pone VTRM. Otras fuentes, como TGB, incluyen el punto y coma como carácter no permitido. En mis pruebas con SNOBOL4+ solo he podido usar (al menos en los Goto) letras inglesas, guión bajo y punto. Es decir, que más vale pensar en las etiquetas como si fueran nombres de variables. [La explicación de las dos notas anteriores se encuentra más adelante en la sección llamada REFERENCIA INDIRECTA.] En las etiquetas también se produce "case folding" (o no) según el valor de la variable &CASE. Al igual que las variables, las etiquetas son todas globales, aunque según he probado en SNOBOL4+, parece que no hay problema en que una etiqueta y una variable tengan el mismo nombre. En la sección central o cuerpo de la sentencia es donde está la "chicha". De momento solo hemos visto en ella asignaciones. El cuerpo está separado de los otros campos por espacios en blanco, pero también puede tener espacios dentro. La tercera es la sección "Goto". Puede tener (en principio; creo que se puede complicar) una de estas cuatro formas: :(etiqueta) :S(etiqueta) :F(etiqueta) :S(etiqueta1) F(etiqueta2) El primer caso es un Goto incondicional. La S significa Success y la F Failure. Independientemente del valor de &CASE, se pueden usar s y f. También se puede usar antes la f y después la s. Antes de los dos puntos debe ir siempre un espacio. Ejemplo de programa que muestra los números del 1 al 5: N = 0 BUCLE N = N + 1 OUTPUT = N EQ(A, N) :F(BUCLE) END La salida es: 1 2 3 4 5 En el ejemplo hay un elemento del que no hemos hablado: la función condicional EQ(A, N) que genera una señal de Success si A es igual a N y de Failure si no. ------------------------------------------------------------------------------ FUNCIONES INTEGRADAS SNOBOL4 también incluye el concepto de función. Como los operadores, una función parte de unos datos llamados argumentos para producir un resultado (también hay funciones que no necesitan argumentos). Este resultado siempre incluye una señal (S o F) y en caso de que la señal sea S, un valor. Este valor puede ser de cualquier tipo. La forma de invocar una función es la habitual: su nombre seguido de la lista de argumentos entre paréntesis y separados por comas. Entre el nombre de la función y el primer paréntesis no puede haber espacios, pero después de las comas sí. Si se omiten argumentos se les da el valor "". Si se ponen más de la cuenta, se ignoran. Hay muchas funciones que forman parte del lenguaje (integradas) y el usuario puede definir más. Las funciones condicionales, como EQ, generan una señal dependiendo de sus argumentos. Si la señal es S, devuelven el valor "". Son equivalentes a las funciones booleanas de otros lenguajes. Pero en SNOBOL4 no se devuelve FALSE: cuando la señal es F no se devuelve nada. Si ejecutamos este programita: A = 8 A = EQ(4, 3) OUTPUT = A END La salida será: 8 La asignación A = EQ(4, 3) no se ejecuta (sin que se produzca ningún error) y A conserva el valor que tenía. Si el 3 fuera un 4, A tomaría el valor "". Algunas funciones condicionales de SNOBOL4 se corresponden con los operadores relacionales de otros lenguajes. EQ(X,Y) se escribiría en otros lenguajes X=Y o X==Y. He aquí seis funciones de este tipo, con sus operadores (típicos) equivalentes. Todas ellas aceptan dos argumentos numéricos (o strings numéricos, que se transforman automáticamente en números): EQ = NE != LT < GT > LE <= GE >= Otras funciones condicionales son IDENT(A,B) que genera la señal S si A y B son del mismo tipo y valor y F en caso contrario; y DIFFER(A, B) que hace justamente lo contrario. IDENT(A) equivale a A == "" y DIFFER(A) a A != "". INTEGER(X) genera Success si X es un entero o un string con forma de entero. LGT(S, T) compara los strings S y T y "tiene éxito" si S > T, es decir, si S va después de T según el orden alfabético. Se compara de acuerdo con los códigos ASCII (en el caso de MS-DOS), lo que hay que tener en cuenta cuando se mezclan mayúsculas y minúsculas y cuando se usan letras no inglesas. El hecho de que, en caso de éxito, el valor devuelto por las funciones condicionales sea el string nulo está pensado para que pase lo más desapercibido posible. Esta es otra versión del programita que escribe los cinco primeros números: N = 1 BUCLE OUTPUT = N N = LT(N, 5) N + 1 :S(BLUCLE) END En el cuerpo de la tercera línea están mezcladas una asignación y una condición. Mientras N es menor que cinco, LT(N, 5) tiene éxito y devuelve "". La suma tiene prioridad sobre la concatenación, así que N aumenta en una unidad y después no cambia al concatenarse con "". (Recuérdese que al concatenar un número con el string nulo no cambiaba de tipo.). La sección goto de la misma línea hace que se repita el bucle. En el momento en que N vale 5, LT(N, 5) falla, la asignación no tiene lugar (antes ya vimos un ejemplo de esto) y el goto, como no tiene F, se ignora, pasando a la siguiente línea que termina el programa. Digamos que N = LT(N, 5) N + 1 es equivalente a (en un lenguaje tipo BASIC): IF N < 5 THEN N = N + 1 La concatenación de varias funciones condicionales falla si cualquiera de ellas falla y tiene éxito si todas ellas tienen éxito, devolviendo como valor final el string nulo. Por ejemplo: INTEGER(N) GE(N, 5) LE(N, 10) :S(LOQUESEA) es equivalente a: IF INTEGER(N) AND N >= 5 AND N <= 10 THEN GOTO LOQUESEA El goto se produce si N es un número entre 5 y 10, ambos incluidos. He aquí otras funciones (no condicionales) mencionadas en la parte de tutorial de VTRM: DATE() devuelve la fecha y la hora actual en forma de string del tipo: "07-15-23 19:54:29.03". DUPL(S, N) repite el string S N veces. REMDR(X, Y) devuelve el resto de dividir X por Y. REPLACE(S1, S2, S3) devuelve un string de la misma longitud que S1 pero con los caracteres de S2 cambiados por los de S3. Ejemplo, REPLACE("Rodolfo", "o", "a") devuelve "Radalfa". REPLACE("Rodolfo", "odfR", "afdM") devuelve "Mafalda". Si S2 y S3 no son igual de largos se genera Failure. SIZE(S) devuelve el número de caracteres del string S. TRIM(S) devuelve el string S sin los blancos que tenga al final (al principio no). Quita los espacios, pero no los tabuladores. ------------------------------------------------------------------------------ ENTRADA Y SALIDA Ya hemos hablado un poco sobre la entrada y salida y hemos usado en los ejemplos las variables INPUT y, sobre todo, OUTPUT. Vamos a profundizar más en estos asuntos. SOBOBOL4 puede comunicarse, en una u otra dirección, con hasta 16 ficheros a la vez. El concepto de fichero, en este contexto, es más amplio que el de fichero de disco, pudiendo referirse, además, a cualquier dispositivo de E/S. Cada uno de los ficheros usados debe estar asociado a un número entre 1 y 16. Esta asociación puede hacerse al invocar al intérprete con opciones numéricas. Por ejemplo: > SNOBOL4 PROGRAM /1=DATOS.TXT /2=RESULT.TXT Una vez hecha la asociación, antes de poder leer de los ficheros o escribir en ellos hay que invocar respectivamente a las funciones INPUT y OUTPUT, que no hay que confundir con las variables homónimas (se diferencian por los paréntesis y argumentos). La función INPUT admite hasta 4 argumentos, aunque a veces bastan los dos primeros: INPUT("S", 1) El primer argumento es un string que contiene el nombre de una variable que se usará para leer líneas de texto del fichero asociado con el número del segundo argumento. (*) * No sé si hay alguna forma de leer o escribir ficheros binarios. Si el segundo argumento no coincide con ninguno de los que se pasó en la invocación del intérprete se genera Failure. Si sí coincide se genera Success y se devuelve el string nulo. En adelante, cada vez que se use la variable S (por supuesto, ya escrita sin comillas) se leerá una línea del fichero DATOS.TXT y S tomará como valor el contenido de esa línea (después matizaremos un poco esto). La asociación del número con el fichero no es obligatorio hacerla en la invocación, puede añadirse el nombre del fichero como cuarto argumento de INPUT: INPUT("S", 1, , "DATOS.TXT") Como se ve, se pueden poner dos comas seguidas para saltarse un argumento. En este caso, el tercer argumento, la longitud de la línea, toma el valor por defecto, que es 80. El dato leído será siempre un string con un número fijo de caracteres (80, si no se especifica otro). Si la línea leída es más larga, se descartan los caracteres del 81 en adelante. Si es más corta, se le añaden espacios al final para que tenga 80 caracteres. Los caracteres de fin de línea se descartan. Todo esto, es, obviamente, otra herencia de la época de las tarjetas perforadas. Para eliminar los espacios de más se puede usar TRIM. En el momento en que se intenta leer de un fichero que ya se ha leído entero, la invocación de la variable genera Failure. La función OUTPUT es equivalente a INPUT, pero en este caso, cada vez que la variable tome un valor, se escribirá este en el fichero asociado. Si al ejecutarse OUTPUT el fichero no existe, se crea. Si ya existe, se borra su contenido. (Supongo que habrá alguna forma de añadir líneas a un fichero existente.) Si el valor de la variable es más largo de lo indicado en el tercer argumento no se ignora, sino que se divide en varias líneas. Si es más corto no se le añanden espacios. Los saltos de línea se insertan automáticamente. Las variables estándar INPUT y OUTPUT están ya asociadas a la entrada y salida estándar (teclado y pantalla, en el caso de SNOBOL4+ y Vainilla). Por razones históricas, se reservan los números 5 y 6 para estos "ficheros", cosa que hay que tener en cuenta. Se puede cambiar la asociación con una de las funciones: INPUT("INPUT", 5, 80, "ENTRADA.IN") Cada vez que aparezca INPUT, leerá del fichero ENTRADA.TXT, no del teclado. El número 5 podría ser otro. Esto también se puede hacer desde la llamada al intérprete con la opción I: > SNOBOL4 PROGRAM /I=ENTRADA.TXT Hay una opción O análoga. Otra alternativa es usar la redirección de MS-DOS con < y > (digo yo). ------------------------------------------------------------------------------ KEYWORDS Las keywords (palabras claves) son similares a las variables, pero empiezan por el carácter &. Ya hemos mencionado dos, &ALPHABET y &CASE, llamándolas "variables especiales". Las keywords sirven para controlar el comportamiento del intérprete y obtener información del sistema. Estas son unas cuantas. &TRIM (0) Si no vale 0 no se añaden espacios en blanco a las líneas leídas de un fichero y también se eliminan los espacios al final que ya estuvieran presentes. Por alguna extraña razón, su valor por defecto es 0. &MAXLNGTH (5000) Máxima longitud de los strings. Puede llegar hasta 32767 (5000 por defecto). &DUMP (0) Sirve para depuración de programas. Si tiene un valor entero no negativo, muestra al final de la ejecución los valores de todas las variables, por orden alfabético si es positivo. Las variables con valor nulo no se muestran. &LCASE Un string conteniendo las letras del alfabeto inglés, en minúsculas. &UCASE Un string conteniendo las letras del alfabeto inglés, en mayúsculas. ------------------------------------------------------------------------------ PATTERN MATCHING Este es el punto fuerte de SNOBOL4, la razón de ser del lenguaje. Permite la búsqueda dentro de strings de partes que concuerden con lo especificado en un patrón, que puede llegar a ser muy complejo. Todo el que haya tenido algún contacto con las expresiones regulares sabe de qué va la cosa (SNOBOL4 es anterior). Una sentencia "pattern matching" está formada por dos elementos: sujeto y patrón. El sujeto es un string. El patrón es normalmente una entidad de un tipo nuevo, identificado por la función DATATYPE como "PATTERN". Pero puede ser también un simple string: "SACACORCHOS" "CACO" Este es un ejemplo simplísimo de "patter matching". El sujeto debe estar al principio de la sección central de la sentencia, justo después de la etiqueta (y el espacio de detrás) si es que hay etiqueta. Entre sujeto y patrón solo hay uno o más espacios. La razón por la que los creadores del lenguaje decidieron especificar de la misma forma el "pattern matching" y la concatenación de strings se me escapa. El intérprete decide si es una cosa u otra por el contexto. En el caso anterior, como el string "SACACORCHOS" está al principio de la segunda sección de la línea, se trata de pattern matching. En cambio: T = "SACACORCHOS" "CACO" es una asignación y a derecha del = hay una concatenación de strings. En el caso: "SACACORCHOS" "CA" "CO" el primer espacio representa pattern matching, el segundo concatenación (así que es equivalente al primer ejemplo). El primer string evaluado es considerado un sujeto. Si queremos que el primer espacio sea interpretado como concatenación hay que usar paŕentesis: ("SACACORCHOS" "CA") "CO" Volvamos al primer ejemplo. El intérprete busca si en el sujeto "SACACORCHOS" aparece como substring el string "CACO". Sí aparece, lo que genera una señal S. El valor devuelto es el substring encontrado, "CACO". En caso de que no apareciera, la señal sería F y no se devolvería nada. Con CODE.SNO podemos comprobar que la señal es Success. Pero, ¿cómo podemos comprobar que el valor devuelto es "CACO"? El segundo ejemplo nos enseñó que no podemos usar una asignación convencional, y da igual que la variable se llame T u OUTPUT (en el segundo caso aparecería por pantalla SACACORCHOSCACO). Para esto hay que usar un operador nuevo, llamado asignación condicional y que se representa por un punto. A la izquierda del operador . debe haber un patrón (incluyendo un solo string) y a la derecha un nombre de variable. El conjunto es otro patrón equivalente al de la izquierda, pero con el efecto colateral de asignarle el valor devuelto a la variable. "SACACORCHOS" "CACO" . OUTPUT devuelve CACO. Por lo visto antes en el segundo ejemplo podríamos pensar que lo siguiente es equivalente: "SACACORCHOS" "CA" "CO" . OUTPUT pero no es así: devuelve CO, no CACO. Mi interpretación es que en este caso los espacios funcionan al revés: el primero como una concatenación y el segundo como pattern matching (!). Supongo que el intérprete mira primero el punto y une OUTPUT con lo primero a su izquierda que puede ser un patrón, en este caso, "CO". El espacio a la izquierda del patrón "CO" . OUTPUT obligatoriamente debe ser el operador pattern matching, y como a la izquierda del operador pattern matching debe ir un string, el otro espacio se debe interpretar como concatenación. Sí, la sintaxis de SNOBOL4 es peculiar. En realidad un pattern matching como "SACACORCHOS" "CACO" ya sabemos que va a devolver o "CACO" o nada, porque el patrón (string en este caso) no da más opciones. Lo normal es que no se sepa de antemano qué substring se va a encontrar (si se encuentra alguno). Para eso hay que construir patrones más elaborados usando operadores. Ya hemos visto uno (el punto), que no modifica el patrón sino que produce un efecto colateral. Otro que hemos visto con los strings y que también funciona con patrones es la concatenación (¡otra vez el espacio!). Si P1 y P2 son dos patrones, P1 P2 es otro patrón que casa con cualquier substring del sujeto que consista en un substring que case con P1 inmediatamente seguido de un substring que case con P2. Para poner un ejemplo veremos antes un tercer operador. La barra vertical funciona como un operador llamado alternativa. P1 | P2 (siempre con espacios) casa con cualquier substring que case con P1 o con P2: ? "SACACORCHOS" ("CO" | "CA") . OUTPUT CA Success En realidad "CO" también casaba, pero CA aparece antes en sl sujeto. Al igual que los strings, los patrones pueden ser asignados a variables: ? P = ("CO" | "CA") . OUTPUT ? "SACACORCHOS" P CA Success Vamos ahora con el ejemplo de concatenación de patrones. ? P = ("CO" | "CA") "R" ? "SACACORCHOS" P . OUTPUT COR Success P casa con COR o con CAR y se encuentra el primero. En una alternativa puede aparecer el string nulo. Un patrón que casa tanto con "hotel" como con "hoteles" es: "hotel" ("es" | "") (los paréntesis son esenciales). Si uno de los elementos de la alternativa falla, es decir, genera una señal F, se ignora. EQ(N, 1) 'ZORRO' | EQ(N, 2) 'LOBO' es un patrón cuyo significado dependerá del valor de N: ZORRO si N vale 1; LOBO si N vale 2. Si N = 1 la parte izquierda permanece al concatenarse el string nulo con "ZORRO"; la parte derecha falla. Y si N = 2 lo contrario. ------------------------------------------------------------------------------ PATRONES PRIMITIVOS Son patrones predefinidos con un significado especial que depende del contexto. Hay siete pero veremos de momento dos: REM (de "remainder") casa con el resto del sujeto. Ejemplo: ? "Las ciudades invisibles" "v" REM . OUTPUT isibles ARB (de "arbitrary") casa con 0 o más caracteres. Ejemplo: ? "Las ciudades invisibles" "c" ARB . OUTPUT "s" iudade ------------------------------------------------------------------------------ EL CURSOR Cuando el intérprete analiza un string buscando un pattern match mueve un "cursor" a derecha e izquierda. Este cursor (lógico) está siempre entre los caracteres (es una rayita, no un rectangulito). La posición del cursor al principio es 0. Cuando es 5 quiere decir que a su izquierda hay 5 caracteres. La posición del cursor se puede consultar mediante el operador unario @, que debe ir inmediatamente antes de una variable. Ejemplos: ? "Rodolfo" "d" @OUTPUT 3 Success ? "Rodolfo" "d" @OUTPUT "l" @OUTPUT 3 Failure ? "Rodolfo" "d" @OUTPUT ARB "l" @OUTPUT 3 5 Success ? "Rodolfo" @OUTPUT "d" 0 1 2 Success ------------------------------------------------------------------------------ FUNCIONES DE PATRONES Las siguientes funciones devuelven un patrón, en lugar de un número o un string. Son similares a los patrones primitivos, pero necesitan un argumento, que puede ser un entero o un string. Veremos primero funciones que esperan un argumento entero. LEN(I) Casa con cualquier string de exactamente I caracteres. Ejemplo: ? "Rodolfo" "o" LEN(4) . OUTPUT "o" dolf Success Usando &ALPHABET podemos acceder al carácter de código I así: &ALPHABET LEN(I) LEN(1) (Un poco más largo que CHR$(I).) La operación inversa, acceder al número correspondiente a un carácter es posible con la intervención del operador @: ? &ALPHABET @N "A" ? OUTPUT = N 65 POS(I) y RPOS(I) no casan con ningún substring, sino que generan S o F según la posición del cursor sea o no I (desde la derecha, en el caso de RPOS). POS(0) exige que el cursor esté al principio del sujeto y RPOS(0) que esté al final (son equivalentes a los símbolos ^ y $ de las expresiones regulares). Ejemplo: ? &ALPHABET POS(65) ARB . OUTPUT POS(70) ABCDE TAB(I) y RTAB(I) son son una especie de mezcla de POS y RPOS con ARB. El ejemplo anterior se podría escribir un poco más corto así: ? &ALPHABET POS(65) TAB(70) . OUTPUT ABCDE TAB(I) casa con el substring que va desde la posición actual del cursor hasta la posición I. RTAB es igual pero cuenta I desde la derecha. Una forma más enrevesada de hacer lo mismo sin TAB (y sin ARB) es esta: ? &ALPHABET POS(65) @N LEN(70 - N) . OUTPUT ABCDE TAB y RTAB casan con el string nulo, pero fallan si el cursor está pasado el argumento o si este es más grande que el sujeto. Posible confusión: RTAB cuenta I desde la derecha, pero casa a la izquierda de esa posición Ahora veremos funciones que esperan un argumento de tipo string. ANY(S) casa con un carácter contenido en S. NOTANY(S) casa con un carácter que no esté contenido en ese. S tiene que ser un string no nulo. Ejemplo: ? VOCAL = ANY("AEIOU") ? CONSON = NOTANY("AEIOU") ? ANIMAL = "MURCIELAGO" ? ANIMAL VOCAL . OUTPUT U ? ANIMAL (VOCAL VOCAL) . OUTPUT IE ? ANIMAL (VOCAL CONSON VOCAL) . OUTPUT ELA SPAN(S) y BREAK(S) son versiones multicaracteres de ANY y NOTANY. S debe ser un string no nulo. SPAN casa uno o más caracteres seguidos, todos ellos contenidos en S. BREAK casa con caracteres no contenidos en S. Ejemplos: ? &ALPHABET POS(65) LEN(26) . LETRAS ? PALABRA = SPAN(LETRAS) ? ESPACIO = BREAK(LETRAS) ? YO = "RODOLFO VALEIRAS REINA" ? YO PALABRA . OUTPUT RODOLFO ? YO PALABRA ESPACIO PALABRA . OUTPUT VALEIRAS ? YO PALABRA . OUTPUT RPOS(0) REINA ------------------------------------------------------------------------------ PATERN MATCHING CON SUSTITUCIÓN Los substrings encontrados mediante pattern matching pueden sustituirse por otros. La forma de hacerlo es mediante el operador =. (Parece que una de las características clave que guiaban a los creadores de SNOBOL4 en su diseño fue la de no malgastar signos.) Copio de VTRM: SUBJECT PATTERN = REPLACEMENT SUBJECT debe ser un nombre de variable (al que previamente se le ha asignado un valor string). Un literal no puede cambiarse. REPLACEMENT es un string o una expresión cuyo valor es un string. Un primer ejemplo: ? YO = "RODOLFO VALEIRAS REINA" ? YO "RODOLFO" = "GERARDO" ? OUTPUT = YO GERARDO VALEIRAS REINA La cadena de reemplazo puede ser el string nulo, en cuyo caso puede omitirse: ? YO = "RODOLFO VALEIRAS REINA" ? YO " REINA" = ? OUTPUT = YO RODOLFO VALEIRAS Solo se reemplaza la primera aparición del patrón: ? YO = "RODOLFO" ? YO "O" = "A" ? OUTPUT = YO RADOLFO Para que el reemplazo se repita hay que usar un bucle (es un programa, parece que no se puede hacer con CODE.SNO (*)): YO = "RODOLFO" L YO "O" = "A" :S(L) OUTPUT = YO END La salida es RADALFA. * Según leo (después) en VTRM, para casos como este, con CODE.SNO hay que añadir un punto y coma después de cerrar el paréntesis de la sección Goto: ? YO = "RODOLFO" ?L YO "O" = "A" :S(L); ? OUTPUT = YO RADALFA Un subtring encontrado en el sujeto puede usarse directamente en la misma sentencia formando parte del string de sustitución. He aquí un ejemplo simple (en VTRM hay uno un poco más complejo): ? P = ANY("AEIOU") . V ? ANIMAL = "MURCIELAGO" ? ANIMAL P = V V ? OUTPUT = ANIMAL MUURCIELAGO ? ANIMAL = "SERPIENTE" ? ANIMAL P = V V ? OUTPUT = ANIMAL SEERPIENTE ------------------------------------------------------------------------------ PATTERN MATCHING ANCLADO Y NO ANCLADO Todos los ejemplos de patter matching que hemos visto hasta ahora son "no anclados" (unanchored). El pattern matching anclado (anchored) solo busca substrings que empiecen en el primer carácter del sujeto. Es mucho más eficiente y se puede usar más de lo que parece... ------------------------------------------------------------------------------ REFERENCIA INDIRECTA El operador unario $ permite llamar a una variable cuyo nombre es el valor devuelto por una expresión: ? ABC = "El nombre de un periódico" ? OUTPUT = $("A" "B" "C") El nombre de un periódico ? OUTPUT = $"ABC" El nombre de un periódico ? P = "ABC" ? OUTPUT = $P El nombre de un periódico Cualquier string no nulo (con el límite de tamaño general de los strings, 5000 caracteres, no 120) puede ser el nombre de una variable, siempre que se llame usando referencia indirecta. ? $ABC = 50 ? OUTPUT = $"El nombre de un periódico" 50 ? $$ABC = 10 ? OUTPUT = $50 10 La referencia indirecta permite lo que en VTRM se llama "programción asociativa". En el ejemplo que pone, se lee un fichero con los nombres de todos los estados de EEUU y sus capitales, creando para cada pareja una variable con el nombre del estado cuyo valor es su capital (p. 41; ficheros en VANILLA). La referencia indirecta también puede usarse en la sección Goto, lo cual explica por qué según toda la documentación sobre SNOBOL4 las etiquetas pueden incluir letras no inglesas y otros caracteres, e incluso empezar por un dígito: para incluir esas etiquetas en la sección Goto hay que usar referencia indirecta: OUTPUT = "Escribe 1 o 2:" DONDE = TRIM(INPUT) :($DONDE) 1 OUTPUT = "UNO" :(END) 2 OUTPUT = "DOS" :(END) END Este tipo de Goto puede sustituir a un bloque "case switch" o similar. ------------------------------------------------------------------------------ EXPRESIONES NO EVALUADAS Cuando se define un patrón como valor de una variable, las referencias a variables que incluya se evalúan en ese momento. Por ejemplo, si N vale 5, definir P = LEN(N) es lo mismo que definir P = LEN(5). El operador unario * (otro signo reciclado) indica que la expresión que le sucede debe ser evaluada cuando se use, no cuando se defina: ? P = LEN(*I) . OUTPUT ? YO = "RODOLFO" ? YO P ? I = 4 ? YO P RODO ? I = 6 ? YO P RODOLF En la primera sentencia YO P, I valía 0 (en realidad "", que tiene el mismo efecto) y se devuelve el string nulo. El operador * se puede usar con una expresión que no consista solamente en una variable: ? P = LEN(*(2 * I)) . OUTPUT ? I = 1 ? YO P RO ? I = 2 ? YO P RODO Pero solo puede usarse en patrones. Hemos visto un ejemplo en el argumento de una función de patrón. En este otro se usa en una alternativa: ? P = ("F" | *A) . OUTPUT ? A = "W" ? YO P F ? A = ANY("AEIOU") ? YO P O ------------------------------------------------------------------------------ ASIGNACIÓN INMEDIATA Recordemos el operador . y la asignación condicional con un ejemplo: ? YO = "RODOLFO" ? YO = "R" ARB . OUTPUT "L" ODO ARB era un patrón primitivo que casa con cualquier substring. El substring más corto a la derecha de "R" es el string nulo, después "O", "OD", etc. Todos estos substrings casan con "R" ARB, pero no con "L", que está después de la asignación. El intérprete va probando todos estos substrings, pero finalmente solo hace la asignación cuando todo el "pattern matching" tiene éxito. Esto sucede cuando prueba "ODO". En ese momento el "pattern matching" genera la señal Success y la variable OUTPUT toma el valor "ODO", que se muestra por la salida estándar. Se le llama asignación condicional porque solo tiene lugar en caso de éxito. Repasado esto, veamos ahora la asignación inmediata. Su operador es $, ahora usado como binario (recordemos: espacios a los dos lados). En este caso, cuando un substring "va casando de momento" se produce la asignación, sin esperar a que el "pattern matching" completo se resuelva. ? YO = "R" ARB $ OUTPUT "L" O OD ODO Success Todos los susbtrings que se van probando, que casan con "R" ARB, se van asignando a OUTPUT antes de comprobar si casan con el resto del patrón. Cuando se resuelve el "pattern matching" general, se produce, en ese caso la señal Success y termina la búsqueda. La asignación inmediata se produciría también en caso de Failure: ? YO = "R" ARB $ OUTPUT "K" O OD ODO ODOL ODOLF Failure La asignación inmediata puede combinarse con el operador * para crear patrones muy potentes (VTRM p. 44). ------------------------------------------------------------------------------ ARRAYS En SNOBOL4 también existen los arrays de una, dos, tres o más dimensiones. Para declarar un array se usa la función ARRAY. Esta función admite uno o dos argumentos. El primero, obligatorio, indica las dimensiones. El segundo, opcional, establece el valor inicial de todos los elementos: $ A = ARRAY(5, 0) OUTPUT = A[1] 0 Si se omite el segundo argumento, el valor inicial es el string nulo. Como se ve en el ejemplo, para acceder a un elemento del array se usan corchetes y el índice del elemento, desde 1 hasta el número indicado en la creación del array (en el ejemplo, 5). El identificador del array no se puede separar del corchete de apertura. En lugar de corchetes cuadrados se pueden usar angulares (<>). El primer argumento de ARRAY es, en realidad, algo llamado prototipo: un string que indica las dimensiones. En el caso de una sola dimensión podemos poner un entero porque el intérprete lo convierte en string, pero para dos dimensiones hay que usar un string: $ B = ARRAY("3,3") No se pueden añadir espacios ni a izquierda ni a derecha de la coma. De la misma forma que se accede a un elemento del array se puede referenciar para asignarle un valor. En el caso de varias dimensiones, los índices se separan con comas (en este caso sí puede haber espacios): $ A[1] = 6 $ A[2] = "JFK" $ B[1, 2] = LEN(5) LEN(1) . L Cada elemento de un array puede ser de un tipo diferente y pueden cambiar de valor y de tipo durante la ejecución del programa. Si se intenta usar un índice fuera de rango se genera la señal Failure, pero no un error. Esto permite iterar por los elementos de un array sin preocuparnos de su tamaño: I = 0 L I = I + 1 OUTPUT = A[I] :S(L) Al parecer no hay una forma rápida de crear un array y darle valores específicos a todos sus elementos. He hecho este programita como demostración de cómo especificar los valores de los elementos en un string. Cambiando el valor de D, variará la longitud y los valores del array A (se supone que todos los valores son enteros positivos). D = "13,23,33,45,4,9,199" P = SPAN("0123456789") DD = D N = 0 L1 DD P ("," | "") REM . DD :F(L2) N = N + 1 :(L1) L2 A = ARRAY(N) I = 0 L3 I = I + 1 D P . A ("," | "") REM . D :S(L3) END El prototipo que define las dimensiones de un array es más flexible de lo que hemos visto, pudiendo indicarse cualesquiera números enteros para sus índices extremos. ARRAY("4:8,-3,1") crea un array con dos dimensiones, ambas con cinco elementos, la primera con índices entre 4 y 8 y la segunda entre -3 y 1. Un array no puede cambiar de tamaño. Si se declara de nuevo se pierde su contenido anterior. Si A es un array y lo asignamos como valor de B, haremos que tanto A como B se refieran (apunten) al mismo array, no crearemos un array nuevo. Supongamos que A es el array definido en el programa anterior. ? OUTPUT = A[1] 13 ? B = A ? OUTPUT = B[1] 13 ? A[1] = 15 ? OUTPUT = B[1] 15 ? B[2] = 9 ? OUTPUT = A[2] 9 Para crear un array nuevo copiando el contenido de uno previo existe la función COPY. Si eso era lo que queríamos, en el caso anterior habría que sustituir B = A por B = COPY(A). ------------------------------------------------------------------------------ TABLAS Una tabla es como un array unidimensional, pero sin tamaño fijo y con índices de cualquier tipo. Se crean con la función TABLE, que no requiere argumentos: ? T = TABLE() ? T["NOMBRE"] = "RODOLFO" ? T["EDAD"] = 26 ? OUTPUT = T["NOMBRE] " " T["APELLIDOS"] " " T["EDAD"] RODOLFO 26 En las tablas no existe el concepto de "fuera de rango". Si se usa un índice nuevo ("APELLIDOS" en el ejemplo) se crea el nuevo elemento con el valor por defecto "" (no se puede especificar otro). Una tabla se puede convertir en un array bidimensional de N filas (siendo N el número de elementos de la tabla) por 2 columnas. El primer elemento de cada fila es el índice, el segundo el valor: ? A = CONVERT(T,"ARRAY") ? OUTPUT = A[1,1] ": " A[1,2] NOMBRE: RODOLFO ? OUTPUT = A[2,1] ": " A[2,2] EDAD: 26 Esto puede ser muy útil cuando los índices no son conocidos, por ejemplo, por proceder de un fichero. La transformación inversa también es posible. El array de partida debe tener dos dimensiones, la segunda de longitud 2. Un array unidimensional no se puede transformar en tabla. ------------------------------------------------------------------------------ EL OPERADOR NOMBRE El operador unario . (ya no digo nada) devuelve el nombre (NAME) de una variable. Es el inverso de $: ? V = 65 ? OUTPUT = .V V ? OUTPUT = $.V 65 En este ejemplo podríamos haber puesto "V" en lugar de .V, pero eso no funciona para un elemento de un array. ? A = ARRAY(5) ? A[2] = 65 ? OUTPUT = $"A[2]" ? OUTPUT = $.A[2] 65 ? V = .A[2] ? OUTPUT = $V 65 El problema en el primer uso de $ es que A[2] es un nombre de variable válido y el intérprete no entiende que es una referencia a un elemento de un array. En el segundo uso sí lo entiende. El nombre puede estar guardado en una variable (V = "A[2]" no funcionaría). ------------------------------------------------------------------------------ FUNCIONES DEFINIDAS POR EL PROGRAMA Además de las funciones integradas, SNOBOL4 permite definir nuevas funciones, lo que facilita una mejor organización del programa. La forma en que SNOBOL4 maneja las funciones es bastante particular. Su comportamiento no se define en tiempo de compilación, sino de ejecución. Según VTRM este sistema es más flexible. Aunque son mucho más potentes, a mí me recuerdan un poco a las subrutinas de BASIC. Para definir una función nueva hay que usar la función (integrada) DEFINE. Su argumento es un prototipo, es decir, un string que describe el nombre y los argumentos de la función. Vamos a empezar con una función sin argumentos: DEFINE("SALUDA()") El cuerpo de la función va aparte de esta definición y debe empezar por una etiqueta con el mismo nombre: SALUDA OUTPUT = "Yo te saludo, oh, ser humano." :(RETURN) Al ejecutarse DEFINE se crea la función SALUDA, pero se ignora de momento (al menos eso es lo que yo he entendido) el cuerpo de dicha función. Cuando se invoca la función, se busca en el código la etiqueta que señala dónde empieza su cuerpo y se ejecuta lo que haya a partir de ahí. En algún momento debe tomarse, en una sección Goto, una salida con una etiqueta especial, RETURN, que indica que la ejecución de la función ha terminado, siguiendo el flujo donde estaba la invocación. La etiqueta RETURN está reservada y no debe usarse para otra cosa. Si creamos un programa uniendo sin más las dos líneas anteriores (da igual el orden) se producirá un error al ejecutarlo. Eso es porque, incluso aunque la llamada a DEFINE esté antes indicando el nombre de la función, la línea etiquetada con SALUDA se ejecuta normalmente, y se produce un error al intentar salir (¿de dónde?) con :(RETURN). El cuerpo debe estar aislado de alguna forma. Lo que se recomienda en VTRM es terminar el cuerpo con una etiqueta con el mismo nombre de la función terminada en _END, y poner la definición justo antes del cuerpo que le corresponde con un goto a esta segunda etiqueta: DEFINE("SALUDA()") :(SALUDA_FIN) SALUDA OUTPUT = "Yo te saludo, oh, ser humano." :(RETURN) SALUDA_FIN Esto da una apariencia compacta a la función, algo más cerca de las funciones de otros lenguajes. He puesto _FIN y no _END, porque eso no forma parte del lenguaje y da igual una cosa que otra. Otra opción sería poner todos los cuerpos juntos, al principio o al final. Veremos un ejemplo después de ver una función con un argumento y que devuelve un valor. Todas las funciones, si no generan la señal Failure, devuelven un valor. Si no se hace explícitamente que devuelva otra cosa, devolverá la cadena nula. Es el caso de la función SALUDA (sería lo que en Pascal o Euphoria se llama un procedimiento). Para devolver un valor expresamente hay que asignarle ese valor a una variable especial con el mismo nombre de la función. DEFINE("CUAD(N)") :(CUAD_FIN) CUAD CUAD = N * N :(RETURN) CUAD_FIN Un detalle importante que conviene resaltar, ya que supone otra diferencia con otros lenguajes, es que la línea CUAD = N * N no implica la salida de la función. Vamos a juntar las dos funciones y hacer una llamada a cada una. En este caso vamos a poner los cuerpos al principio. :(FIN_FUNCS) SALUDA OUTPUT = "Yo te saludo, oh, ser humano." :(RETURN) CUAD CUAD = N * N OUTPUT = "¿Verdad que soy lista? :(RETURN) FIN_FUNCS ************************************************************ DEFINE("SALUDA()") DEFINE("CUAD(N)") ************************************************************ SALUDA() OUTPUT = CUAD(6) END Al ejecutar este maravilloso programa tendremos la siguiente salida: Yo te saludo, oh, ser humano. ¿Verdad que soy lista? 36 Si queremos que la función produzca, en ciertas condiciones una señal Failure (y no devuelva nada), debemos usar en la sección Goto la etiqueta especial FRETURN. Por ejemplo, esta es una versión modificada del cuerpo de CUAD que genera Failure después de mostrar un mensaje cuando se invoca con un argumento no entero: CUAD IDENT(DATATYPE(N),"INTEGER") :F(CUAD_F) CUAD = N * N :(RETURN) CUAD_F OUTPUT = "Solo argumentos enteros." :(FRETURN) Si una función no genera la señal Failure genera la señal Success. Esto sucede aunque se genere Failure en la asignación que establece el valor devuelto. Supongamos que queremos "traducir" la función GT por MQ (de "mayor que"). Podríamos intentarlo así: DEFINE("MQ(X,Y)") :(MQ_FIN) MQ MQ = GT(X,Y) :(RETURN) MQ_FIN ¿Qué pasaría en una llamada MQ(1,2)? El flujo va a la etiqueta MQ y se ejecuta MQ = GT(1,2). GT(1,2) genera una señal Failure, sin devolver nada, y la asignación no tiene lugar y genera también una señal Failure. Pero como en la sección Goto de la sentencia hay una salida RETURN incondicional, la función genera Success, devolviendo el string nulo. Lo correcto sería poner en la sección Goto :S(RETURN):F(FRETURN). De hecho, la asignación también sobra, bastaría GT(X,Y). En resumen, la salida "a través" de RETURN genera Success y devuelve el valor que tenga la variable del mismo nombre que la función o "". La salida a través de FRETURN genera Failure y no devuelve ningún valor. ------------------------------------------------------------------------------ VARIABLES LOCALES Una función puede usar sus propias variables locales sin interferir con otras del mismo nombre que pudieran existir globalmente. Los nombres de estas variables se indican en el prototipo después del paréntesis de cierre: DEFINE("FUNC(A,B)X,Y") Tanto FUNC, como A y B y X e Y tienen un significado especial en el cuerpo de la función y no colisionan con variables globales con los mismos nombres. En los ejemplos de VTRM añaden, entre la definición de la función y su cuerpo, una o varias líneas de iniciación. La idea, según veo, es evitar todo el trabajo posible a la función, de forma que lo que basta hacerlo una vez no haya que repetirlo cada vez que se llama a la función. El problema, me parece a mí, es que estas líneas de iniciación incluyen siempre la definición de variables, que al estar fuera del cuerpo, no pueden ser locales. Estaría bien que el lenguaje incluyera el concepto de iniciación con variables locales (las funciones tendrían entonces tres partes: definición, iniciación y cuerpo), pero no es así, desde el punto de vista del intérprete, estas líneas de iniciación son líneas normales y corrientes del programa (*). Lo que veo que hacen en los ejemplos es usar nombres de variables que empiezan por el nombre de la función. Por ejemplo, una función es SHIFT y una variable que se usa en ella se llama SHIFT_PAT. Esta función es global y si, en otra parte del programa tomara otro valor, la función SHIFT fallaría. SHIFT_PAT podría definirse dentro de la función (entonces se llamaría seguramente solo PAT), pero esta definición se repetiría cada vez que se llama a SHIFT. * De hecho, las líneas pertenecientes al cuerpo son también líneas normales y corrientes del programa. Nada impide que el flujo que ha entrado por la etiqueta del nombre de la función salte a una parte totalmente distinta del programa y tal vez salir por un RETURN que se utiliza para varias funciones distintas. No creo que sea recomendable, pero es posible. Otra cosa que se aprende de los ejemplos de VTRM es que no siempre es necesario que una función que devuelve un valor incluya una asignación del tipo FUNC = loquesea. Lo que importa es que al salir de la función la variable FUNC valga loquesea. En este ejemplo uso el operador punto: DEFINE("PL(S)") :(PL_FIN) * Devuelve la primera letra de S. PL S LEN(1) . PL :(RETURN) PL_FIN Usando iniciación (aunque en este caso tal vez no mereciera la pena) quedaría así: DEFINE("PL(S)") * Devuelve la primera letra de S. PL_PAT = LEN(1) . PL :(PL_FIN) PL S PL_PAT :(RETURN) PL_FIN ------------------------------------------------------------------------------ ARGUMENTOS POR VALOR Y POR REFERENCIA La mayoría de las veces los argumentos de una función se pasan por valor, es decir, si usamos una variable como argumento lo que va a usar la función es el valor que tenga en ese momento. Esto sucede en SNOBOL4 y en la mayoría de lenguajes (yo creo que casi todos). Veamos un ejemplo (muy tonto, como casi todos los que pongo). DEFINE("FUNC(V)") :(FUNC_END) FUNC V = 8 FUNC = 9 :(RETURN) FUNC_END V = 19 OUTPUT = FUNC(V) " " FUNC(123) " " V END FUNC es una función que devuelve siempre el valor 9. Su argumento, V, no sirve para nada, pero se le da, en el cuerpo de la función, el valor 8, para ver qué pasa. Fuera de la función, otra variable llamada V, pero distinta, toma otro valor. La salida de este programa es: 9 9 19 Observamos: Los argumentos de las funciones son como variables locales que toman inicialmente el valor que se le da al llamar la función. En la primera línea del cuerpo de la función, antes de la asignación V = 8, V vale 19 (en la primera llamada) y 123 (en la segunda llamada). El cambio de valor a 8 es interno, no afecta al V de fuera. Una llamada a FUNC puede ser un nombre de variable (como en el primer caso), pero también un valor literal (como en el segundo caso) o, en general, una expresión. Lo que pasamos es el valor devuelto por esa expresión. En el caso de pasar una variable estamos pasando su valor, la variable en sí no importa. Por eso se habla de paso de argumentos por valor. Algunos lenguajes (Pascal, por ejemplo) tienen mecanismos para indicar expresamente que un argumento de una función debe pasarse por referencia. En ese caso se pasa una variable (su dirección en memoria, en el fondo), no su valor (no sirve como argumento una expresión, solo una variable). Los cambios que se hagan al argumento en el cuerpo de la función le afectan a la variable (que ahora sí es la misma). Si en el ejemplo, V fuera un argumento por referencia, cambiaría su valor a 8 (la salida sería: 9 9 8). La V de fuera podría tener otro nombre distinto de V. SNOBOL4 no tiene un mecanismo expresamente diseñado para pasar argumentos por referencia, pero con lo que ya sabemos es posible hacer que se comporten de esa forma. VTRM pone el ejemplo sencillo y útil de una función SWAP(X,Y) que intercambia los valores de X e Y. Este otro, simplemente le da a una variable el valor 42. DEFINE("RESPUESTA(V)") :(RESPUESTA_FIN) RESPUESTA $V = 42 :(RETURN) RESPUESTA_FIN RESPUESTA("X") OUTPUT = X END La salida de este programa es 42. Hay que pasar como argumento "X" (o .X), no X. El uso del operador $ hace el truco. Hay un problema: no funciona bien con "V": como en el caso anterior, esa V de dentro de la función es distinta a la de fuera y su valor se pierde al salir. Pasaría lo mismo si usáramos variables locales y el argumento coincidiera con alguna de ellas. Este "problemilla" es ladinamente pasado por alto en VTRM, pero he comprobado que pasa lo mismo con su ejemplo. Algunos tipos de datos, como los arrays y las tablas se pasan por referencia de forma implícita. ------------------------------------------------------------------------------ FUNCIONES RECURSIVAS SNOBOL4 permite el uso de funciones recursivas. A mí lo primero que se me ocurre para probar la recursividad es definir una función factorial: DEFINE("FACTORIAL(N)") :(FACTORIAL_END) FACTORIAL GT(N,1) :F(FACTORIAL_1) FACTORIAL = N * FACTORIAL(N - 1) :(RETURN) FACTORIAL_1 FACTORIAL = 1 :(RETURN) FACTORIAL_END Si (usando SNOBOL4+ o Vanilla) creamos un fichero con ese contenido y lo llamamos FACT.SNO, podemos probar la función con CODE.SNO así: ? SLOAD("FACT.SNO") ? OUTPUT = FACTORIAL(6) 720 En Vanilla, el máximo argumento que admite esa función es 7, cuyo factorial es 5040. El factorial de 8 es 40320, mayor que el máximo número entero permitido (32768). En SNOBOL4+ se pueden poner argumentos mayores pero indicando que son decimales: ? OUTPUT = FACTORIAL(8.) 40320. VTRM pone como ejemplo un programa (copiado del libro "Algorithms in Snobol4") para escribir números enteros en mumeración romana ------------------------------------------------------------------------------ TIPOS DE DATOS DEFINIDOS POR EL PROGRAMA En uno de los primeros apartados hablamos de los tipos de datos de SNOBOL4. A los STRING, INTEGER y REAL que conocíamos entonces hemos añadido PATTERN, ARRAY y TABLE. También hay un tipo NAME para lo que en el GB llaman "nombre creado" (el nombre de una "variable creada", por oposición a "variable natural"). Por ejemplo, si A es un array de una dimensión, A<1> es una variable creada y .A<1> es un objeto de tipo NAME (recordemos que "A<1>" no es lo mismo): ? A = ARRAY(5) ? B = 5 ? OUTPUT = DATATYPE(.A<1>) " " DATATYPE(.B) NAME STRING Además de estos tipos predefinidos (aún hay algunos más), SNOBOL4 permite definir tipos nuevos. Estos tipos nuevos son parecidos a los "records" de Pascal. Cada tipo tiene un nombre y varios campos, también con sus nombres. Un poco como una base de datos. Un nuevo tipo se define con la función DATA con un prototipo como argumento (copio el ejemplo de VTRM): ? DATA("PRODUCT(NAME,PRICE,QUANTITY,MFG)") MFG es la abreviatura de "manufacturing" (el nombre de la empresa que fabrica el producto). No pueden incluirse espacios. Ahora ya tenemos un tipo nuevo llamado PRODUCT. Creemos una variable de este tipo. ? P1 = PRODUCT("CAPERS", 2, 48, "BRINE BROTHERS") ? OUTPUT = DATATYPE(P1) PRODUCT Como se ve, la definición del tipo no incluye cómo debe ser cada campo, solo los nombres. Se ha creado una función PRODUCT que devuelve un objeto (este es el término usado en VTRM) del nuevo tipo. Es una función similar a ARRAY o TABLE. Pero por cada argumento se ha creado también una función, cuyo argumento debe ser de tipo PRODUCT. Podemos usar estas funciones para conocer el valor de un campo o cambiarlo: ? OUTPUT = NAME(P1) " " PRICE(P1) CAPERS 2 ? QUANTITY(P1) = 47 ? OUTPUT QUANTITY(P1) 47 Los argumentos de la función PRODUCT son opcionales, tomando los distintos campos no incluidos el valor por defecto "" (se pueden escribir comas seguidas). Como sucedía con los arrays y las tablas, una variable como P1 es un puntero al objeto y si la asignamos a otra variable: ? P2 = P1 tendremos dos formas de llamar al mismo objeto. ? NAME(P1) = "ALCAPARRAS" ? OUTPUT = NAME(P2) ALCAPARRAS Si lo que queremos es crear otro objeto diferente con el mismo contenido, podemos usar la función COPY: ? P2 = COPY(P1) ? PRICE(P1) = 3 ? OUTPUT = PRICE(P2) 2 Los tipos definidos por el programa se pueden usar para crear estructuras complejas, como colas, pilas, árboles y grafos arbitrarios. La idea es usar uno de los campos como puntero a otro objeto. En VTRM lo ejemplifica con una versión de PRODUCT a la que se ha añadido el campo adicional MFGLINK, que va a apuntar siempre a otro objeto que tenga el mismo MFG: ? DATA("PRODUCT(NAME,PRICE,QUANTITY,MFG,MFGLINK)") Se crea una tabla M que va a tener un elemento por cada MFG: ? M = TABLE() Los nuevos objetos se crean como elementos de la tabla. Pero en realidad cada elemento no va a ser un objeto único (o no siempre va a ser un objeto único), sino una lista de objetos que se señalan unos a otros. La orden básica de creación de un nuevo objeto será: ? M = PRODUCT(..., ..., ..., COMPANY, M) COMPANY es una variable que contiene el nombre del fabricante, el MFG. Los puntos son los otros campos, que variarán. Cuando el valor de COMPANY es nuevo, se crea un nuevo elemento en M, un PRODUCT cuyo campo MFGLINK vale "". Cuando el valor de COMPANY se repite por primera vez, un nuevo PRODUCT sustituye al anterior, con MFGLINK apuntando al primero. Así se van creando cadenas de productos que comparten el mismo fabricante.