Notas

Anuncio
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.
Descargar