Standard ML en Universidad de Chile / SML/NJ

Uso del sistema SML/NJ

  1. Vuelta a la página del Curso CC41A

  2. Interacción con SML/NJ

  3. Uso de archivos y y base estándar.

  4. Significado de algunos mensajes de error más comunes

  5. Exportación de Heaps

  6. Herramientas


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 .


Interacción con SML/NJ

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 ]


Uso de archivos y la Base estándar

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 ]


Mensajes de Error

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!

Incompatibilidades de tipo (Type mistmatches)

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.

Sobrecargamiento no resuelto

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.

La restricción de valores

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.

Errores de Sintaxis

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 ]


Exportación de Heaps

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 ]


Herramientas

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 ]