Introducción a Modula-2 para Pascal
1-Introducción
Módula-2 es un lenguaje creado por Niklaus Wirth, el diseñador
del lenguaje Pascal, y es posterior a éste. En Modula-2, Wirth trató
de corregir las deficiencias y/o carencias que tanto él como los
distintos programadores a lo ancho del mundo habían ido descubriendo.
Muchas de estas mejoras las incorporan los compiladores de Pascal actuales,
como Turbo Pascal, TMT Pascal, FPK Pascal o Delphi, pero no son parte del
lenguaje Pascal estándar, lo que ha provocado que muchas Universidades
empleen Modula-2 en vez de Pascal como lenguaje para introducirse en el
mundo de la programación.
Por eso, creo que puede haber gente a quien le resulte interesante una
introducción
a Modula-2, en la que se comenten las principales diferencias con Pascal.
En esta introducción supondré que se han seguido (y asimilado)
los temas básicos del curso, al menos los que van del 1 al
12. Como siempre, procuraré ser mucho más práctico
que teórico: intentaré poner ejemplos y después comentarlos,
en vez de dar primero la carga teórica y luego los ejemplos.
2-Esqueleto de un
programa
Vamos a comenzar por ver cómo sería el "esqueleto" de un
programa, la parte mínima que debe existir en todo programa en Modula-2.
Doy por sentado que ya se sabe que en Pascal sería:
begin
end.
Y en muchos compiladores es posible que se nos obligase a emplear la
sentencia "program", de modo que quedaría
program NombreDelPrograma;
begin
end.
Pues bien, en Modula-2 es MUY parecido, pero aun así existen
diferencias importantes:
MODULE vacio;
BEGIN
END vacio.
-
La primera diferencia es la palabra "MODULE" en vez de "program": se pretende
que Modula-2 sea un lenguaje modular: que un programa de un cierto
tamaño se pueda escribir como un conjunto de subprogramas, llamados
"módulos". En nuestro caso, éste es un programa tan sencillo
que está formado por un único módulo, llamado "vacio"
(el fichero se llamaría VACIO.MOD).
-
La segunda diferencia es que después de la palabra "END" que marca
el fin del programa (del módulo, para ser más exactos) se
repite nuevamente el nombre de dicho módulo, buscando una
mayor legibilidad.
-
La tercera diferencia es que Modula-2 sí distingue entre mayúsculas
y minúsculas. Y las palabras reservadas están definidas en
mayúsculas, de modo que debemos escribir "BEGIN", y no "begin" ni
"Begin".
3-Escribir en pantalla
Compliquemos la cosa. Ahora vamos a escribir el texto "Hola" en pantalla:
MODULE saludo;
FROM InOut IMPORT WriteString;
BEGIN
WriteString('Hola');
END saludo.
Aquí ya hay más cosas nuevas.
-
La orden encargada de escribir una cadena de texto en pantalla es "WriteString".
Ésta no es una orden estándar del lenguaje Modula-2 (aunque
es de suponer que cualquier compilador la incluirá), sino que se
trata de una "ampliación" al lenguaje estándar. Esta ampliación
se encuentra en un módulo adicional llamado "InOut". Por eso le
decimos que queremos importar la orden "WriteString" del módulo
InOut: FROM InOut IMPORT WriteString.
-
La orden "WriteString" en sí no tiene mayor complicación:
escribe una cadena de texto en pantalla, y el cursor se queda a continuación
de dicha cadena de texto. Eso sí, el texto a escribir se puede indicar
entre comillas simples o dobles: también podríamos haber
escrito: WriteString("Hola").
Más adelante veremos las demás órdenes de escritura
que tenemos a nuestra disposición, pero antes vamos a resumir los
tipos de datos que podemos manejar.
4-Tipos de datos
existentes
Los tipos de datos simples que existen en Modula-2 son casi idénticos
a los de Pascal:
-
INTEGER, número entero (sin decimales), con un valor de -32.768
a 32.767.
-
CARDINAL, número entero, de 0 a 65.535.
-
REAL, número real (con o sin decimales).
-
BOOLEAN, valor booleano (TRUE o FALSE).
-
CHAR, un carácter (letra, cifra, signo de puntuación y otros
símbolos).
(Nota: puede existir compiladores en los que un número "INTEGER"
no sea un valor de 16 bits, sino de 32 bits o incluso más, de modo
que podría tomar un rango de valores mucho mayor).
Al igual que en Pascal, se pueden agrupar varios datos del mismo tipo
para formar un ARRAY. Los arrays de una dimesión se declaran
igual que en Pascal, y los de dos dimensiones se pueden declarar de cualquiera
de esas dos formas:
VAR
dato1: ARRAY[1..8] OF ARRAY[1..8] OF CARDINAL;
dato2: ARRAY[1..8],[1..8] OF CARDINAL;
También se pueden agrupar varios datos de distinto tipo formando
un registro o RECORD. Los registros se declaran y se utilizan igual
que en Pascal, tanto los registros normales como los registros variantes
(bueno, aquí habria que mencionar un pequeño matiz, pero
no vamos a hilar tan fino...).
Los otros tipos de variables que son "casi exclusivos" de Pascal,
como datos enumerados, subrangos y conjuntos, existen también en
Modula-2 y se manejan básicamente igual.
5-Bucles
En Modula-2 tenemos disponibles los mismos tipos de construcciones repetitivas
que en Pascal: los bucles WHILE, REPEAT y FOR (además de otro que
veremos). Eso sí, hay una pequeña diferencia: en Pascal se
presupone que después de FOR o de WHILE habrá una única
orden, y si queremos que se repita un bloque formado por varias órdenes
habrá que delimitarlas entre "begin" y "end"; en Modula-2 no es
así: se espera un bloque de varias órdenes, que deberá
terminar con END.
Para que sea vea mejor, vamos con un ejemplo. Nos limitaremos
a contar del 1 al 10, primero con FOR, después con WHILE, con REPEAT,
y finalmente con una instrucción que no existe en Pascal: LOOP,
que repite indefinidamente una parte del programa:
MODULE bucles;
FROM InOut IMPORT WriteString, WriteCard, WriteLn;
VAR
i: CARDINAL;
BEGIN
(* Primero contamos de 1 a 10 con FOR *)
FOR i := 1 TO 10 DO
WriteCard(i, 4);
END;
WriteLn;
(* Ahora lo hacemos con WHILE *)
i := 1;
WHILE i <= 10 DO
WriteCard(i, 4);
INC(i);
END;
WriteLn;
(* Y ahora con REPEAT *)
i := 1;
REPEAT
WriteCard(i, 4);
INC(i);
UNTIL i>10;
WriteLn;
(* Y finalmente con LOOP *)
i := 1;
LOOP
WriteCard(i, 4);
INC(i);
IF i>10 THEN
EXIT;
END;
END;
WriteString('Terminado');
WriteLn;
END bucles.
Hay algunas novedades que comentar:
-
Ahora importamos más órdenes de escritura: WriteString
(para escribir una cadena de texto, ya la hemos visto), WriteCard (para
escribir un número entero no negativo -CARDINAL-) y WriteLn (para
avanzar el cursor a la línea siguiente).
-
Los comentarios se crean igual que en Pascal: comienzan con (* y
terminan con *), y pueden abarcar varias líneas. Eso sí,
no se pueden delimitar entre llaves ({ y }), cosa que en Pascal sí
se puede hacer.
-
Como ya he comentado, después de FOR irá un conjunto
de líneas, terminadas con END. Esto difiere de Pascal, lenguaje
en el que después de "For" se indica una única orden, y si
queremos que sean varias se deben delimitar entre "Begin" y "End".
-
Lo mismo ocurre con WHILE: deberá seguirle un conjunto de
líneas, terminadas con END, cosa que no sucede en Pascal.
-
WriteCard escribe un CARDINAL en pantalla, y se le indica también
la anchura con la que queremos que lo muestre (en nuestro caso, con 4 espacios).
Si el número ocupa menos de esa anchura, se rellenará con
espacios en blanco por la izquierda; si ocupa más de esa anchura,
ocupará más de lo que le hemos pedido, pero no se trunca
el número (por ejemplo, si escribimos "WriteCard(12345,3)" el ordenador
escribirá 12345 con sus 5 cifras, no se limitará a las 3
cifras que le hemos pedido).
-
INC incrementa el valor de una variable, al igual que en Pascal;
se puede usar como en este ejemplo, para que salte de 1 en 1, o bien especificar
qué salto queremos: "INC(a,3)" aumentaría el valor de la
variable "a" en 3 unidades. Análogamente, DEC decrementaría
el valor de una variable.
-
WriteLn escribe un salto de línea en pantalla, es decir,
avanza el cursor a la línea siguiente. Este uso es distinto del
de Pascal, porque en Pascal escribe el texto que le indiquemos y finalmente
avanza una línea, mientras que en Modula-2 no escribe ningún
texto, sólo avanza de línea: no es válido escribir
WriteLn(‘Hola’);
-
REPEAT es básicamente igual que en Pascal, como se ve en
el ejemplo.
-
LOOP repite indefinidamente una parte del programa, que debe terminar
con END. Si queremos abandonar la parte repetitiva del programa, la única
forma será emplear EXIT, como se ve en el ejemplo. En nuestro caso,
la abandonamos cuando el contador supera el valor 10, condición
que comprobamos con IF. La sintaxis de IF es muy parecida a la de Pascal,
pero la veremos con detalle en el próximo apartado.
-
Un último comentario: FOR permite contar con incrementos
distintos de 1. Por ejemplo, si queremos contar de 3 en 3, usaríamos
algo como "FOR i := 1 TO 30 BY 3 DO ..." y si queremos contar "hacia atrás",
sería "FOR i := 1 TO 30 BY -1 DO ..."
6-Condiciones
En Modula-2 existen las dos mismas construcciones que en Pascal para comprobar
condiciones: IF para comprobar una condición (o unas pocas)
y CASE para los casos de selección múltiple.
Veamos primero un ejemplo del uso de IF en Modula-2:
MODULE if1;
FROM InOut IMPORT WriteString;
VAR
i: CARDINAL;
BEGIN
i := 7;
IF i=5 THEN
WriteString('La variable i vale 5');
ELSIF i=3 THEN
WriteString('La variable i vale 3');
ELSE
WriteString('La variable i no vale 5 ni 3');
END;
END if1.
Como siempre, vamos a comentar cosas:
-
Después de IF se indica la condición a comprobar, con la
misma sintaxis que en Pascal, seguida por la palabra THEN y por
los pasos a dar en caso de que se cumpla esa condición. Estos pasos
serán varias órdenes, terminadas con END, al igual que ocurría
en el caso de FOR y WHILE, y al contrario que en Pascal, en el que se esperaba
una sola orden y debíamos emplear "begin" y "end" si se trataba
de varias órdenes.
-
Los pasos a dar si no se cumple la condición se indican con ELSE,
al igual que en Pascal (aunque se esperarán varias órdenes
en vez de una sóla). Eso sí, si después de ELSE vamos
a comprobar otra nueva condición , se puede utilizar ELSIF, como
en el ejemplo.
Un ejemplo de CASE podría ser:
MODULE case1;
FROM InOut IMPORT WriteString;
VAR
i: CARDINAL;
BEGIN
i := 7;
CASE i OF
1,2: WriteString('La variable i vale 1 o 2');
|
3: WriteString('La variable i vale 3');
|
4..8: WriteString('La variable i vale entre
4 y 8');
ELSE
WriteString('La variable no está entre
1 y 8');
END;
END case1.
Como se ve, el uso de CASE es muy parecido al de Pascal, con una única
diferencia: para terminar cada una de las posibles opciones se emplea el
símbolo | (la barra vertical, que en los teclados españoles
está como tercer símbolo en la tecla del 1).
7-Entrada/Salida
básica
Hemos visto algunas de las órdenes que más se emplean para
escribir en pantalla, como WriteString, WriteCard o WriteLn. Vamos
a resumir el uso de otras pocas (nota: se trata de órdenes que podrían
no estar disponibles en todos los compiladores):
-
Write( letra ); Escribe una letra (char).
-
WriteLn; Avanza el cursor a la siguiente línea.
-
WriteString( texto ); Escribe una cadena de texto.
-
WriteLine( texto ); Escribe una cadena de texto y avanza
una línea.
-
WriteCard( numero, ancho); Escribe un CARDINAL con un
ancho mínimo indicado, justificado a la derecha.
-
WriteInt( numero, ancho); Escribe un INTEGER con un ancho
mínimo indicado, justificado a la derecha.
-
WriteHex( numero, ancho); Escribe un CARDINAL en hexadecimal.
-
WriteOct( numero, ancho); Escribe un CARDINAL en octal.
Análogamente, tenemos una serie de órdenes de entrada,
como:
-
Read( letra ); Lee una sola letra.
-
ReadString( texto ); Lee una cadena de texto, terminada
con un espacio o un carácter de control.
-
ReadLine( texto ); Lee una cadena de texto, terminada
con un avance de línea (una pulsación de la tecla Intro).
-
ReadCard( numero ); Lee un número CARDINAL.
-
ReadInt( numero ); Lee un numero INTEGER.
8-Procedimientos
y funciones
Vamos a tratar cómo se declaran procedimientos y funciones en Modula-2.
Como siempre, veamos primero un ejemplo:
MODULE proc1;
FROM InOut IMPORT WriteString, WriteInt, WriteLn;
PROCEDURE Saludo;
BEGIN
WriteString('Hola');
WriteLn;
WriteString('Esta es una prueba de definicion
de procedimientos');
WriteLn;
END Saludo;
PROCEDURE Suma4 (num1, num2, num3, num4: INTEGER) : INTEGER;
BEGIN
RETURN num1+num2+num3+num4;
END Suma4;
BEGIN
Saludo;
WriteString('La suma de los numeros esocogidos es ');
WriteInt(
Suma4 ( 5, 7, 12, -3 )
, 2);
WriteLn;
END proc1.
-
Los procedimientos no revisten mayor dificultad para quien ya los
ha programado en Pascal: la única diferencia es que (como viene
siendo habitual) la palabra PROCEDURE debe escribirse en mayúsculas
y que después de END debe repetirse el nombre del procedimiento
que termina.
-
Si el procedimiento fuese recursivo, no habría ninguna diferencia
con cómo se hace en Pascal.
-
Si quisiéramos pasar parámetros, tampoco existiría
diferencia, sean estos parámetros por valor o por referencia.
-
Con las funciones sí cambia la cosa: no se usa la palabra
"Function", sino que es un PROCEDURE que devuelve un valor. La sintaxis
recuerda mucho a la de Pascal, pero cambiando la palabra FUNCTION por PROCEDURE.
La forma de devolver el valor final también es distinta: se indica
con la palabra RETURN.
-
El resto del programa no debería revestir mayor dificultad... espero.
9-Ficheros
No voy a entrar con detalle en el manejo de ficheros, pero sí daré
las pautas básicas para quien quiera investigar:
-
Las rutinas de manejo de fichero están en el módulo
FileSystem, de modo que nuestro programa debería incluir algo como
"FROM FileSystem IMPORT File, LookUp, Close, WriteChar;"
-
El tipo de datos asociado a un fichero es File.
-
La orden que trata de abrir un fichero es LookUp, que espera tres
parámetros: una variable de tipo File (el fichero que intentamos
abrir), un String (el nombre físico de ese fichero) y un booleano
(que será TRUE si queremos que se cree un fichero nuevo en caso
de no existir el que buscamos o FALSE si queremos que se devuelva un código
de error).
-
Si queremos comprobar si ha habido un error al abrir el fichero
con ese nombre, podemos hacer "IF fichero.res = done THEN...", donde "res"
es la abreviatura de resultado, y "done" indicaría que todo ha sido
correcto.
-
La orden para cerrar un fichero es Close.
-
La orden que lee un carácter del fichero es ReadChar y la
que lo escribe es WriteChar.
-
Existen otras muchas órdenes, como ReadNBytes (leer varios
bytes), WriteNBytes (escribir varios bytes), SetLPos (para saltar a una
posición, dada por una "cardinal largo" LONGCARD), GetLPos (para
hallar la posición actuar), LLength (para hallar la longitud del
fichero), etc. Puede que no todas existan para todos los compiladores,
así que se debería mirar la referencia del compilador que
se esté empleando. Si se trata de FST 3.1 (Fitter Software Tools
Modula-2 compiler, version 3.1), que es el más difundido, se puede
mirar la definición del módulo FileSystem, echando un vistazo
al fichero FILESYST.DEF, en la carpeta LIB.
10-Punteros y memoria
dinámica
El concepto de memoria dinámica y de punteros es el mismo que para
Pascal. En algún compilador puede que existan NEW y DISPOSE; pero
es preferible utilizar las órdenes ALLOCATE (para reservar
memoria) y DEALLOCATE (para liberarla).
MODULE pointer1;
FROM InOut IMPORT WriteString, WriteCard, WriteLn;
FROM Storage IMPORT ALLOCATE, DEALLOCATE;
FROM SYSTEM IMPORT TSIZE;
TYPE
texto = ARRAY[0..20] OF CHAR; (* un string hecho "a
mano" *)
VAR
unTexto: POINTER TO texto; (* puntero
a string *)
unNumero: POINTER TO CARDINAL; (* puntero a Cardinal *)
BEGIN
(* Reservamos el espacio para cada dato *)
ALLOCATE(unNumero, TSIZE( INTEGER ));
ALLOCATE(unTexto, TSIZE( texto ));
(* Asignamos valores *)
unNumero^ := 27;
unTexto^ := "Prueba de texto";
WriteString("El texto es ");
WriteString( unTexto^ );
WriteString(" y el numero es");
WriteCard( unNumero^,3 );
WriteLn;
(* Liberamos el espacio reservado *)
DEALLOCATE(unNumero, TSIZE( INTEGER ));
DEALLOCATE(unTexto, TSIZE( texto ));
END pointer1.
Creo que no es difícil para quien ya lo haya visto en Pascal:
-
Los punteros a un cierto tipo se definen con "POINTER TO TipoBase".
-
Con ALLOCATE reservamos el espacio que requerirá cada variable
dinámica. ALLOCATE lo importamos del módulo "Storage".
-
Debemos saber cuánto espacio es el que ocupa en memoria cada
tipo de datos, para decírselo a ALLOCATE. Esta información
nos la da TSIZE (el equivalente al "sizeof" de Pascal), que importamos
de SYSTEM.
-
Asignamos los valores (o los leemos) con la misma noación
que en Pascal: unNumero^ = 27;
-
Finalmente, liberamos el espacio que habíamos reservado.
Para ello utilizamos DEALLOCATE.
11-Módulos
La característica fundamental de Modula-2, la que le da su nombre,
y la que más lo distingue del Pascal estándar, es la posibilidad
de crear programas modulares, formado por distintos módulos
que se relacionan.
Para quien haya usado alguna versión de Pascal moderna, esto
ya le sonará a conocido: son las "units". Eso sí,
la definición en Modula-2 es ligeramente distinta: en Pascal, la
"interface" y la "implementation" de una "unit" van en el mismo fichero,
pero en Modula-2 no es así: un fichero contendrá la definición
del módulo y otro los detalles de la implementación.
Como ejemplo, vamos a crear un módulo que agrupe distintas
funciones para calcular áreas. La parte de definición (fichero
SUPERF.DEF) podría ser así:
DEFINITION MODULE Superf;
EXPORT QUALIFIED AreaCuadrado, AreaRectangulo, AreacCirculo;
PROCEDURE AreaCuadrado(lado: REAL) : REAL;
PROCEDURE AreaRectangulo(ladoA, ladoB: REAL) : REAL;
PROCEDURE AreaCirculo(radio: REAL) : REAL;
END Superf.
Sencillo, ¿verdad? Comienza con DEFINITION MODULE y el
nombre, indicamos qué "procedures" deseamos exportar, y luego la
"cabecera" (o el prototipo) de cada uno de esos "procedures".
La parte de definición podría ser como ésta:
IMPLEMENTATION MODULE Superf;
CONST
PI = 3.141592;
PROCEDURE AreaCuadrado(lado: REAL) : REAL;
BEGIN
RETURN( lado*lado );
END AreaCuadrado;
PROCEDURE AreaRectangulo(ladoA, ladoB: REAL) : REAL;
BEGIN
RETURN( ladoA*ladoB );
END AreaRectangulo;
PROCEDURE AreaCirculo(radio: REAL) : REAL;
BEGIN
RETURN( radio*radio*PI );
END AreaCirculo;
END Superf.
También es sencillo: comienza con IMPLEMENTATION MODULE, y detalla
lo que hacen los distintos procedures. Puede existir algún "procedure"
que no se exporte y que esté para ser utilizado por algún
otro que sí se exporte; en nuestro caso, lo que estamos utilizando
y que no se exporta es la constante PI.
Finalmente, un programita que utilizase los servicios que nos
proporciona este módulo no sería distinto de los que ya hemos
visto:
MODULE UsaMod; (* Usa el módulo de superficies
*)
FROM Superf IMPORT AreaCuadrado,
AreaRectangulo, AreaCirculo;
FROM InOut IMPORT WriteString, WriteInt, WriteLn;
BEGIN
WriteString("El area de un cuadrado de lado 3 es ");
WriteInt( TRUNC( AreaCuadrado( 3.0 )), 3);
WriteLn;
WriteString("El area de un rectangulo de lados 3 y 4 es ");
WriteInt( TRUNC( AreaRectangulo( 3.0, 4.0 )), 3);
WriteLn;
WriteString("El area de un circulo de radio 3 (redondeada)
es ");
WriteInt( TRUNC( AreaCirculo( 3.0 )), 3);
WriteLn;
END UsaMod.
La única "novedad" es algo que ya conocerá quien venga
de Pascal: he utilizado la función TRUNC para convertir un número
real (las áreas calculadas) en uno entero (que mostraré con
WriteInt), despreciando sus decimales.
¿Es que no se pueden escribir directamente números reales?
Sí, por ejemplo tenemos la orden "WriteReal", del módulo
"RealInOut", que los muestra en formato científico, pero como los
números en formato científico se ven tan feos... Podríamos
haber convertido a una representación más bonita con "RealToString"
(en el módulo "RealConversions"), y escribir el String resultante,
pero eso lo dejo como "trabajo voluntario" para quien quiera profundizar
más...
Nota: puede ser muy interesante echar un vistazo a las definiciones
de los módulos de librería: los ficheros con extensión
DEF, que se encuentran en la carpeta LIB del compilador. Por ejemplo, en
FST 3.1 hay procedimientos para crear menús (menu.def), para ver
el estado del teclado (incluyendo las teclas de función de un PC,
en keyboard.def), para manejo de directorios (director.def), para crear
ventanas en modo texto (windows.def), etc.
12-Otras posibilidades
Hemos visto casi todas las posibilidades estándar del lenguaje
Modula-2.
Existen algunas características estándar que no veremos,
por el carácter introductorio de estas lecciones. es el caso de
la concurrencia (multitarea entre distintas partes de un programa).
De igual modo, tampoco veremos características que dependan claramente
del compilador, como puede ser el manejo de gráficos.
Aun así, confío en que esta introducción haya sido
útil para quien tenga que "pelearse" con Modula-2, y que le haya
dado un buen punto de partida desde el que seguir investigando.
(Texto creado por Nacho Cabanes, Dic. 1999)