11 EJEMPLOS DE PROGRAMAS OCAML En esta sección ilustraremos los conceptos presentados hasta ahora, con dos aplicaciones desarrolladas en Ocaml. La primera aplicación es una calculadora implementada como un autómata finito. La segunda aplicación es una versión naive de un manejador de bases de datos. Los ejemplos aproximan problemas comunes de la informática, desde una perspectiva funcional. Se enfatizará el rol de los tipos de datos y las aplicaciones parciales como parte de las soluciones propuestas. ��.� ��� ����������� ���� ������� �� ������� ������� Para confrontar la forma de construir programas en Caml, es necesario desarrollar uno. Hemos elegido como ejemplo el de una calculadora de escritorio, el modelo más sencillo, donde solo podemos teclear números y llevar a cabo las cuatro operaciones aritméticas estándar. Esta calculadora será modelada como un autómata finito. Para comenzar, definimos el tipo tecla para representar las teclas de la calculadora. Esta tendrá 15 teclas: una por cada dígito y operación a realizar, más la tecla de igual (Igual): 1 2 3 4 # type tecla = Mas | Menos | Por | Entre | Igual | Digito of int;; type tecla = Mas | Menos | Por | Entre | Igual | Digito of int Observen que las teclas numéricas se definen mediante el constructor de tipo Digito tomando un argumento un entero. De hecho, algunos valores del tipo tecla, no representan exactamente una tecla, por ej. (Digito 32). Por lo tanto, escribiremos una función validar que verifique si el argumento corresponde a una tecla de nuestra calculadora. El tipo de está función será tecla ->bool, esto es, toma un valor de tipo tecla y regresa un valor del tipo bool. El primer paso para construir nuestra función de verificación es programar una función que verifique si un entero es un dígito (una función de int a bool): 1 2 # let es_digito = function x -> (x >= 0) && (x <= 9) ;; val es_digito : int -> bool = <fun> Finalmente programamos la función validar: 1 2 3 4 # let validar tecla = match tecla with Digito n -> es_digito n | _ -> true ;; val validar : tecla -> bool = <fun> 133 134 �������� �� ��������� ����� Observen que la función está implementada tomando en cuenta que su argumento es de tipo tecla. Esto es, si la tecla tiene el patrón Digito n, entonces se debe verificar si n es un dígito con validar. En cualquier otro caso, la tecla será una tecla válida (si no lo fuese, el sistema detectaría un error en el tipo del argumento de la función validar (líneas 7 a la 16): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # validar Mas ;; - : bool = true # validar (Digito 9) ;; - : bool = true # validar Menos ;; - : bool = true # validar RaizCuadrada ;; Characters 8-20: validar RaizCuadrada ;; ^^^^^^^^^^^^ Unbound constructor RaizCuadrada # validar 9 ;; Characters 8-9: validar 9 ;; ^ This expression has type int but is here used with type tecla Antes de continuar con el código correspondiente al mecanismo de la calculadora, es necesario expecificar el modelo que nos permitirá describir formalmente las respuestas a la activación de cada tecla. Consideraremos que la calculadora tiene cuatro registros, que incluyen: la última computación realizada (ultimaComp), la última tecla activada (ultimaT ecla), el último operador activado (ultimaOp) y el número impreso en la pantalla (pantalla). Al conjunto de esos registros le llamaremos estado de la calculadora. El estado se modifica cada vez que una tecla es activada. Está modificación se llama transición y la teoría que gobierna este tipo de mecanismos se la teoría de automatas. El estado será representado en nuestro programa mediante un tipo producto: 1 2 3 4 5 6 7 8 9 10 # type estado = { ultimaComp : int ; (* Ultima computacion hecha *) ultimaTecla : tecla ; (* Ultima tecla activada *) ultimaOp : tecla ; (* Ultimo operador activado *) pantalla : int ; (* Pantalla del dispositivo *) };; type estado = { ultimaComp : int; ultimaTecla : tecla; ultimaOp : tecla; pantalla : int; } La siguiente tabla ejemplifica las transiciones de nuestra calculadora para la operación 3 + 21 ⇥ 2 =. El estado obedece a la tecla de la línea anterior: ��.� ��� ����������� ���� ������� �� ������� ������� estado tecla (0,=,=,0) (0,3,=,3) (3,+,+,3) (3,2,+,2) (3,1,+,21) (24,*,*,24) (24,2,*,2) 48,=,=,48) 3 + 2 1 135 * 2 = Necesitaremos una función evaluar que tome dos enteros y un valor de tipo tecla que contenga un operador; y que regrese el resultado de la operación correspondiente al operador aplicado a los dos enteros. La función puede definirse usando correspondencia entre patrones: 1 2 3 4 5 6 7 8 # let evaluar x y op = match op with Mas -> x+y | Menos -> x-y | Por -> x*y | Entre -> x/y | Igual -> y | Digito n -> failwith "evaluar: operacion invalida" ;; val evaluar : int -> int -> tecla -> int = <fun> Ahora podemos abordar la función de transición entre los estados de la calculadora. Para ello debemos considerar todos los casos posibles dado un estado de la calculadora y una tecla: • Un dígito x ha sido pulsado, por lo que hay dos casos a considerar: – La última tecla pulsada era también un dígito, por lo que el usuario está introduciendo un número de forma que el dígito x debe agregarse al valor en pantalla: pantalla = pantalla ⇥ 10 + ultimaT ecla – La última tecla pulsada no era un dígito, de forma que el usuario está comenzando a introducir un número. El nuevo estado es: (ultimaComp, (Digitx), ultimaOp, pantalla) • La tecla es un operador y la calculadora debe registrar la operación. El nuevo estado será: (ultimaOp(ultimaComp, pantalla), ultimaOp, ultimaOp, ultimaOp(ultimaComp, pantalla)) La función de transición que toma como argumentos un estado y una tecla, queda definida como sigue: 1 2 3 # let transicion estado tecla = let transicion_digito n = function Digito _ -> { estado with ultimaTecla = tecla; 136 �������� �� ��������� ����� 4 5 6 7 8 9 10 11 12 13 14 15 16 pantalla = estado.pantalla*10+n } | _ -> {estado with ultimaTecla = tecla; pantalla=n} in match tecla with Digito p -> transicion_digito p estado.ultimaTecla | _ -> let resultado = evaluar estado.ultimaComp estado.pantalla estado.ultimaOp in {ultimaComp=resultado; ultimaOp=tecla; ultimaTecla=tecla; pantalla=resultado};; val transicion : estado -> tecla -> estado = <fun> La función transicion funciona con base a una correspondencia de patrones sobre el argumento tecla. Si éste es un dígito, la función llama a la función local transicion_digito. Si no es un dígito, se computa el resultado con ayuda de la función evaluar (líneas 9–10) que toma como argumentos el último valor computado, la pantalla y la última operación registrada. La función regresa un estado donde la última computación guarda el resultado computado, la última operación y teclas toman su valor de tecla y la pantalla refleja el resultado computado. Si la tecla fue un dígito válido, la función transicion_digito hace una correspondencia de patrones sobre la última tecla registrada. Si la última tecla era un dígito (líneas 3-4) la pantalla se actualiza mediante un corrimiento decimal a la izquierda; en cualquier otro caso (línea 5) se actualiza la última tecla y la pantalla registra el dígito tecleado. Podemos hacer uso de fold para aplicar transicion sobre una lista de teclas y un estado inicial: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ��.� # let estado_inicial = {ultimaComp=0; ultimaTecla=Igual; ultimaOp=Igual; pantalla=0};; val estado_inicial : estado = {ultimaComp = 0; ultimaTecla = Igual; ultimaOp = Igual; pantalla = 0} # let ejemplo_clase = [ Digito 3; Mas; Digito 2; Digito 1; Por; Digito 2; Igual];; val ejemplo_clase : tecla list = [Digito 3; Mas; Digito 2; Digito 1; Por; Digito 2; Igual] # let lista_transiciones estado lista = List.fold_left transicion estado lista;; val lista_transiciones : estado -> tecla list -> estado = <fun> # lista_transiciones estado_inicial ejemplo_clase ;; - : estado = {ultimaComp = 48; ultimaTecla = Igual; ultimaOp = Igual; pantalla = 48} ����� �� ����� �� ����� Esta práctica tiene como objetivo aplicar los elementos de la programación funcional presentes en Ocaml, a la resolución de problemas asociados a la consulta de bases ��.� ����� �� ����� �� ����� de datos. Utilizaré la misma base de datos: mi colección de CDs y su codificación en MP3. ��.�.� Formato de los datos Aunque la mayoría de las bases de datos usan formatos propietarios, aquí asumiremos que los datos se encuentran en un archivo de texto con la siguiente estructura: • La base de datos es una lista de tarjetas separadas por saltos de línea. • Cada tarjeta es una lista de campos, separados por algún caracter especial, “:” en nuestro caso. • Un campo es una cadena de texto que no contiene saltos de línea, ni caracteres especiales. • La primer tarjeta de la base de datos corresponde a la lista de los nombres asociados a los campos, separados por barras “|”. Nuestro archivo sería algo de la forma: Artist|Cd|Rank|Ripped U2:How To Dismantle An Atomic Bomb:4:True Bob Dylan:Unplugged:4:True Pau Cassals:Les 6 Suites for Cello, Bach:1:True Thelonious Monk:All Monk (cd 1):2:False La primer línea incluye los nombres de los campos y su significado es el siguiente: • Artist es el artista que grabó el disco. • Cd es el disco en cuestión. • Rank es su ranking en mi lista de popularidad. • Ripped es un booleano que indica si el disco está codificado en MP3 o no. Tomando en cuenta estas consideraciones, es necesario decidir la representación a utilizar. Podemos trabajar con listas o arreglos de tarjetas. Las listas son fácilmente modificables: agregar o eliminar tarjetas son operaciones sencillas. Los arreglos permiten tiempo de acceso constante a cualquier tarjeta. Como nuestra meta es trabajar con todas las tarjetas y no sólo sobre algunas de ellas, la lista parece una buena opción. ¿Cual es la representación adecuada para cada tarjeta? Las mismas consideraciones se repiten: deberían ser una lista o un arreglo de cadenas de texto. En esta ocasión el arreglo parece adecuado, ya que el formato de la tarjeta es fijo (no habrá que agregar o eliminar campos) en toda la base de datos. Puesto que las consultas deben acceder sólo a algunos campos, es importante que este acceso sea rápido. La solución natural hubiera sido usar arreglos para las tarjetas, indexadas por el nombre de los campos. Como tal tipo no está disponible en Ocaml, podemos usar un arreglo (indexado por enteros) y una función asociando el nombre de un campo con el índice del arreglo que le corresponde. 137 138 �������� �� ��������� ����� 1 2 3 4 # type tarjeta = string array;; type tarjeta = string array # type base_datos = {indice : string -> int ; datos : tarjeta list};; type base_datos = { indice : string -> int; datos : tarjeta list; } El acceso al campo c de la tarjeta t en la base de datos bd, se implementa como sigue: 1 2 # let campo bd c (t:tarjeta) = t.(bd.indice c);; val campo : base_datos -> string -> tarjeta -> string = <fun> El tipo de t se restringió (cast) a tarjeta para forzar a la función f a aceptar únicamente cadenas de texto. Ejemplo 47 Veamos ahora una pequeña base de datos y el uso de la función campo. Observen su uso con map mediante aplicaciones parciales. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # let base = { datos = [ [|"Stereolab";"Serene Velocity";"1";"False"|]; [|"Offenbach";"Les Contes d’Haufmann";"1";"True"|] ] ; indice = function "Artista" -> 0 | "Cd" -> 1 | "Rank" -> 2 | "Ripped" -> 3 | _ -> raise Not_found };; val base : base_datos = {indice = <fun>; datos = [[|"Stereolab"; "Serene Velocity"; "1"; "False"|]; [|"Offenbach"; "Les Contes d’Haufmann"; "1"; "True"|]]} # campo base ;; - : string -> tarjeta -> string = <fun> # campo base "Artista" ;; - : tarjeta -> string = <fun> # campo base "Artista" (List.hd base.datos) ;; - : string = "Stereolab" # List.map (campo base "Artista") base.datos ;; - : string list = ["Stereolab"; "Offenbach"] El uso de aplicaciones parciales es el siguiente: al ser evaluada la expresión campo base “Artist”, esta genera una función que toma una tarjeta y regresa el valor de su campo “Artist”. La función List.map aplica esta función a cada una de las tarjetas en base y regresa la lista de los resultados (los artistas en la base de datos). Si bien esta implementación de field hace uso correcto de las aplicaciones parciales, explotando así una técnica funcional, es ineficiente. Esto se debe a que siempre accesamos el mismo registro, pero computamos su índice en cada iteración de List.map. Una definición más eficiente sería: 1 2 3 # let campo bd c = let i = bd.indice c in fun (t:tarjeta) -> t.(i);; 4 ��.� ����� �� ����� �� ����� val campo : base_datos -> string -> tarjeta -> string = <fun> Así, luego de aplicar dos argumentos a campo, el índice del campo se computa y puede ser usado (sin volverse a computar) en aplicaciones subsecuentes. Esta definición es funcionalmente equivalente a la anterior. ��.�.� Lectura de la base de datos desde un archivo Un archivo conteniendo la base de datos es sólo una lista de líneas. Lo primero que necesitamos hacer es separar cada línea en sus componentes separados por el caracter :. Luego necesitamos extraer los datos correspondientes y generar una función índice. Así que implementaremos una función split que parte una cadena en cada ocurrencia de un caracter separador. Esta función hará uso de la función sufijo que regresará el sufijo de una cadena s después de la posición dada i. Haremos uso de tres funciones predefinidas en Ocaml: • String.length regresa la longitud de una cadena. • String.sub regresa la subcadena de s de tamaño l iniciando en la posición i. • String.index_from computa la posición de la primera ocurrencia del carácter c en la cadena s iniciando en la posición n. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # let sufijo s i = try String.sub s i ((String.length s)-i) with Invalid_argument("String.sub") -> "";; val sufijo : string -> int -> string = <fun> # sufijo "morirse" (String.length "morir") ;; - : string = "se" # sufijo "desde la sexta" 6 ;; - : string = "la sexta" # let split c s = let rec split_from n = try let p = String.index_from s n c in (String.sub s n (p-n))::(split_from (p+1)) with Not_found -> [sufijo s n] in if s=" " then [ ] else split_from 0;; val split : char -> string -> string list = <fun> # split ’:’ "Divine Comedy:The triumph of the comic muse:3:True" ;; - : string list = ["Divine Comedy"; "The triumph of the comic muse"; "3"; "True"] Observen el uso de las excepciones. Mediante Invalid_argument se regresa una cadena vacía si el entero que se pasa a sufijo es mayor que la longitud del mismo. Con Not_found se define el caso terminal para split_from cuando ya no encontramos el carácter separador “:”. Ahora podemos computar la estructura de la base de datos. Los módulos Array y List proveen las funciones necesarias para procesar una lista de cadenas y asociar sus componentes a un índice correspondiente a su posición en la lista. La salida de mk_index debe ser una función de string ->int. 139 140 �������� �� ��������� ����� 1 2 3 4 5 6 7 8 9 10 11 12 13 # let construye_indice campos = let rec secuencia a b = if a>b then [] else a::(secuencia (a+1) b) in let indices = (secuencia 0 ((List.length campos)-1)) in let indices_campos = List.combine campos indices in function campo -> List.assoc campo indices_campos;; val indice : ’a list -> ’a -> int = <fun> # construye_indice ["Artista";"Cd";"Ranking";"Ripped"] ;; - : string -> int = <fun> # construye_indice ["Artista";"Cd";"Ranking";"Ripped"] "Artista" ;; - : int = 0 La función recursiva secuencia (líneas ) construye una secuencia en el rango a . . . b. Por ejemplo, si se tratará de una función global podríamos invocarla para obtener: 1 2 # secuencia 0 10 ;; - : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10] Las funciones List.combine y List.assoc permiten crear una estructura parecida a las listas de propiedades en Lisp y consultar tal estructura. La función resultante toma un nombre de campo y llama a assoc con el nombre y la mencionada lista de asociación para regresar el índice asociado al nombre del campo. A continuación definiremos una función para cargar una base de datos que se encuentra en un archivo de texto, con el formato que hemos definido. 1 2 3 4 5 6 7 8 9 10 11 12 # let carga_bd archivo = let canal = open_in archivo in let split_linea = split ’:’ in let campos = split ’|’ (input_line canal) in let rec read_file () = try let data = Array.of_list (split_linea (input_line canal )) in data :: (read_file ()) with End_of_file -> close_in canal ; [] in { indice = construye_indice campos ; datos = read_file () };; val carga_bd : string -> base_datos = <fun> Y ahora podemos usar carga_bd para leer nuestra base de datos: 1 2 3 4 5 6 7 8 # let mypath = "/Users/aguerra/Desktop/2008-prog-func/codigo/clase11/ ";; val mypath : string = "/Users/aguerra/Desktop/2008-prog-func/codigo/ clase11/" # let bd = carga_bd (mypath^"mycds.dat");; val bd : base_datos = {indice = <fun>; datos = [[|"U2"; "How To Dismantle An Atomic Bomb"; "4"; "True"|]; [|"Bob Dylan"; "Unplugged"; "4"; "True"|]; ��.� ����� �� ����� �� ����� [|"Pau Cassals"; "Les 6 Suites for Cello, Bach"; "1"; "True"|]; [|"Thelonious Monk"; "All Monk (cd 1)"; "2"; "False"|]]} 9 10 El índice de la base de datos bd nos permite computar: 1 2 3 4 5 6 # # # - bd.indice "Artist" ;; : int = 0 bd.indice "Cd" ;; : int = 1 List.map (campo bd "Artist") bd.datos ;; : string list = ["U2"; "Bob Dylan"; "Pau Cassals"; "Thelonious Monk "] ��.�.� Principios generales del procesamiento de bases de datos La efectividad y dificultad en el procesamiento de una base de datos, es proporcional al poder y complejidad del lenguaje de consulta usado. Puesto que queremos usar Ocaml como lenguaje de consulta, en principio no hay límites sobre el tipo de consulta que podemos expresar. Sin embargo, queremos además proveer algunas herramientas simples para manipular las tarjetas y sus datos. Esto nos llevará a limitar el poder del lenguaje de consulta, para adoptar el uso de metas generales y principios para el procesamiento de bases de datos. La meta es poder obtener un estado de la base de datos, mediante: 1. Seleccionar, de acuerdo a algún criterio, un conjunto de tarjetas; 2. Procesar cada una de las tarjetas seleccionadas; 3. Procesar todos los datos recuperados de las tarjetas. De acuerdo a lo anterior, necesitamos tres funciones con los siguientes tipos: 1. (tarjeta ! bool) ! tarjeta list ! tarjeta list 2. (tarjeta ! 0 a) ! tarjeta list ! 0 a list 3. ( 0 a ! 0 b ! 0 b) ! 0 a list ! 0 b ! 0 b Por suerte el lenguaje Ocaml provee tres iteraciones que se pueden hacer corresponder a estos tipos: List.find_all, List.map, y List.fold.right (aunque también utilizaremos List.iter). Para más detalles sobre estos operadores, consulten el manual del usuario. ��.�.� Criterios de selección El criterio de selección de una tarjeta, se forma por una combinación booleana de propiedades de algunos o todos los campos de la tarjeta. Cada campo, aunque de tipo string, puede incluir información de otro tipo, por ejemplo, int o bool. Para seleccionar de acuerdo a algún campo se necesita una función de tipo base_datos ->’a ->string ->tarjeta ->bool. El tipo ’a corresponde al tipo de la información 141 142 �������� �� ��������� ����� contenida en el campo. El tipo string corrresponde al nombre del campo en la base de datos. Definiremos dos pruebas simples sobre cadenas de texto: la igualdad con otra cadena y la prueba de no vacío. 1 2 3 4 5 6 # let campo_igual bd s n t = (s = (campo bd n t));; val campo_igual : base_datos -> string -> string -> tarjeta -> bool = <fun> # let campo_vacio bd n t = ("" <> (campo bd n t));; val campo_vacio : base_datos -> string -> tarjeta -> bool = <fun> # campo_igual bd "U2" "Artista" (List.hd bd.datos) ;; - : bool = true También es posible definir pruebas para valores reales: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # let campo_test_real r bd v n t = r v (float_of_string (campo bd n t ));; val campo_test_real : (’a -> float -> ’b) -> base_datos -> ’a -> string -> tarjeta -> ’b = <fun> # let campo_igual_real = campo_test_real (=);; val campo_igual_real : base_datos -> float -> string -> tarjeta -> bool = <fun> # let campo_menor_que_real = campo_test_real (<);; val campo_menor_que_real : base_datos -> float -> string -> tarjeta -> bool = <fun> # let campo_menor_igual_que_real = campo_test_real (<=);; val campo_menor_igual_que_real : base_datos -> float -> string -> tarjeta -> bool = <fun> # List.map (campo_igual_real bd 4. "Rank") bd.datos ;; - : bool list = [true; true; false; false] Observen nuevamente el uso de aplicaciones parciales para especializar la función campo_test_real en los test igual, menor que y menor o igual que. ��.� ������������� � ����������� Comenzaremos con una función que compute el inverso de split. Esto es, dada una lista de cadenas y un carácter separador, computa una cadena de caracteres formada por las cadenas en la lista y el carácter separador intercalado. 1 2 3 4 5 6 # let anti_split c = let separador = String.make 1 c in List.fold_left (fun x y -> if x="" then y else x^separador^y) "";; val anti_split : char -> string list -> string = <fun> # anti_split ’:’ ["U2" ; "Exitos" ; "2" ; "True"] ;; - : string = "U2:Exitos:2:True" La función List.fold_left recibe una función de dos argumentos, un elemento mínimo y una lista. Por ejemplo ��.� ������������� � ����������� 1 2 # List.fold_left (+) 0 [1;2;3;4;5] ;; - : int = 15 La sumatoria se computa de la siguiente manera: (. . . (3 + (2 + (0 + 1))) . . . ) Para construir la lista de campos en la que estamos interesados, implementamos la función extraer que regresa los campos asociados con una lista de nombres en una tarjeta dada: 1 2 3 4 5 6 7 8 # let extraer bd ns t = List.map (fun n -> campo bd n t) ns;; val extraer : base_datos -> string list -> tarjeta -> string list = <fun> # List.map (extraer bd ["Artista";"Cd"]) bd.datos ;; - : string list list = [["U2"; "How To Dismantle An Atomic Bomb"]; ["Bob Dylan"; "Unplugged "]; ["Pau Cassals"; "Les 6 Suites for Cello, Bach"]; ["Thelonious Monk"; "All Monk (cd 1)"]] Y ahora podemos escribir la función de formateo de línea: 1 2 3 4 5 6 7 # let formatea_linea bd ns t = (String.uppercase (campo bd "Artista" t)) ^" "^(campo bd "Cd" t) ^"\t"^(anti_split ’\t’ (extraer bd ns t)) ^"\n";; val formatea_linea : base_datos -> string list -> tarjeta -> string = <fun> Una corrida de ejemplo: 1 2 3 4 5 6 7 8 9 10 11 12 # List.iter print_string (List.map (formatea_linea bd ["Rank";"Ripped "]) bd.datos);; U2 How To Dismantle An Atomic Bomb 4 True BOB DYLAN Unplugged 4 True PAU CASSALS Les 6 Suites for Cello, Bach 1 True THELONIOUS MONK All Monk (cd 1) 2 False - : unit = () # List.iter print_string (List.map (formatea_linea bd []) bd.datos) ;; U2 How To Dismantle An Atomic Bomb BOB DYLAN Unplugged PAU CASSALS Les 6 Suites for Cello, Bach THELONIOUS MONK All Monk (cd 1) - : unit = () Todo esto puede integrarse en un menu, haciendo uso de la siguiente función main: 143 144 �������� �� ��������� ����� 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # let main() = let localbd = carga_bd (mypath^"mycds.dat") in let finished = ref false in while not !finished do print_string" 1: Listar CDs y Artistas\n"; print_string" 2: Listar CDs y Ranking\n"; print_string" 3: Listar CDs y Ripped\n"; print_string" 0: Salir\n"; print_string"Su Opcion: "; match read_int() with 0 -> finished := true | 1 -> (List.iter print_string (List.map (formatea_linea localbd []) localbd.datos)) | 2 -> (List.iter print_string (List.map (formatea_linea localbd ["Rank"]) localbd.datos)) | 3 -> (List.iter print_string (List.map (formatea_linea localbd ["Ripped"]) localbd.datos)) | _ -> () done; print_string"bye\n";; val main : unit -> unit = <fun> Esto generará un menú con el que se pueden obtener diferentes reportes. Para probar main ejecuten su programa en un shell (el modo tuareg que estamos utilizando en emacs, no permite capturar lecturas con read_int y funciones similares.