Esta es una guía para editar y ejecutar Standard ML (SML) usando el sistema Standard ML of New Jersey (SML/NJ). Este documento es una traducción con adaptaciones (hecha por C.G.) del original en inglés escrito para Carnegie Mellon University por Peter Lee con extensas contribuciones de Robert Harper, Iliano Cervesato, Carsten Shurmann, Frank Pfenning y Herb Derby. (Para mayores detalles ver el original en Using the SML/NJ System ).
Este no es un manual de referencia para el lenguaje SML. Si usted necesita un manual de referencia o un tutorial, puede encontrar varias fuentes de información, ya sea en línea o impresas, en Introduction . Existe una copia en local en postcript de una breve introducción (102 páginas) por Robert Harper en Introduction to Standard ML .
Cuando usted comienza la sesión con el sistema SML/NJ, carga y responde un mensaje que da el número de versión actual, y luego el prompt para la entrada del usuario. El prompt es un simple guión: ("-").
Entonces, usted puede tipear una
declaración top-level .
Hay diversas clases de declaraciones top-level en SML.
Por ejemplo, la siguiente es una declaración de una función
llamada
inc
que incrementa su argumento entero. (En estos ejemplos, el guión
("-") es el prompt de SML/NJ,
y tipo
letra de maquina
es la entrada del usuario.
En algunos browsers, la entrada del usuario aparecerá también
en texto azul. El tipo
italic se usará para la respuesta del sistema SML/NJ.
El símbolo
representa el retorno de carro en los sistemas UNIX
o la tecla Enter en los sistemas PC y Macintosh.)
-fun inc x = x + 1;
val inc = fn : int -> int
El texto "fun inc x = x + 1
"
es la declaración para la función
inc
El punto y coma (";
")
es un marcador que indica al sistema SML/NJ que debe efectual las
siguientes acciones:
elaborar (esto es, efectuar chequeo de tipos y otros análisis
estáticos,
compilar (obtener código de máquina ejecutable)
ejecutar , y finalmente imprimir
el resultado de esta declarción.
Después de todo esto, el sistema está listo para una
nueva entrada y todo el proceso comienza de nuevo.
Este es el llamado "bucle top-level".
Para salir del sistema SML/NJ simplemente tipee un carácter
de fin de archivo (Control-d) en el prompt.
En el ejemplo antes expuesto, el resultado impreso muestra que
es una función que toma un argumento entero y produce un
resultado entero. De hecho, es importante saber que en SML las
funciones son valores de "primera clase",
es decir, esencialmente no diferentes de valores como enteros.
Luego, para ser más precisos, es mejor decir que el
identificador
inc
ha sido ligado
al valor (que resulta ser una función, como lo indica la
palabra reservada fn) de tipo int -> int.
Si hubiésemos olvidado el punto y coma, entonces la
elaboración, compilación, ejecución e impresión
habrín sido diferidos y habríamos recibido un prompt
(esta vez, un signo igual "=")
para, ya sea la continuación de la declaración de
inc
, u otra declaración
top-level.
Cuando finalmente es ingresado un punto y coma
(quizás después de muchas declaraciones top-level),
todas las declaraciones desde el último punto y coma serán
procesadas secuencialmente.
Por ejemplo:
-fun inc x = x + 1
=
fun f n = (inc n) * 5;
val inc = fn : int -> int val f = fn : int -> int
En este ejemplo, definimos la función
inc
así como también la función
f
que usa inc
.
En el bucle interactivo top-level, la forma más simple
de entrada es una expresión
Por ejemplo, después de tipear las declaraciones para
inc
y
f
arriba,
ahora podemos llamar
f
tipeando:
-f (2+4);
val it = 35 : int
Observe que puesto que no se ha dado ningún identificador
para ligar el valor obtenido,
el sistema interactivo ha elegido el identificador
it
y lo ha ligado al resultado de compilar y ejecutar la expresión
f (2+4)
.
Es posible que Ud. tenga experiencia con otros lenguajes cuya
implementación soporta un bucle interactivo de top-level
similar. Por ejemplo, la mayoría de las implementaciones de
los lenguajes Lisp, Scheme y Basic soportan bucles top-level.
Si usted tiene experiencia con cualesquiera de estos lenguajes,
entonces esperaría que la redefinición de una función
cambie el enlace (binding) del nombre de la función asi como
de todas las funciones que llaman a esa función.
Sin embargo, en el sistema SML/NJ este no es el caso.
Por ejemplo, supóngase que queremos cambiar la definición
de la función
inc
de tal forma que
incremente por dos en vez de uno:
-fun inc x = x + 2;
val inc = fn : int -> int
En sistemas Lisp y Scheme típicos, tal redefinición
causaría que la función
f
cambiase también
puesto que f
llama a inc
.
Pero en el sistema
SML/NJ, el enlace de f
no cambia, luego de hecho, referirse a
f
ahora aún da la función original:
-f (2+4);
val it = 35 : int
Para entender por que el sistema SML/NJ se comporta de esta manera,
considere que hubiese ocurrido si redefinimos
inc
de tal forma que tuviese un tipo distinto de
int -> int, por ejemplo:
-fun inc x = (x mod 2 = 0);
val inc = fn : int -> bool
Aquí, inc
fué
cambiado a una función que retorna
true
si y solo si su argumento entero es par. Ahora bien, si
quisiéramos cambiar
f
para reflejar esta redefinición (como ocurriría en los
sistemas Lisp o Scheme), no pasaría el chequeo de tipos.
Esto no es necesariamente algo malo, pero en todo caso el sistema
SML/NJ no se molesta en volver a declaraciones top-level anteriores
y reelaborarlas; luego el enlace de
f
permanece inalterado.
Si usted esta familiarizado con el lenguaje SML, entonces puede pensar en la sucesión de declaraciones top-level ingresadas a un bucle interactivo top-level SML/NJ como un grupo de enlaces let anidados:
let fun inc x = x + 1 in let fun f n = (inc n) * 5 in let fun inc x = x + 2 in
...
[ Vuelta a la tabla de Contenidos ]
En vez de escribir su programa en el top-level interactivo,
es más productivo poner su programa en un archivo
(o conjunto de archivos) y después cargarlo(s) en el
sistema SML/NJ. La manera más simple de hacer esto es usar
la función primitiva del sistema
use
. For example:
-use "myprog.sml";
[opening myprog.sml] ... val it = () : unit
La función use
toma el nombre del archivo (de tipo string) para cargarlo.
Si el archivo existe, es abierto y leído, con cada declaración
top-level en el archivo procesada en orden de aparición
(y los resultados impresos en la salida estandar).
El "resultado" de la función use
es el valor unitario
("()
").
A medida que sus programas crezcan y el código comienze a
estar diseminado en varios modules, se hace más difícil
recordar exactamente el orden correcto en el cual usar
use
sobre los archivos.
Para aliviar este problema, el sistema SML/NJ tiene un Administrador
de Compilación (Compilation Manager, CM) que es altamente
recomendado usar. (De hecho, usted pudiera hacer correr el sistema
SML/NJ invocando el binario
"sml-cm",
en vez de simplemente "sml".)
El CM es un sistema complejo cuya documentación está
disponible en línea en
http://www.cs.princeton.edu/~blume/cm-manual.ps.
Para la mayoría de los usos la interface más simple
es suficiente: simplemente cree un archivo en el directorio en que trabaja
llamado
sources.cm
que contiene los nombre de todos sus archivos fuentes SML
listados uno por línea en cualquier orden. Una vez que este
archivo está creado, entonces usted puede usar la función
CM.make
para cargar, compilar, y ejecutar su sistema. Por ejemplo, suponga
que tenemos tres archivos fuentes,
a.sig
,
b.sml
, y c.sml
.
Entonces usted puede crear un archivo llamado
sources.cm
con los siguientes contenidos:
Group is a.sig b.sml c.sml
Observe que no importa el orden en que estén los nombres de los archivos. Una vez que este archivo ha sido creado, escribir lo siguiente al sistema SML/NJ hará todo lo que sea necesario para cargar su programa:
-CM.make();
La función CM.make
escaneará todos sus archivos fuentes y calculará las
dependencias entre ellos de tal forma de compilar y cargarlos en
el orden adecuado.
Si CM.make
ya ha sido usado antes para compilar y cargar su programa,
entonces inspecciona cuales arcivos han sido cambiados desde
el último "make",
y entonces carga y compila el número minimal de archivos
necesario para poner el sistema al día.
Después de correr
CM.make
,
usted probablemente notará un nuevo directorio en el
directorio que contiene sus archivos fuentes. Este nuevo directorio
es usado por CM para
"recordar" los resultados del cálculo de dependencias,
asi como para guardar los resultados de la compilación de sus
archivos de tal forma de que no tengan que ser compilados de nuevo
(a menos, por supuesto, que hayan sido cambiados).
Hay una extenso conjunto de valores y funciones predefinidos en el sistema SML/NJ. Esto se conoce como la standard basis , o a veces como el pervasive environment . Similarmente a CM, existe también una extensa documentación accesible en línea en para la standard basis . Para trabajar con archivos, la siguiente funció es útil:
OS.FileSys.chDir : string -> unit
Esta función implementa el comando Unix estandar "cd" que cambia el directorio de trabajo actual por el directorio especificado por el argumento del string. Esto es útil si usted comenzó el sistema SML/NJ en un directorio diferente del que contiene sus archivos fuentes.
Otro conjunto de funciones bases son útiles para controlar la salida producida por el sistema SML/NJ:
Compiler.Control.Print.printDepth : int ref Compiler.Control.Print.printLength : int ref
Estas variables controlan la profundidad y largo máxima
en las cuales las listas, tuplas y otras estructuras de datos serán
impresas. Cuando una estructura de datos es más profunda que
printDepth
o más larga que
printLength
,
la porción restante de la estructura es impresa como
tres puntos suspensivos
("...
").
Para cambiar el valor de una de estas variables, puede usarse una asignación. Por ejemplo:
-Compiler.Control.Print.printDepth := 10;
cambia la profundidad máxima de impresión a diez.
La base estándar contiene muchos módulos y funciones para manipular valores de todos los tipos básicos, incluyendo booleanos, enteros, reales, caracteres, strings, arrays, y listas. Desafortunadamente, el sistema SML/NJ no provee ninguna forma de "browsear", luego, o usted deberá referirse a , o "e;hackear"e; un poco para ver el conjunto completo de funciones bases que actualmente provee el sistema SML/NJ para esos tipos. Por ejemplo tipee lo siguiente en el top-level interactivo:
-signature S = INTEGER;
Cada conjunto de funciones bases estándar es encapsulada
en un módulo SMOL, y cada tal módulo tiene una signatura,
o "interfaz", cuyo nombre está escrito enteramente
en letras mayúsculas y se refiere al tipo de valores para los
cuales el módulo provee la funcionalidad.
(Note the SML distingue entre minúsculas y mayúsculas).
Para las funciones enteras, la signatura se llama
INTEGER
.
Luego, la declaración anterior sólo liga el identificador
S
a la signatura
INTEGER
, which causes the
SML/NJ system to respond with a listing of the entire INTEGER
interface.
(Podríamos haber usado cualquier nombre aparte de
(We
S.)
Otras signaturas útiles incluyen
BOOL
, REAL
, CHAR
, STRING
, ARRAY
, and LIST
.
Para funciones que interfacean el sistema operativo
(tales como OS.FileSys.chDir
above), véase la signatura OS
(and POSIX
, si su sistema la tiene).
Hay muchas otros módulos útiles en la base estándar.
[ Vuelta a la tabla de Contenidos ]
Al igual que la mayoría de los compiladores, el sistema SML/NJ a menudo produce mensajes de error que son difíciles de descifrar. El problema se complica por el hecho de que SML soporta inferencia de tipos polimórficos, lo que hace muy difícil para el compilador determinar precisamente cual es la fuente real de un error de tipo. Por otra parte, una vez que todos los errores de tiempo de compilación han sido removidos, a menudo esto significa que el grueso de los bugs han sido eliminados. En la práctica, los programas SML corren una vez que todos los errores de tipo reportados por el compilador han sido removidos!
El error más común es la incompatibilidad de tipos.
Por ejemplo, suponga que tenemos el siguiente código en un
archivo llamado
myprog.sml
:
fun inc x = x + 1 fun f n = inc true
Observe que el punto y coma no se necesita aquí, puesto que el marcado de fin de archivo servirá para ese propósito. Ahora bien, si cargamos este archivo, obtendremos el siguiente mensaje de error:
use "myprog.sml";
myprog.sml:2.11-2.18 Error: operator and operand don't agree (tycon mismatch) operator domain: int operand: bool in expression: inc true
El mensaje de error indica que la expresión inc true
, en la línea 2,
entre las columnas 11 y 18, es culpable de la incompatibilidad de tipos.
La función
inc
está siendo aplicada
a un argumento de tipo
bool
en esta expresión, pero su dominio (tipo de su argumento) es
int
.
Algunos de los operadores aritméticos, como
+
,
*
, -
,
=
, etc., están
"sobrecargados" (overloaded), en el sentido de que pueden
ser usados ya sea por argumentos enteros o reales. Esta funcionalidad de
sobrecargamiento lleva a posibles fuentes de confusión para el
programador novato de SML. Considere por ejemplo, la siguiente
declaración de una función que eleva números al
cuadrado:
fun square x = x * x
El siguiente mensaje de error se obtiene al compilar el programa:
myprog.sml:1.18 Error: overloaded variable not defined at type symbol: * type: 'Z
Debido a que no existe suficiente información en el programa
para determinar si
*
es para enteros o para reales, se genera un mensaje de error que
indica que hay que "resolver" el sobrecargamiento.
La sencilla forma de arreglar este error es simplemente declarar el tipo de uno de los argumentos (o el resultado) de la operación aritméca. Por ejemplo, aquí hay tres versiones que funcionan:
fun square' x = x * x : int fun square'' (x : int) = x * x fun square''' x : int = x * x
La primera versión declara explícitamente el tipo
del segundo argumento del operador
*
La segunda versión declara el tipo del argumento.
Finalmente, la tercera versión declara el tipo del resultado
de la función
square'''
.
Las tres versiones permiten al mecanismo de inferencia de tipos de
SML inferir los tipos de los identificadores en las declaraciones.
No es inusual ocupar largo tiempo buscando la fuente
de un error de tipo. (De hecho, el tiempo ocupado haciendo esto
es casi siempre mucho menos que el tiempo que toma buscar el
mismo error sin el beneficio del chequeo de tipos estático!)
Una forma común de cercar y achicar las posibilidades,
y también de mejorar la precisión del mensaje de
error producido por el compilador, es anotar el programa con
tipos explícitos, en la forma que lo hicimos más arriba.
Es particularmente útil anotar los tipos de los parámetros
de funciones, como lo hicimos con
square''
.
Es es similar a la declaración de los tipos de los parámetros
en lenguajes como C y Pascal. Por supuesto, en esos lenguajes
las declaraciones son requeridas; en SML ellas son opcionales.
Una de los cambios más fundamentales en la revisión de 1997 del lenguaje SML es que ahora algo llamado restricción de valores (value restriction). Esencialmente, esto restringe el polimorfismo a expresiones que claramente son valores, específicamente identificadores individuales y funciones. Cuando esta restricción se viola, se produce el mensaje de error "nongeneric type variable,". Por ejemplo, el siguiente programa produce este error:
fun id x = x fun map f nil = nil | map f (h::t) = (f h) :: (map f t) val f = map id
El mensaje que se obtiene es
myprog.sml:6.1-6.14 Error: nongeneralizable type variable f : 'Y list -> 'Y list
que indica que la expresión
map id
es polimórfica, pero no sintácticamente un valor
(esto es, no un identificador o una expresión lambda),
y por lo tanto, el intento de usarla como un valor polimórfico
(a través de ligar f
to it) viola la restricción
de valores. Las razones para esta restricción están más
allá del alcance de este documento, pero están explicadas
en diversos artículos así como en el libro de Paulson.
Debido a que la sintaxis de SML es algo compleja, hay varios errores comunes que los novatos de SML tienden a producir. Uno de los más comunes tiene que ver con la sintaxis de patrones en declaraciones de funciones en forma clausal (clausal-form function) y en expresiones de casos (case expressions). Considere el siguiente código:
datatype 'a btree = Leaf of 'a | Node of 'a btree * 'a btree
fun preorder Leaf(v) = [v] | preorder Node(l,r) = preorder l @ preorder r
El sistema SML/NJ reclama enérgicamente ante esto:
myprog.sml:4.5-5.48 Error: data constructor Leaf used without argument in pattern myprog.sml:4.5-5.48 Error: data constructor Node used without argument in pattern myprog.sml:4.1-5.48 Error: pattern and expression in val rec dec don't agree (tycon mismatch) pattern: 'Z -> ('Z * 'Z) list expression: 'Z -> 'Z * 'Z -> ('Z * 'Z) list in declaration: preorder = (fn arg => (fn <pat> => <exp>))
El problema aquí es que Leaf y Node son patrones que están sintácticamente separados de los patrones (v) y (l,r) respectivamente. La (extraña) sintaxis de SML requiere paréntesis extra:
fun preorder (Leaf v) = [v] | preorder (Node(l,r)) = preorder l @ preorder r
Esto es así en todos los contextos donde se usen patrones, incluyendo declaraciones de funciones en forma clausal, expresiones de casos y manejadores de excepciones.
Otro aspecto algo confuso de la sintaxis tiene que ver con la interacción entre expresiones de casos, manejadores de excepciones y declaraciones de funciones en forma clausal. Considere la siguiente función tomada en forma levemente modificada de la biblioteca de SML/NJ (que se describe más adelante):
datatype 'a option = NONE | SOME of 'a
fun filter pred l = let fun filterP (x::r, l) = case (pred x) of SOME y => filterP(r, y::l) | NONE => filterP(r, l) | filterP ([], l) = rev l in filterP (l, []) end
En este ejemplo, la función local
filterP
se define en dos cláusulas, la primera que maneja el caso
de un argumento de lista no vacía, y la segunda que maneja
el caso de una lista vacía.
En la primera cláusula, una expresión de caso es usada.
La ambiguedad sintáctica surge del hecho que se necesita mirar
demasiado adelantado (toma mucho ``lookahead'') para darse cuenta si
la segunda cláusula
filterP
es realmente el tercer brazo de una expresión de casos.
Esto lleva al siguiente y algo críptico mensaje de error:
myprog.sml:8.23-8.28 Error: syntax error: deleting EQUALOP ID myprog.sml:9.3-9.13 Error: syntax error: deleting IN ID
Como antes, algunos paréntesis arreglan el problema:
fun filter pred l = let fun filterP (x::r, l) = (case (pred x) of SOME y => filterP(r, y::l) | NONE => filterP(r, l)) | filterP ([], l) = rev l in filterP (l, []) end
Alternativamente, en este ejemplo podemos también intercambiar
las dos cláusulas de
filterP
:
fun filter pred l = let fun filterP ([], l) = rev l | filterP (x::r, l) = case (pred x) of SOME y => filterP(r, y::l) | NONE => filterP(r, l) in filterP (l, []) end
Como con muchos lenguajes de programación, el consejo básico a seguir es: Si está en duda, use paréntesis.
[ Vuelta a la tabla de Contenidos ]
El lenguaje SML recomienda uso de modularidad, y en el práctica módulos separados tienden a ser ubicados en diferentes archivos. Aunque esto es útil durante el desarrollo, llega ser altamente inconveniente cuando finalmente usted "envía" su programa terminado a sus usuarios. La forma estándar de enviar un programa, entonces, es grabar una imagen del heap del sistema después que todos sus archivos han sido cargados. Esto se llama "exportar" el heap, y produce un único archivo que contiene el estado de su mundo SML al tiempo que hizo la operación de exportación.
Usted puede exportar un heap con la función
exportML
.
Por ejemplo, para grabar la imagen del heap en un archivo llamado
mysml
,
es necesario tipear lo siguiente en el prompt SML/NJ:
-SMLofNJ.exportML "mysml";
Esto grabará el actual estado del sistema SML/NJ en el
archivo
mysml
.
Esto puede ser ejecutado posteriormente corriendo el sistema sml con la
opción de línea de comando,
"@SMLload=mysml
".
Esto relanzará el sistema SML/NJ al mismo punto en el cual
exportML
tomó lugar.
(Nota: exportML
no está
soportado para el Sistema Macintosh versión 7.)
Hay también una función llamada
exportFn
,
que graba un estado SML como una función que toma los
argumentos de línea de comando de la shell cuando es iniciado
de nuevo. La funcionalidad de
exportFn
es
SMLofNJ.exportFn : string * (string * string list -> OS_Process.status) -> unit
El primer argumento es el nombre de un archivo que contiene la
imagen del heap exportado. El segundo argumento es una función que
toma la línea de comando y los argumentos de la línea de
comando (como strings) y retorna un valor de estado de proceso
(process-status) (usualmente
OS_Process.success
o OS_Process.failure
).
[ Vuelta a la tabla de Contenidos ]
Además de la base estándar, el sistema SML/NJ contiene muchas herramientas y bibliotecas. Por ejemplo, los programas lexgen y MLyacc hace generación automática de analizadores léxicos y parsers LALR(1), respectivamente.
Una extensa biblioteca de útiles estructuras de datos y funciones está disponible en http://cm.bell-labs.com/cm/cs/what/smlnj/doc/smlnj-lib/index.html.
Finalmente, extensiones de SML para concurrencia e interacción con el sistema X windows están soportadas por el sistema Concurrent ML y extensiones eXene a SML, disponibles en http://cm.bell-labs.com/cm/cs/who/jhr/sml/eXene/index.html.
[ Vuelta a la tabla de Contenidos ]