Programación con Ocaml

Vladimir Támara Patiño. Carlos F. Rodriguez

2004. Dominio público.

Este escrito se dedica a nuestro Padre Creador, a su santo Espíritu y a Jesús su Hijo y maestro nuestro.
Agradecemos las definiciones aportadas por Edwin Castiblanco y las correcciones de David Cruz Ramos y Edwin Castiblanco.

Capítulo 1  Introducción

1.1  Vistazo de programación funcional y Ocaml

Indicadores de logro:

1.1.1  Teoría

Vistazo a Ocaml

Ocaml es uno entre muchos lenguajes funcionales (como Lisp, Scheme, Haskell) que además de programación funcional pura, permite programar empleando características imperativas, algunas características propias de lenguajes orientados a objetos e incluye un sistema de módulos que facilita el desarrollo de proyectos grandes (con signaturas y functores).

Se ha desarrollado desde 1992, con base en su predecesor Caml Special Light (inicio de la década 1990), cuyo predecesor a su vez es Caml Light (1985-1990) que pertenece a la familia de lenguajes de programación funcionales ML (que se inicio en la década de 1970).

Como los demás lenguajes funcionales emplea un recolector de basura que permite en muchos casos desentenderse del manejo de memoria dinámica (el recolector elimina automáticamente memoria que no se usa --de forma que no es necesario usar free como en C).



Cuenta con un sistema de tipos fuerte, con un derivador de tipos que facilita el chequeo estático de programas sin requerir del programador tiempo en declaración explicita de tipos. Además de tipos básicos (enteros, flotantes, booleanos, caracteres y cadenas) ofrece diversas formas de construir nuevos tipos como tuplas, arreglos, listas, registros, conjuntos, streams y facilidades para definir tipos recursivos, polimórficos y paramétricos.

En cuanto a interpretación/compilación se basa en un modelo de máquina abstracta portable (máquina Zinc). Cuenta con un interprete (ocaml) que es a su vez un entorno interactivo útil para experimentar y dos compiladores: Incluso hay un compilador sobre la marcha (JIT o Just in Time Compiler) que traduce en línea bytecode a código de máquina [5].

Cuenta con herramientas que facilitan el desarrollo como un depurador simbólico; herramientas para análisis léxico y sintáctico; herramientas para automatizar compilación de proyectos grandes; editor y visualizador gráfico de módulos; modos para diversos editores; preprocesador muy configurable pues permite redefinir la sintaxis del lenguaje; extractor de documentación técnica; medidor de desempeño.

La distribución básica incluye diversas librerías portables para manejo de estructuras de datos (e.g. tablas de hashing, conjuntos, pilas), gráficas, aritmética de precisión arbitraria, multi-threading, emulación de Unix, interfaz para Tcl/Tk y C. Hay también disponibles librerías desarrolladas por diversas personas y grupos (ver índices en:
http://caml.inria.fr/humps/caml_latest.html y
http://www.npc.de/ocaml/linkdb/)

Ciertos tipos de problemas pueden programarse de forma MUY eficiente empleando Ocaml como lo muestran los resultados en las competencias internacionales ICFP realizadas desde 1998, en los que han participado grupos de programadores empleando su lenguaje de programación favorito (incluyendo lenguajes imperativos como C, orientados a objetos como Java y funcionales como Ocaml). De las 5 versiones de 1998 a 2002, equipos que han programado en Ocaml han ocupado el primer puesto en 3 oportunidades, el segundo puesto en 2 y el tercero en uno.

En cuanto a licencia, las herramientas de Ocaml emplean la GPL, mientras que las librerías la LGPL la cual permite poner la licencia que se desee a programas producidos con Ocaml y enlazados con las librerías.

Programación funcional pura

En programación funcional pura no hay estados, ni asignación, ni secuenciación, sólo funciones y aplicación funcional. En Ocaml podría programarse de forma funcional pura usando funciones anónimas, un ejemplo de una función anónima es:

fun x -> x+1;;

que define la función sucesor. Note que esta función asocia una variable x con una expresión x+11. Esta función podría aplicarse al número 2, por ejemplo:

(fun x -> x+1) 2;;

Operacionalmente aplicar un valor a una función significa remplazar en la expresión la variable de la función por el valor2. En este ejemplo la aplicación resultaría en

2+1

Esta expresión es a su vez una aplicación funcional que al reducirse3 da 3.

En programación funcional, una función es un ente de primera clase como las constantes y variables. Es decir que puede emplearse en expresiones y usarse como se usan constantes y variables. Por ejemplo :

fun x-> (fun y -> (x+y)/2)

es una función cuya expresión incluye otra función. Note que al aplicar esta función a un valor se obtendría otra función:

(fun x-> (fun y -> (x+y)/2)) 5 ®b fun y -> (5+y)/2

Que promediaría un valor (y) con la constante 5.

Como abreviación para fun x -> fun y -> exp puede emplearse:
fun x y -> exp4.

let asocia nombres con expresiones

Para asociar un nombre con una expresión se emplea

let nombre = expresion;;

por ejemplo:

let prom=(fun x -> (fun y -> (x+y)/2));;

asociará el nombre prom con una función que recibe dos enteros y calcula el promedio entre ellos. Puede usarse por ejemplo así:

prom 2 6

que reducirá a 4. O por ejemplo

let prom10=prom 10;;

que asociará el nombre prom10 a una función que recibe un entero y calcula el promedio entre 10 y ese entero.

La palabra reservada let también permite abreviar la definición de funciones:

let f=fun x->exp

puede escribirse como

let f x=exp

Así que la función prom pudo haberse escrito como

let prom x y= (x+y)/2;;



Derivación de tipos

Los tipos básicos en Ocaml son enteros int (e.g 2, -100), flotantes (e.g -0.23) float , caracteres char (e.g 'a') y cadenas string (e.g "Dios nos ama"). Para cada uno de estos tipos hay diversas funciones definidas.



Ocaml es fuertemente tipado es decir que toda expresión tiene un tipo único y sólo pueden aplicarse valores a funciones cuando el tipo del valor corresponde al tipo esperado por la función. Por ejemplo (+) es la función que suma dos enteros, mientras (+.) es la que que suma dos flotantes (en general los operadores entre enteros tienen una análogo para flotantes cuyo nombre termina con el carácter punto).



La ventaja de este tipado fuerte es la posibilidad de derivar automáticamente el tipo de expresiones (librando al programador de esa tarea). Por ejemplo una expresión como fun y -> y +. y es una función que recibe un flotante y retorna un flotante, lo derivamos sabiendo que (+.) recibe dos flotantes y retorna un flotante.

1.1.2  Hacía la práctica

Sobre documentación

Hay abundante documentación sobre Ocaml, aunque la mayoría esta en inglés, la referencia más exacta aunque tal vez no la más fácil es la guía del usuario (ver [3]).

Para aprender además de estas guías sugerimos Developing Applications in Ocaml (ver [1]).

Sobre instalación

Hay binarios precompilados de la versión más reciente de Ocaml para diversas plataformas (incluyendo x86/Windows) en el sitio de distribución http://caml.inria.fr/ocaml. Para seguir estas guías sugerimos de forma especial emplearlo en un ambiente tipo Unix (puede ser un sistema operativo de fuentes abiertas como OpenBSD o Linux). Para algunas plataformas tipo Unix hay precompilados disponibles, en otras tipo Unix pueden descargarse las fuentes y compilarse (proceso que generalmente no tiene complicaciones). Junto con las fuentes más recientes se distribuye un documento (INSTALL) que explica como realizar la compilación y que hacer en caso de algunas dificultades.

En caso de compilar, sugerimos que primero instale Tcl/Tk y se asegure de compilar con soporte para LablTk, de forma que pueda crear interfaces gráficas desde Ocaml usando Tcl/Tk.

Uso del entorno interactivo

Una vez instalado Ocaml puede emplearse el entorno interactivo para experimentar y hacer los primeros programas. Se inicia con:

ocaml

Allí pueden ingresarse construcciones válidas del lenguaje terminadas con ;; para que sean reducidas. Por ejemplo al ingresar la constante 2

# 2;;
- : int = 2


# es el prompt de este entorno interactivo 2;; es el dato ingresado y la respuesta del entorno interactivo es el tipo de la expresión reducida.

Por ejemplo al ingresar una función que eleva un entero al cuadrado:

# fun x -> x*x;;
- : int -> int = < fun >


el entorno interactivo analiza tipos y contesta indicando que la expresión es una función que recibe un entero y retorna un entero.

Al ingresar una aplicación funcional el entorno realiza la reducción completa y retorna el tipo (y de ser posible el valor) de la expresión resultante:

# (fun x -> x*x) 5;;
- : int = 25


Note que las funciones que se definan tienen asociatividad derecha, es decir:

fun x -> fun y -> x+y;;

equivale a fun x -> (fun y -> x+y);;

el tipo que Ocaml responde para esta función es:

int -> int -> int = <fun>

que puede leerse como

int -> (int -> int) = <fun>

Es decir una función que recibe un entero y devuelve una función de enteros en enteros.

A diferencia por ejemplo de:

fun f -> 1+(f 5);;

cuyo tipo es:

(int -> int) -> int

es decir es una función que retorna un entero y que recibe una función de enteros en enteros.

Además de poder ingresar expresiones directamente en el entorno interactivo pueden cargarse de un archivo fuente empleando:

#use "archivo";;

O desde la línea de comandos pueden interpretarse las expresiones de un archivo ejecutando:

ocaml archivo



1.1.3  Lecturas recomendadas

Un vistazo más completo de Ocaml puede consultarse en:
http://caml.inria.fr/ercim.html y en
http://caml.inria.fr/FAQ/general-eng.html

Puede consultar más sobre programación funcional pura y sus bases teóricas (calculo l) en los 3 primeros capítulos de [2].

Para lograr la instalación además de la documentación oficial de Ocaml puede consultar el capítulo correspondiente de [1].

1.1.4  Ejercicios para afianzar teoría

  1. ¿Qué expresión resulta tras aplicar el valor 3 a la siguiente función (haga sólo un paso de reducción)?: fun z -> z+z;;
  2. ¿Qué valor resulta tras hacer la reducción completa de la siguiente expresión?: (fun var1 -> (fun var2 -> (var1*var1)+var2)) 2 3;;
  3. La función suma puede verse en una notación prefija así: (+) 2 3 que equivale a 2+3. ¿Qué valor se obtiene al reducir por completo la siguiente expresión?: (fun v -> (+) v 3) 5 ;;
  4. ¿Qué valor resulta tras reducir por completo la siguiente aplicación?: (fun f -> 1+(f 5)) (fun y -> 2*y);;
  5. ¿Qué valor resulta tras reducir por completo la siguiente aplicación?: (fun f x y -> (f x)+(f y)) (fun y -> 2*y) 3 4;;
  6. Consulte en la documentación de Ocaml la lista de operadores. ¿En que sección del manual está dicha lista?
  7. ¿Cual es el tipo de la siguiente expresión? fun s t -> s t
  8. Busque en la documentación del módulo Pervasives una función que permita convertir un entero en flotante. Después defina una función que reciba una variable entera x y retorne el flotante que resulta de sumar x (como flotante) con 0.5.
  9. Suponga que ha ingresado en el entorno interactivo la función dif:

    let dif=fun f x h-> ((f (x+.h))-.(f x))/.h;;

    que calcula la aproximación a la pendiente de la tangente de una función en un punto x (usando como referencia el punto x+.h. Emplee esta función para definir una nueva función con nombre dif02 que haga lo mismo que la anterior pero fijando el valor de h en 0.2.

  10. Use la función dif02 del ejercicio anterior para calcular una aproximación a la derivada de la función x2 en x=1. ¿Cuál es el error en la aproximación? (el error entre un par de valores u y v es u - v ).

1.1.5  Ejercicios de programación

  1. En un archivo de nombre areas.ml asocie el nombre areatriangulo a una función cuyo tipo sea float -> float -> float y que calcule el área de un triángulo, dada la base (primer parámetro) y la altura (segundo parámetros).

    Asocie el nombre alturatriangulo a una función de tipo float -> float -> float -> float que calcule la altura de un triángulo, recibiendo como primer parámetro la base (lado mayor), y como segundo y tercer parámetros la longitud de los otros dos lados. Puede serle de utilidad emplear la función sqrt: float -> float que retorna la raíz de un flotante no negativo.

    Finalmente asocie el nombre areatriangulo2 a una función de tipo float -> float -> float -> float que calcule el área de un triángulo recibiendo los mismos parámetros de la función alturatriangulo (sugerimos que emplee la función alturatriangulo).

1
En este ejemplo decimos que la variable x está acotada en la expresión.
2
Esta aplicación en cálculo lambda (l), se llama reducción beta (b).
3
Reducir significa efectuar aplicaciones funcionales reiteradamente hasta obtener una expresión que no puede aplicarse más.
4
fun permite definir una función, function también aunque este último no permite la abreviación mencionada.

Capítulo 2  Características funcionales

2.1  Introducción

2.1.1  Teoría

Sistema de tipos.

Tanto programación funcional como en la mayoría de los lenguajes de programación, utilizamos los tipos para definir conjuntos de datos.

Los tipos de datos se pueden clasificar en dos grupos: los tipos de datos básicos (llamados también atómicos) y los tipos agregados. Ya hemos trabajado con algunos tipos atómicos como los enteros y flotantes. Los tipos agregados son colecciones de datos, dos muy usados son listas y tuplas.



Tipos básicos (atómicos).

Enteros - Tipo int

El tipo int es el tipo asignado a los números enteros (negativos, cero y positivos). El rango soportado depende de la arquitectura de hardware, para arquitecutras comptibles con Intel i386 y en general de 32 bits es -230 a 230-1 (otros tipos enteros con rangos diferentes o independientes de arquitectura son Int32, Int64 y Nativeint --ver 3.2.1).

A continuación se ejemplifica la sintaxis de literales de tipo entero y algunos operadores:
# 4;; 
- : int = 4 
# 4+5;; 
- : int = 9 
# (-) 9 5;; 
- : int = 4
# 0x2 + 0X8;;
- : int = 10
# 0b101 + 0o13;;
- : int = 16
Note que el operador infijo -, puede usarse como prefijo poniendolo entre paréntesis. Por defecto los enteros se especifican en decimal, pueden especificarse enteros en hexadecimal precediendolos con 0x o con 0X y usando los digitos adicionales A, B, C, D, F. Pueden especificarse también números en octal (precediendolos con 0o) y en binario (0b).

Los operadores utilizados para el manejo de enteros se presentan a continuación (los que se presentan entre paréntesis son operadores infijos, los otros son prefijos): Las constantes max_int : int y min_int retornan máximo y mínimo entero representable respectivamente, i.e. 230-1 y -230 en plataformas de 32 bits.

También hay definidas funciones que operan enteros a nivel de bits: Flotante - Float - float

Se emplea para representar números de punto flotante. A continuación se dan ejemplos de literales (i.e valores constantes) de tipo flotante: Note que para distinguir un flotante de un entero debe o bien usarse el punto decimal o bien especificarse un exponente con e o con E1



Los operadores utilizados para el manejo de números de punto flotante son: Las siguientes son funciones incluidas en la librería básica2: También pueden emplearse las constantes: así como la función classify_float que se explicará en la sección sobre tipos.

Caracter - char

El tipo char implementa los carácteres del estándar ASCII. Para escribir un carácter se utiliza comillas simples ejemplo: ' a', ' B', '9'. También se puede especificar el codígo ASCII utilizando \ seguido del decimal de 3 dígitos, dentro de comillas simples, ejemplos:



# '\126';;
- : char = ' '
# '\065';;
- : char = 'A'
# '\111';;
- : char = 'o'


También puede especificarse el ASCII en hexadecimal por ejemplo '\x1F'. Y pueden usarse algunas secuencias de escape para representar ciertos caracteres:

\\ barra invertida \
\" comillas "
\' apostrofe '
\n cambio de línea (LF)
\r retorno de carro (CR)
\t tabulador horizontal (TAB)
\b espacio atras (BS)

Para hacer conversiones entre caracter y código ASCII se pueden utilizar las funciones: o las siguientes definidas en el módulo Char: Booleano - bool

Los tipos de datos booleanos sólo tienen dos valores : true y false. Los operadores de relación son polimórficos (i.e. operan sobre diversos tipos) y retornan un valor booleano.

Algunos ejemplos:
# (fun x y -> x=y) 5 5;; 
- : bool = true  \\
# (fun x y -> x=y) 5 54;;
- : bool = false \\
# (fun x y -> x<>y) 5 54;;
- : bool = true \\
# (fun x y -> x<=y) 5 54;;
- : bool = true \\
# (fun x y -> x<=y) 5 5;; 
- : bool = true 
Entre booleanos pueden emplearse los operadores: Cadenas - string

Una cadena es una secuencia de caracteres5. Las literales de tipo cadena se escriben entre comillas, por ejemplo:
# "Dios es el único que lo sabe todo";;
- : string = "Dios es el único que lo sabe todo"
Pueden emplearse las mismas secuencias de escape del caso de caracters (ver 2.1.1).

El único operador entre cadenas es el operador infijo que concatena dos cadenas. Para convertir entre cadenas y otros tipos básicos pueden emplearse las siguientes funciones:
string_of_bool : bool -> string
bool_of_string : string -> bool
string_of_int : int -> string
int_of_string : string -> int
string_of_float : float -> string
string_of_float : float -> string
float_of_string : string -> float

Tipos agregados.

Tuplas.

Una tupla es un elemento de un producto cartesiano de dos o más tipos. Sintacticamente corresponde a una secuencia ordenada y heterogénea de valores separados por comas. Por ejemplo:

let f = 2, true;;

En donde creamos una tupla de 2 elementos, un entero y un booleano.

Pueden obtenerse los valores individuales de una tupla empleando let con tantas variables como valores tenga la tupla:

# let estudiante = "Carlos",18;;
val estudiante : string * int = ("Carlos", 18)
# let nombre,edad = estudiante;;
val nombre : string = "Carlos"
val edad : int = 18


En donde nombre tomara el valor string "Carlos" y edad tomara el entero 18.

Como funciones sobre tuplas están predefinidas: Listas.

Una lista es una secuencia de valores homogénea (i.e todos son valores del mismo tipo). A diferencia de las tuplas, la cantidad de valores en una lista es variable.



Una lista puede ser una lista vacia (sin elementos). Una lista que no es vacía siempre tiene una cabeza seguida de una cola, donde la cabeza es un elemento y la cola es la lista de elementos, del mismo tipo que la cabeza (la cola puede ser vacía).

Una lista en Ocaml se puede representar de las siguientes formas: Ocaml utiliza para concatenar listas el simbolo arroba @ ejemplos:

# [1]@[1;2;3];;
- : int list = [1; 1; 2; 3]
# 1::2::[3;4;5]@[1];;
- : int list = [1; 2; 3; 4; 5; 1]




Puede consultar funciones sobre listas disponibles en la biblioteca estándar de Ocaml en 3.2.1

Expresiones y Funciones.

Entre las expresiones posibles, se cuentan funciones. Una función puede definirse con la palabra reservada fun; luego de la cual siguen los argumentos que recibe, el separador -> y el cuerpo de la función. Ejemplos:

fun x -> x+x
fun x y -> x*y
fun x -> (fun y -> (x*.y));;


Las expresiones anteriore definen funciones sin nombre, pero se le puede asignar uno utilizando la palabra clave let para asociar un nombre a una expresión. Ejemplo:

let suma = fun x y -> x+y;;
let suma2 = x+y;;


Recursión.

Definir una cosa por partes, entre las partes se identifica uno o más casos que emplena la misma definición (empleando una parte más ``pequeña'') y uno o más casos que no acuden directa o indirectamente a la misma definición (i.e. los casos triviales).



Una función recursvia es una función que se llama a si misma.

Puede asociarse un nombre a na función recursiva empleando antes de la definición las palabras reservadas let rec.

Una función recursiva esta definida siempre por dos tipos de casos Un ejemplo clasico de recursividad es el cálculo del factorial de un número. El factorial de un número n es el producto de todos los números enteros entre 1 y n. Por ejemplo, 3 factorial es 1*2*3 es 6. El caso trivial de la función fatorial es si N es 1 o 0 devuelve 1. El caso recursivo si n es mayor a 1 retorna n por el factorial de n-1.

# let rec fc = function
1 -> 1
| n -> n*(fc (n-1));;
val fc : int -> int = <fun>


2.1.2  Hacía la práctica

2.1.3  Lecturas recomendadas

Un Tutorial de OCaml básico en
http://www.malditainternet.com/index.php/section=article/sid=344

Introduction to the Objective Caml Programming Language
http://www.cs.caltech.edu/courses/cs134/cs134b/book.pdf

2.1.4  Ejercicios para afianzar teoría

  1. Asocie el nombre cuadrado a una función cuyo tipo sea int -> int y que calcule el cuadrado de un entero dado.

  2. Utilizando la función cuadrado del punto anterior, realice una función que calcule el cubo de un entero dado.

  3. ¿Qué nombre debería recibir la siguiente función?
    let rec nombre = function
        0 -> 1
        | n -> n*(nombre (n-1));;
    


  4. ¿Qué diferencia tiene la anterior función a la siguiente ?
    let rec nombre n = 
    if n=0 then 1
    else n*(nombre (n-1));;
    


  5. ¿Cómo se llama el mecanismo que permite que una función se comporte como un switch case ? De un ejemplo:

  6. ¿Cuál es el tipo de las siguientes funciones ?
    let suma x y z = x+y+(z (x-y));;
    let rec serie j i n = if i=n then (j i) else (j n)+(serie j i (n-1));;


  7. ¿Que condición debe cumplirse para que opere correctamente la siguiente función?
    let rec serie j i n = 
      if i=n then 
        (j i) 
      else 
        (j n)+(serie j i (n-1))
    ;;
    


  8. ¿Qué función permite pasar un número ASCII a caracter ?

  9. Asocie el nombre miembro a una función que recibe una lista y un elemento, la función debe determinar si el elemento esta o no en la lista. ¿Cuál es el tipo de la función ?

  10. Asocie el nombre anexar a una función que recibe 2 listas y las une colocando la primera detras de la ultima. La función no debe ser recursiva.

2.1.5  Ejercicios de programación

  1. En un archivo de nombre tadlista.ml construya las siguientes funciones:

2.2  Funciones recursivas

2.2.1  Teoría

Condicional if

La sintaxis del condicional if es:

if expr1 then expr2 else expr3

Donde expr1 es una expresión de tipo bool, mientras que expr2 y expr3 son expresiones del mismo tipo. El tipo del condicional completo es el mismo de expr2 y expr3. Por ejemplo:

if 2>3 then "xy" else "cd"

Omitir la parte else expr3 es equivalente a escribir else (). La constante () es el único valor del tipo unit, se emplea en funciones que no deben retornar o recibir valor alguno. Algunos ejemplos de funciones que emplean el tipo unit son6 :

Correspondencia de patrones

Pueden definirse funciones por casos empleando function en lugar de fun y patrones. Por ejemplo, la siguiente función retorna la cadena es uno cuando recibe el entero 1 y la cadena diferente en otro caso:

function
| 1 -> "es uno"
| _ -> "diferente" ;;


Decimos que en esta definición los casos son 1 -> "es uno" y _ -> "diferente" , que el patrón del primer caso es 1 y el del segundo es _, mientras que "es uno" y "dos" son los cuerpos de cada casos. Note que un caso se separa de otro con el carácter |8.

Al aplicar esta función a un valor (que llamaremos objetivo) se inicia un algoritmo de correspondencia de patrones que determina el patrón apropiado (diremos que concuerda) con el objetivo y el cuerpo que se evalúa es el del patrón que concuerde.



En el ejemplo presentado, el carácter _ es un patrón que concuerda con cualquier objetivo, mientras que 1 es un patrón que sólo concuerda con el objetivo 1.

Los patrones son expresiones que pueden formarse con constructoras de tipos (por ejemplo :: o [] o [n1;n2;···;nk] en el caso de listas, o , en el caso de tuplas), constantes, el carácter _ y variables que concordarán con cualquier objetivo y quedaran asociadas a este.

Por ejemplo el patrón 1::n::_ concuerda con todas las listas de dos o más elementos cuya cabeza sea 1 y tras la correspondencia quedará asociada la variable n al segundo elemento de la lista. Puede emplearse por ejemplo en:

function
| [] -> 0
| [_] -> 0
| 1::n::_ -> n;;


que devolverá el segundo elemento de listas de dos o más enteros y 0 si recibe la lista vacía o listas de un entero.

En un patrón no debe emplear la misma variable más de una vez, de requerir hacer corresponder lo mismo en dos posiciones diferentes del patrón, emplee variables diferentes y comparelas como parte del cuerpo del caso o empleando when. Por ejemplo:

let f=function
| a,b when a=b -> 1
| _ -> 0;;


De requerirse, pueden emplearse paréntesis para especificar un patrón, puede indicarse el tipo del objetivo con el que concuerde una variable de un patrón con la sintaxis (var:tipo) o puede asociarse una variable a una parte del objetivo que concuerde con patrón as var. Por ejemplo el siguiente patrón que asocia una pareja con la variable c y cada elemento de la pareja con las variables a y b.

function ((a:int),b) as c -> (a,b+1,c)

Al definir una función por casos empleando correspondencia de patrones, si no cubre con patrones todos los posibles casos, al compilar o interpretar Ocaml genera una advertencia.

Además de poder usar correspondencia de patrones al definir una función con function, puede emplearlo con la expresión:

match expr with
| patrón
1 -> expr1
| patrón
2 -> expr2
:
| patrón
n -> exprn

en la que expr será el objetivo y que evaluará y retornará la expresión del caso cuyo patrón concuerde con el objetivo. Tenga en cuenta que todas las expresiones de los casos deben ser del mismo tipo (ese será el tipo retornado por esta expresión).



Definiciones locales y recursión mutua

Para abreviar algunas expresiones que incluyen subexpresiones repetidas pueden introducirse definiciones locales que asocian una variable con el valor de una subexpresión y que pueden emplearse en una expresión. Se introducen con

let var = subexpr in expr



El siguiente ejemplo resulta de adaptar una solución de dominio público de Ugo Albarello y Juan Carlos Avellaneda a un ejercicio del primer taller:

let alturatriangulo=fun b c d ->
let semip=(b +. c +. d) /. 2.0 in
((2.0 /. b) *. sqrt( semip *. (semip -.b ) *.
(semip -. c) *. (semip -. d))
;;


Por otra parte, para definir ``simultáneamente'' varias funciones recursivas puede emplearse and así:

let rec nomfun1 = expr1
and nomfun
2 = expr2
:
and nomfun
n = exprn

con lo cual pueden definirse funciones que empleen recursión mutua como:

let rec intriga x=
if (x<5) then g (x-1)
else 1+(intriga (x-1))
and g x=
if (x>=4) then intriga x
else if (x<=0) then 0
else 2+(intriga (x-1))
;;

2.2.2  Hacía la práctica

Documentación de fuentes con Ocamldoc

La herramienta ocamldoc permite extraer documentación de fuentes en Ocaml para generarla en HTML o en LaTeX o en Texinfo o como páginas man de Unix o como un grafo de dependencia entre módulos.





La documentación por extraer debe ponerse en comentarios especiales que comienzan con (** y termina con *), tal documentación se asocia a porciones de código como funciones (de las cuales ocamldoc extrae el tipo automáticamente) y puede incluir construcciones que se interpretan de forma especial como: Empleado estos comentarios puede incluir un encabezado para sus fuentes análogo al siguiente:

(** Programa para sumar dos números.
@author Juan Perez.
Estas fuentes se liberan al dominio público. 2004. Sin garantías. http://elsitio.org/donde/publico *)


y podría documentar cada función de forma análoga al siguiente ejemplo:

(** [suma x y] retorna la suma de [x] con [y] *)
let suma x y= x+y;;


Una vez agregue comentarios especiales a sus fuentes (digamos en un archivo suma.ml), puede generar documentación HTML desde la línea de comandos con:

ocamldoc -html suma.ml

2.2.3  Lecturas recomendadas

2.2.4  Ejercicios para afianzar teoría

  1. ¿A que evalúa la expresión if true=false then false else true ?
  2. ¿Cual es el tipo de la expresión if x<y then x else y+1 ?

  3. ¿Con qué objetivos concuerda el patrón [1]?
  4. Escriba un patrón que concuerde con todas las listas con los elementos "wq" e "il" usando solo los constructores :: y []
  5. ¿Cual es el tipo de la siguiente función (presentada en la lectura)? function ((a:int),b) as c -> (a,b+1,c)
  6. ¿Que objetivos no cubren los casos de la siguiente función?

    function
    | (0,_::_) -> 0
    | (1,[1]) -> 1
    | (2,[x;y]) -> x
    | (n,_) when n<>0 -> 1;;


    Note que si en tiempo de ejecución, se intenta evaluar esta función en un objetivo que no concuerde con patrón alguno se producirá una excepción.
  7. En la función del ejercicio anterior algunos objetivos son cubiertos por más de un patrón. Tras experimentar o consultar la documentación (sección sobre expresiones, subsección definición de funciones) diga cual es el caso que el algoritmo de correspondencia de patrones escoge cuando los patrones de varios casos cubren un objetivo.

  8. Entre las funciones típicas sobre listas (en programación funcional general) está la función map:

    let rec map f = function
    | [] -> []
    | h::t -> (f h)::(map f t)
    ;;


    ¿Cual es el tipo de esta función?
  9. Con una definición local para una función f que multiplique por dos use la función map para transformar la lista [3;0;1;8] en la lista [6;0;2;16].
  10. Otra función típica para lista es la función fold:

    let rec fold f v = function
    | [] -> v
    | h::t -> fold f (f v h) t
    ;;


    ¿qué valor retorna esta función al evaluar la siguiente aplicación?:

    fold (fun n e -> n+1) 0 [8;3;3;1];;

2.2.5  Ejercicios de programación

Trabaje los siguientes ejercicios en un archivo de nombre taller3.ml:
  1. Haga una función reconoce que reciba una cadena con números enteros no negativos con el operador + y espacios entre ellos y que retorne el resultado de evaluar la suma. Si la expresión que recibe no es válida genere excepciones con mensajes de error usando la función (failwith "mensaje").

    Pueden servirle las funciones:

  2. Implemente la función filter p l de tipo ('a -> bool) -> 'a list -> 'a list que filtra la lista l con el predicado p. Por ejemplo al aplicar esta función así

    filter (fun n -> n>5) [1;8;3;9;6]

    daría

    [8;9;6]

2.3  Diversos tipos de funciones

2.3.1  Teoría

Funciones anónimas.

Las expresiones pueden ser usadas para definir funciones anónimas como

# (fun x -> x + 1) 3;;
- : int = 4
# (fun x -> fun y -> fun z -> (x,y,z) ) 2 3 4;;
- : int * int * int = (2, 3, 4)


Las funciones anónimas no pueden ser recursivas porque, siendo anónimas, no tienen un nombre por el cual se puedan llamar. Sin embargo, como ya hemos visto a una función anónima se le puede asociar un nombre de la forma

let nombre = expresion;;

ejemplos:

# let sucesor = fun x -> x + 1 ;;
val sucesor : int -> int = <fun>
# sucesor 3;;
- : int = 4


# let punto3d = fun x -> fun y -> fun z -> (x,y,z) ;;
val punto3d : 'a -> 'b -> 'c -> 'a * 'b * 'c = <fun>
# punto3d 1 2 3;;
- : int * int * int = (1, 2, 3)
# punto3d 1.5 2.43 3.5;;
- : float * float * float = (1.5, 2.43, 3.5)




Funciones polimórficas.

Las funciones polimórficas son aquellas que reciben un parametro de x tipo y hacen algo con el, un ejemplo de una función polimórfica es la funcion identidad.

# let identidad x = x ;;
val identidad : 'a -> 'a = <fun>


La función generada es de tipo val identidad : 'a -> 'a = <fun>. donde 'a es un elemento de cualquier tipo.

Los operadores de comparación introducidos en la guía2 son polimórficos, al igual que las siguientes funciones: El polimorfismo puede usarse para definir funciones para manipular listas que se apliquen a listas de cualquier tipo de elementos. Un ejemplo de una función polimórfica es el numeroocurrencias, que consiste en calcular el número de veces que esta un elemento x en una lista.

# let rec numeroocurrencias x = function
| [ ] -> 0
| c::cs -> if (c=x) then (1+(numeroocurrencias x cs)) else (numeroocurrencias x cs);;
val numeroocurrencias : 'a -> 'a list -> int = <fun>


Como vemos la función generada es de tipo val numeroocurrencias : 'a -> 'a list -> int = <fun> donde 'a es un elemento de cualquier tipo, 'a list es una lista de tipo 'a y la función retorna un entero.

Ejemplos utilizando la función numeroocurrencias:

# numeroocurrencias 5 [1;2;3;4;5;5;6];;
- : int = 2
# numeroocurrencias 5 [];;
- : int = 0
# numeroocurrencias 'a' ['2';'a';'e';'a'];;
- : int = 2


Funciones de orden superior.

Son funciones que toman funciones como parámetros y/o producen funciones como resultado. Por ejemplo:

# let test (f, x) = f x;;
val test : ('a -> 'b) * 'a -> 'b = <fun>
# test (List.hd, [1;2;3]);;
- : int = 1
# test (List.tl, [1;2;3]);;
# - : int list = [2; 3]


La función test recién definida recibe en una pareja, el primer elemento de la pareja es una función f y el segundo un elemento x, lo que hace es aplicar la funciòn al elemento.

Algunas funciones de orden superior que trabajan con listas son filter, map y fold en sus diferentes variantes.

La función map aplica una función sobre todos los elementos de una lista o sobre los pares de elementos obtenidos de dos listas. Por ejemplo,

# let rec map f = function
| [] -> []
| c::cs -> (f c)::(map f cs);;
val map : ('a -> 'b) -> 'a list -> 'b list = <fun>

# map (fun x -> x+1) [1;2];;
- : int list = [2; 3]




Las funciones fold aplican un operador binario sobre los elementos de una lista hasta obtener un único valor. Si se empieza a aplicar el operador por la izquierda tenemos la función foldl (l de left) y si se empieza por la derecha foldr (r de rigth). Ejemplo:

# let rec foldr x f = function
| [ ] -> x
| c::cs -> f(c, foldr x f cs);;
val foldr : 'a -> ('b * 'a -> 'a) -> 'b list -> 'a = <fun>

# let sumar (x,y) = x+y;;
val sumar : int * int -> int = <fun>

# foldr 0 sumar [45;32;645;90];;
- : int = 812

# let mayor (x,y) =
if x>y then x else y;;
val mayor : 'a * 'a -> 'a = <fun>

# foldr 0 mayor [1;2;4;12;2;2;54];;
- : int = 54

# foldr 0 mayor [1;2;4;12;2;2;5;4];;
- : int = 12


2.3.2  Curried. Funciones parciales

Curried / Currificación Correspondencia entre cada función de múltiples parámetros y una de alto orden que retorna una función intermedia que completa el trabajo. Por cada función f definida como f (a,b) -> c siempre se puede escribir f a -> (b -> c).

Curried es una forma de reducir el número de paréntesis de una expresión consiste en reemplazar un argumento estructurado por una secuencia de argumentos más simples. Ejemplo:

let minimo (x,y) = if x < y then x else y;;

La función minimo recibe un argumento que consiste en una tupla de dos elementos. Esta expresión es equivalente a:

let minimo2 x y = if x < y then x else y;;

Donde minimo2 toma dos argumentos, uno después del otro. Más precisamente, minimo2 es una función que toma un entero x como argumento y devuelve una función minimo2 x; la función minimo x toma un entero y como argumento y devuelve un entero, el menor entre x e y. Otro ejemplo:

# let suma x y = x + y;;
val suma : int -> int -> int = <fun>
# suma 5 3;;
- : int = 8
# (suma 5) 3;;
- : int = 8
# let suma5 = suma 5;;
val suma5 : int -> int = <fun>
# suma5 3;;
- : int = 8


En este caso suma recibe dos argumentos x e y y suma5 recibe un argumento y se lo adiciona a 5.

let suma1 (x,y) = x+y;;

let suma2 x y = x+y;;


Para cada entero x la función suma2 añade x a un entero. Ejemplo suma2 1 es la función sucesor y suma2 0 es la función identidad sobre los enteros.

Para que la currificación funcione se necesita que la operación de aplicación funcional asocie a la izquierda en las expresiones. Ejemplo

minimo2 50 42;; significa (minimo2 50) 42
suma2 52 34;; significa (suma2 52) 34


Esta forma que permite reemplazar argumentos estructurados por una secuencia de argumentos más simples, se conoce como currificación, en memoria del lógico americano Haskell B. Curry.

2.3.3  Transparencia referencial

OJO. Más bien un ejemplo explicado ?

La transparencia referencial se refiere a que la evaluación de una función siempre produce el mismo resultado. Una expresión representa un valor y una propiedad a esto se llama transparencia referencial, cada expresión representa siempre al mismo valor, sin importar en que lugar de un programa aparece. Otros paradigmas de programación permiten que una misma expresión o instrucción represente o indique una acción distinta, de acuerdo al contexto en el que se encuentre. Pero la programación funcional, no.

La transparencia referencial es muy útil a la hora de modificar un programa, ya que no tenemos que preocuparnos porque las modificaciones que hagamos en una parte del mismo afecten los cálculos que se hacen en otras. También es muy poderosa a la hora de verificar un programa (demostrar matemáticamente que cumple la especificación), ya que podemos utilizar propiedades ya demostradas de todas las subexpresiones que constituyen una expresión y que valen en cualquier contexto.

2.3.4  Hacía la práctica

ocamldep, ocamlopt, ocamlc Makefile

2.3.5  Lecturas recomendadas

David Matuzeck. A concise introduction to Objective Caml. 2000 http://www.csc.vill.edu/ dmatusze/resources/ocaml/ocaml.html

Richard Bird. Introducción a la programación Funcional con Haskell. Segunda Edición. Capítulo 1 Conceptos fundamentales.

Piere Weis. Caml programming guidelines. 2003 http://caml.inria.fr/FAQ/pgl-eng.html Indentation of programs

2.3.6  Ejercicios para afianzar teoría

  1. ¿A qué evalua la siguiente función? (fun x y -> (if x>y then x else y)) 23 4;;

  2. Escriba la función anónima anexar que recibe dos listas y a la primera lista le una la segunda.

  3. Escriba la función curry que recibe una función que recibe una pareja (x,y) y que retorne la currificación

  4. La función quicksort recibe una lista x y nos devuelve una lista y con los elementos ordenados de x. ... ¿La función quicksort es una función polimórfica? si o no.

  5. La función quicksort recibe una lista x y nos devuelve una lista y con los elementos ordenados de x. ... ¿Cuál es el tipo de la función quicksort ?

  6. ¿Cuál función de orden superior me sirve para obtener de una lista de enteros el número menor ?

  7. Utilizando funciones de orden superior y funciones anónimas, arme una función que obtenga el número de ocurrencias de un elemento en una lista. De como respuesta la función aplicada a encontrar el número de ocurrencias de 'a' en ['1';'2';'a';'b';'c';'a';'c'] (como requerimiento debe utilizar List.length).

  8. ¿Cuál es el tipo de cada una de las funciones List.map y List.map2?

  9. En cuanto a funcionalidad ¿cuál es la diferencia entre List.map y List.map2?

  10. ¿Una función de orden superior puede ser polimórfica?

2.3.7  Ejercicios de programación

  1. En un archivo llamado funciones.ml escriba las siguiente funciones


  2. En un archivo llamado enterog.ml utilizando listas represente enteros positivos grandes y realice las funciones necesarias para sumar, restar y multiplicar enteros grandes, y convertir un entero en un entero grande. Ejemplos
    Opcional: Idee una representación para enteros negativos e implemente las mismas funciones con la nueva representación.

  3. Punto adicional: Cambie el archivo enterog.ml ha floatg.ml y edite las funciones, para permitir guardar flotantes grandes de la forma (parteentera, parteflotante) ejemplo el número 15.5 se representa en floatg ([1;5],[5]).

2.4  Tipos definidos por el usuario

2.4.1  Teoría

Definición de tipos

Un tipo representa un conjunto de valores. Por ejemplo el tipo bool corresponde al conjunto { true, false}, mientras que el tipo int corresponde al conjunto de enteros entre 2-30 y 230-1.



Es posible definir nuevos tipos empleando la sintaxis:

type nomtipo = expr-tipo;;

siendo nomtipo la identificación del tipo y exptipo una expresión de tipo que describe el cojunto que corresponde al tipo. La identificación se compone con letras, números y _, teniendo que ser el primer caracter una letra minúscula.



La expresión de tipo más simple es el nombre de un tipo ya definido, con lo cual se introduce una abreviatura. Por ejemplo una abreviatura para un tipo básico es:

type tnombre=string;;

o una abreviatura para un tipo compuesto es:

type tvars=int list;;

Una vez definidos, estos tipos pueden emplearse en funciones, por ejemplo:

let datose (nombre:tnombre) (edad:int)= (nombre,edad) ;;

Tipos variantes

Un tipo variante se define empleando constructores de tipo, un constructor puede ser bien una nueva constante o bien como una función que aporta elementos de otro tipo al tipo que se define. Por ejemplo el tipo bool se forma con dos constructoras constantes: true y false.

Una expresión de tipo que define un tipo variante, define cada una de las constructoras con esta sintaxis:

const_tipo
1 | const_tipo2 | ··· | const_tipon

En los tipos variante definidos por el usuario, cada constructor tiene un identificador que puede tener letras, números y _ y que debe iniciar con letra mayúscula. Los constructores constantes emplean la sintaxis:

id-const


mientras que las constructoras función:

id-const of expr-tipo


Empleando un tipo variante puede crearse un tipo con sólo nuevas constantes:

type pinta=Pica | Trebol | Diamante | Corazón

que consta justamente de 4 elementos diferentes. Podría usarse así:

let pintaroja=function
| Pica -> false
| Trebol -> false
| Diamante -> true
| Corazón -> true
;;


O como el tipo fpclass empleado por la función classify_float : float -> fpclass que es:
type fpclass =
| FP_normal (* Normal number, none of the below *)
| FP_subnormal (* Number very close to 0.0, has reduced precision *)
| FP_zero (* Number is 0.0 or -0.0 *)
| FP_infinite (* Number is positive or negative infinity *)
| FP_nan (* Not a number: result of an undefined operation *) OJO
Algunos de los constructores pueden ser funciones de otros tipos en el que se define, para lograrlo se usa :

type numero=Entero of int | Flotante of float


Figure 2.1: Ejemplo de tipo variante


el cual se representa en la figura 2.1 y que podría usarse para definir una función suma común para enteros y flotantes empleando correspondencia de patrones:
let suma x t= match (x,y) with 
 | Entero(e1),Entero(e2) -> Entero(e1+e2) 
 | Entero(e1),Flotante(f2) -> Flotante((float\_of\_int e1)+f2)
 | Flotante(f1),Entero(e2) -> Flotante(f1+(float\_of\_int e2))
 | Flotante(f1),Flotante(f2) -> Flotante(f1+f2)
;;

Tuplas y registros

Una tupla es un elemento de un producto de tipos, por ejemplo la tupla (true,3) es un elemento del producto bool*int. Un producto de tipos se indica con el operador *.

Usando un producto puede definirse una estructura que mantenga datos de una persona como nombres, apellidos, edad y sexo:
type tsexo=M | F;;
type datosper=string*string*int*tsexo;;
Sin embargo es fácil confundir el orden en el que se usan nombre y apellidos pues ambos son cadenas. Para facilitar la utilización de tipos como este se emplean registros, los cuales asocian a cada componente de la tupla un nombre:
type datosper2={nombre:string; apellido:string; edad:int; sexo:tsexo};;
Puede definirse un nuevo registro definiendo todos los componentes, por ejemplo:
let j={nombre="Andrés"; apellido="Benavidez"; edad=5; sexo=M};;
Y puede extraerse uno sólo de los componentes de la tupla con el operador punto (.), por ejemplo:
let nombreCompleto r=r.nombre^" "^r.apellido;;

Tipos recursivos

Para definir tipos más complejos puede emplearse recursión en la definición del tipo. Una ejemplo es una lista de enteros:
type lisent= Vacia | Cons of int*lisent;;
La función longitud que determina la cantidad de elementos en una lista de estas puede ser:
let rec longitud=function
 | Vacia -> 0
 | Cons (_,cola) -> 1+(longitud cola)
;;
Esta función podría aplicarse a la lista con los enteros 3; 29; -1:
longitud (Cons(3,Cons(29,Cons(-1,Vacia))));;

Tipos paramétricos

Entre los tipos predefinidos en Ocaml está option cuya definición es:
type 'a option = None | Some of 'a
Este tipo permite definir funciones que en algunos casos retornan un valor y en otros ninguno. Además es un tipo que emplea un parámetro de tipo ('a) que indica que puede usarse con cualquier otro tipo. Podría instanciarse para representar ninguna o alguna cadena con:
type stringOpt=string option;;
Un ejemplo del uso del tipo option es la siguiente función que recibe una lista de enteros y retorna el primer elemento mayor que 10:
let rec primerMayor10 = function
 | [] -> None
 | h::t -> 
  if (h>10) then
   Some h
  else
   primerMayor10 t
;;
Esta función podría emplearse en otra así:
let nomMayor le =
 match (primerMayor10 le) with
 | None -> "No hay elemento mayor que 10"
 | Some e -> "El primer elemento mayor que 10 es "^(string_of_int e)
;;
Un ejemplo de un tipo variante, parámetrico y recursivo puede ser un árbol binario:
type 'a arbbin= Vacio | Nodo of 'a * 'a arbbin * 'a arbbin;;
donde adoptaremos una convención para el constructor Nodo, la primera componente de la tupla es el elemento de la raíz, la segunda es el subárbol izquierdo y la tercera el subárbol derecho.

Ahora podríamos definir una función que calcule la altura de uno de estos árboles:
let rec altura=function
 | Vacio -> 0
 | Nodo (r,i,d) -> 1+(max (altura i) (altura d))
;;
Otro ejemplo de tipo parámetrico y recursivo es el tipo 'a list predefinido en Ocaml. Se trata de un tipo variante, parámetrico y recursivo, sus constructoras son [] y el constructor infijo :: (note que en Ocaml puro el usuario no puede definir constructores con esos nombres porque: (1) se espera que los constructores comiencen con letra mayúscula y (2) los constructores son prefijos).

Excepciones

Las excepciones se emplean para salir de funciones de forma anormal, sin retornar valor alguno. Pueden emplearse excepciones predefinidas o definir nuevas con:

exception id [ of exprtipo]

por ejemplo:
exception NoEsta;;
o
exception ErrorEnPosicion of int*string;;
Para generar una excepción se emplea raise, por ejemplo:
let rec esperaPrimeraX p=function
| [] -> raise NoEsta
| h::t -> 
 if (p=0 && h="X") then
  true
 else if (p=0) then
  raise (ErrorEnPosicion(p,"En lugar de X está "^h))
 else if (h="X") then
  raise (ErrorEnPosicion(p,"X aparece antes en la pos "
   ^(string_of_int p)))
 else
  esperaPrimeraX (p-1) t
;;
Las excepciones predefinidas son: Una función que llama a otra que genere una excepción puede terminar abruptamente con la excepción que eventualmente genera la función llamada. Para controlar esto se emplea:

try expr with pat1 -> expr1 | ··· | patn -> exprn

que detecta generación de excepciones en la expresión expr. Si no ocurre excepción alguna esta expresión evalua a lo que evalúe expr. Si ocurre una excepción al evaluar expr, se hace una correspondencia de patrones entre la excepción generada como objetivo y los patrones pat1 a patn, el resultado en tal caso es la evaluación de la primera expresión expri tal que el patrón patri corresponda al objetivo.

Por ejemplo:
let laXen5 l =
try
 if (esPrimeraX 5 l) then
  "Primera X en posición 5"
 else
  "Nunca entra a este caso"
with
 | _ -> "No se encontró la primera X en la posición 5"
;;

2.4.2  Hacía la práctica

Depuración con ocamldebug

Para emplear el depurador símbolico ocamldebug compile empleando la opción -g. Por ejemplo:

ocamlc -g -o prog prog.ml

Después puede iniciar el depurador con el programa prog con:

ocamldebug prog



Este depurador emplea algunos puntos en las fuentes para definir pasos a tales puntos les llama eventos, por ejemplo inicio de funciones o evaluación de primera expresión de un if, el número de pasos ejecutados desde el comienzo del programa hasta cierto punto se llama el tiempo.

Puede avanzar uno o más pasos con step numero y a diferencia de otros depuradores también puede deshacer pasos con backstep numero (en realida bastan las primeas letras de un comando por ejemplo s 10). Hay un límite en el número de pasos que pueden deshacerse, este límite se cambia con set history tamaño.

Puede comenzar la depuración de su programa avanzando algunos pasos (por ejemplo 12) y retrocediendo hasta reconocer instrucciones que hagan parte de sus fuentes (aparecerá el código que se ejecutará en el siguiente paso, el número de línea a la izquierda, el módulo --i.e el archivo fuente-- y el tiempo). Notará que las llamadas a funciones y el comienzo del programa se depuran en fuentes de la librería de Ocaml.

Si desea continuar la ejecución de su programa hasta el final puede ejecutar run, o para retroceder la ejecución hasta el comienzo reverse. Puede poner un punto de ruptura en cierta línea de sus fuentes con break @numlinea o una vez una función ha sido definida al ejecutar paso a paso, puede poner un punto de ruptura al comienzo con break nomfunción. Si conoce el tiempo exacto en el que desea detenerse puede emplear goto tiempo. Puede ver los puntos de ruptura que ha establecido con info breakpoints.

Para continuar hasta el final de la función en la que este, puede emplear finish o para regresar al comienzo de la función start. El comando step entrará a funciones pero si desea ejecutar la función completa y detenerse en el siguiente evento use next (su análogo hacia atras es previous).

Para evaluar e imprimir el valor asociado a símbolos puede emplear print simbolo que mostrará la estructura completa en casos de tipos compuestos o display simbolo que sólo mostrará el comienzo de la estructura9

Para ver ayuda de los comandos disponibles puede emplear help o help comando y para salir quit.

Rastreo

Desde el entorno interactivo puede rastrearse una función ya definida usando #trace función ;;. Al rastrear una función cada vez que esta sea evaluada y cada vez que termine su evaluación se imprimirán los parametros de entrada y el valor de salida respectivamente. Un ejemplo de una sesión en la que se usa #trace es:
# #use "taller5-2.ml";;
type exprar =
    Const of int
  | ...
val reconoce_exprar : string -> exprar = <fun>
- : exprar = Suma (Const 1, Const 2)
# #trace reconoce_exprar ;;
reconoce_exprar is now traced.
# reconoce_exprar "((5+3)*2)";;
reconoce_exprar <-- "((5+3)*2)"
reconoce_exprar --> Prod (Suma (Const 5, Const 3), Const 2)
- : exprar = Prod (Suma (Const 5, Const 3), Const 2)

2.4.3  Lecturas recomendadas

Pueden definirse tipos variantes más complejos de los que se presentan aquí así como clases y otras formas de tipos, puede consultar la sintaxis completa en las secciones 6.4 y 6.8.1 de [3].

La documentación sobre ocamldebug la encuentra en el capítulo 16 de [3], mientras que la de rastreo en la sección "Debugging tools" del capítulo 10 de [1].

2.4.4  Ejercicios para afianzar teoría

  1. Defina un tipo variante trivaluada cuyos constructores sean Si, No y NoSabe.
  2. Defina un tipo variante intinf que pueda representar todos los enteros soportados por el tipo int con la constructora Ent y las dos constructoras InfP e InfN que representan inf y -inf. Después defina la función sumaii que recibe dos elementos de este tipo y retorna la suma, en el caso de sumar dos infinitos (bien sean positivos o negativos) debe generar la excepción predefinida en Ocaml Invalid_argument, la cual recibe una cadena. una excepción
  3. Con el tipo que definió en el punto anterior podría definirse un racional (numerador, denominador) y una función que reduce:
    type racinf=intinf*intinf;;
    
    let redrac r1=match r1 with
    | (Ent n,Ent d) -> if (n mod d=0) then (Ent (n/d), Ent 1) else (Ent n,Ent d)
    | (InfP,Ent d) | (Ent d,InfP) -> if (d>0) then (InfP,Ent 1) else (InfN,Ent 1)
    | (InfN,Ent d) | (Ent d,InfN)-> if (d>0) then (InfN,Ent 1) else (InfP,Ent 1)
    | (InfN,InfP) | (InfP,InfN) -> (InfN,Ent 1)
    | (InfN,InfN) | (InfP,InfP) -> (InfP,Ent 1)
    ;;
    
    Cual es el tipo de redrac ?

  4. Empleando el tipo registro datosper2 introducido en la lectura, escriba una función mayor: datosper2 -> datosper2->bool que retorne true sólo si la edad del primer registro es mayor que la del segundo.

  5. Defina el tipo exprar que represente expresiones aritméticas entre enteros. Una expresión puede ser una constante (constructor Const), el inverso aditivo de una expresión (InvAd), una suma entre dos expresiones (Suma), una resta entre dos expresiones (Resta), un producto (Prod) o una División (Div).

  6. El tipo que definió en el ejemplo anterior puede usarse en una función que evalue una expresión aritmética. Complete la función siguiente y después usela para evaluar la expresión (3+5)*-4 (use InvAd para representar -4):
    let rec evalua = function
     | Const i -> i
     | InvAd e -> -(evalua e)
    ...
    ;;
    


  7. Defina un tipo parámetrico narb que permita representar un árbol n-ario. Después de definido debería poderse definir la siguiente función que calcula la cantidad de nodos de un árbol n-ario (consulte la especificación de List.fold_left en la documentación de Ocaml):
    let rec npeso = function
    | Vacio -> 0
    | Nodo (e,l) -> (List.fold_left (fun a s -> a+(npeso s)) 1 l)
    ;;
    


  8. El tipo definido en el ejercicio anterior tiene el inconveniente de permitir representar el mismo árbol de diversas formas, por ejemplo: (Nodo ("a",[])) debería ser el mismo (Nodo ("a",[Vacio])) o con tantos subárboles vacios como se desee ((Nodo ("a",[Vacio;Vacio;Vacio;Vacio]))). El tipo paramétrico narb2 definido a continuación soluciona ese inconveniente (aunque introduce otros)
    type 'a nnod=Hoja of 'a | Sub of 'a * (('a nnod) list);;
    type 'a narb2=Vacio2 | Nodo2 of 'a nnod;;
    
    Escriba la función npeso2 que retorne el peso de un arbol de tipo narb2.

  9. Otra posibilidad para el tipo narb es emplear una convención, por ejemplo no permitir que como hijos de un nodo aparezca el árbol Vacio. Defina la excepción VacioComoSubarbol y escriba la función narbBueno: 'a narb -> unit que retorne () cuando el árbol siga la convención o en otro caso que lance la excepción definida (si lo desea y le son de utilidad emplee las funciones List.exists y List.iter).

  10. Escriba la función npeso3: 'a narb -> int empleando las funciones narbBueno y npeso de los ejercicios anteriores. Debe retornar el peso de árboles que sigan la convención y -1 para árboles que no la sigan.

2.4.5  Ejercicios de programación

  1. En un archivo taller5-1.ml escriba la función sublistas: 'a list -> 'a list list que reciba una lista y retorne todas las posibles sublistas no vacias de elementos consecutivos, por ejemplo si recibe [1;2;3] debe retornar [[1];[2];[3];[1;2];[2;3];[1;2;3]]

  2. Descargue y descomprima las fuentes del programa repasa disponibles en http://structio.sourceforge.net/repasa. Compilelo y pruebe por lo menos la interfaz plana repasa.

    El archivo lrepasa.ml es una librería de funciones con diversos propósitos. Entre otras se define la función

    test_equal_answer: heuresp -> string -> string -> bool

    que decide si una respuesta es correcta empleando una posible heurística.

    Complete el archivo taller5-1.ml extendiendo el tipo heuresp con el constructor HIgnoreSpaces y extienda la función test_equal_answer para que emplee la nueva heurística. Lo que debe hacer es decidir que dos cadenas sean iguales aún cuando haya espacios en blanco redundantes (se consideran espacios en blanco: ' ', '\n', '\t' y '\r'). Por ejemplo con esta heurísitca las cadenas " hoy es miercoles" y "hoy es miercoles" deben ser consideradas iguales (aunque son diferentes a "hoyes miercoles" ).

    Opcional: Modifique las fuentes de repasa con su cambio y compile (más rápido con make repasa). Es recomendable que verifique los sitios donde se usan constructores de heuresp por ejemplo desde el interprete de comandos con:

    grep "HCase" *ml

    y que agregue el nuevo caso. Si amplia la función parse_heur (digamos para que la cadena "ignoraesp" corresponda a HIgnoreSpaces) y el DTD dtds/def.dtd para incluir la nueva cadena como caso del atributo heur del elemento sig, podrá modificar algún banco de definiciones para que emplee su heurística cuando sea usado con las interfaces de repaso repasa o repasatk. Por ejemplo puede modificar uno de los significados de regr/ejsimple.def para agregar el atributo heur="ignoraesp" y después verificar su heurística con:

    ./repasa -forma-abierta -D dtds regr/ejsimple.def

  3. Para analizar, interpretar o compilar un programa puede ser conveniente convertir la sintaxis concreta del programa a un árbol de sintaxis abstracta (AST - Abstract Syntax Tree).

    En el ejercicio de afianzamiento 5 se pedía definir un tipo para un AST apropiado para expresiones aritméticas, mientras que en el ejercicio 6 se pedía implementar una función para evaluar.

    En un archivo taller5-2.ml haga la función reconoce_exprar: string -> exprar que recibe una cadena con una expresión aritmética y retorna el AST correspondiente. Puede suponer que la cadena no tiene errores pero si espacios arbitrarios, que emplea el operador prefijo - (inverso aditivo) y los operadores infijos , +, -, * y /, y posibles subexpresiones entre paréntesis. Asigne la precedencia y asociatividad que usted desee a los operadores infijos.

    Sugerencia: Compile el archivo y de requerirlo depure con ocamldebug.

    Opcional: Mejore la función que implementó para que detecte errores y lance una exepción, y para que de prioridades y asociatividad a los operadores así:

1
3.2E4 corresponde a 3.2·104
2
Librería básica es el término con el que traducimos `core library.´
3
Esta igualdad polimórfica compara igualdad estructural, puede emplearse con tipos como listas o con tipos definidos por usuario o con estructuras mutables.
4
Las comparaciones funcionan sobre tipos básicos de la manera usual (para booleanos se considera true > false. En estructuras compuestas se extienden de manera natural --aprovechando su definición usual para enteros, caracteres, flotantes, cadenas y booleanos.
5
La implementación actual soporta cadenas de hasta 224-5 caracteres en plataformas de 32 bits
6
Estas funciones en Ocaml no son "puras" porque tienen efectos laterales (como imprimir en pantalla). En programación funcional pura las funciones no tienen efectos laterales, es decir al llamar una función todo el resultado de su operación será retornado a la función llamadora.
7
También puede hacerse un salto de línea empleando la secuencia de escape \n en una cadena.
8
En realidad no se necesita el primer | después de function, lo ponemos para facilitar visualizar y eventualmente reordenar o agregar casos.
9
Es posible definir funciones de impresión para ciertos tipos (ver en el manual de referencia load_printer).

Capítulo 3  Característcas imperativas

3.1  Características imperativas

3.1.1  Teoría

Estilo imperativo.

Thomas Kuhn define paradigma como "logros científicos universalmente reconocidos que durante un tiempo proporcionan problemas y soluciones modelo para una comunidad de profesionales". Además sostiene que "a pesar de ambigüedades ocacionales, los paradigmas de una ciencia madura pueden ser determinados con relativa facilidad".

Un paradigma de programación representa un esquema particular para la construcción de software, abarca ciertos lenguajes que comparten elementos estructurales y elementos metodológicos.

El paradigma imperativo es aquel que facilita los cálculos por medio de cambios de estado (condición de una memoria o almacenamiento). Un programa imperativo realiza una secuencia de acciones que se ejecutan en orden, pero también cuenta con herramientas para modificar el orden de ejecución de las acciones. Algunos lenguajes de programación que utilizan el paradigma imperativo son Fortran, C, C++, Pascal y Cobol. Como hemos visto Ocaml es uno entre muchos lenguajes funcionales que además de programación funcional pura, permite programar empleando características imperativas, algunas características propias de lenguajes orientados a objetos.



Las dos principales desventajas, comparadas al estilo puramente funcional, son: El fin de integrar elementos imperativos en un lenguaje funcional, es poder escribir ciertos algoritmos en este estilo cuando sea necesario, ya que ciertos algoritmos son más fáciles de escribir en este estilo de programación. Como ejemplo el cómputo del producto de dos matrices. Aunque es ciertamente posible traducirlo a una versión puramente funcional, en la cual las listas substituyen vectores, esto no es natural ni eficiente.

Entre las características imperativas de Ocaml se encuentran: En este capitulo trabajaremos con las características imperativas de Ocaml.



Estructuras de datos modificables.

Vectores.

Los vectores son una lista de datos con un número fijo de elementos, todos del mismo tipo. Se puede escribir un vector directamente enumerando sus valores entre los símbolos [| y |] , separado por puntos y comas como en las listas. Ejemplo:

# let v = [| 3.5; 26.25; 19.0 |] ;;
val v : float array = [| 3.5; 26.25; 19.0 |]


Para crear un vector también se puede utilizar la función Array.create, la cual recibe el tamaño del vector y el elemento inicial o inicialización de los campos del vector. Ejemplo:

# Array.create;;
- : int -> 'a -> 'a array = <fun>

zz # let v = Array.create 3 3.14;;
val v : float array = [|3.14; 3.14; 3.14|]
# let v2 = Array.create 4 '1';;
val v2 : char array = [|'1'; '1'; '1'; '1'|]


Al igual que en otros lenguajes de programación, el primer elemento de un vector tiene índice 0 y el índice del elemento final es la longitud del vector menos 1. Para tener acceso a un elemento particular, se debe dar el índice de la siguiente forma:

expr
1 . ( expr2 )

y para modificar una posición:

expr
1 . ( expr2) <- expr3

donde expr1 debe ser un vector (tipo array) luego sigue un punto y la expr2 entre paréntesis, es el índice. Se utiliza el símbolo <- para decir que va tomar un valor en el índice expr2. Ejemplo:

# let v = [| 3.5; 26.25; 19.0 |] ;;
val v : float array = [| 3.5; 26.25; 19.0 |]
# v.(1) ;;
- : float = 26.25
# v.(0) <- 4.5 ;;
- : unit = ()
# v ;;
- : float array = [| 4.5; 26.25; 19.0 |]
# v.(2);;
- : float = 19.


Si el índice usado para tener acceso a un elemento en el vector es mayor o igual a la longitud del vector, una excepción se produce en el momento del acceso. Ejemplo:

# v.(3);; Exception: Invalid_argument "Array.get".

Las funciones para manejar vectores (arreglos) hacen parte del módulo Array, en los siguientes ejemplos, se utilizaran cuatro funciones del módulo Array: Ejemplos:

# let v = [|1;2;3|];;
val v : int array = [|1; 2; 3|]
# let v2 = Array.create 5 0;;
val v2 : int array = [|0; 0; 0; 0; 0|]
# let v3 x y = Array.append x y;;
val v3 : 'a array -> 'a array -> 'a array = <fun>
# v3 v2 v;;
- : int array = [|0; 0; 0; 0; 0; 1; 2; 3|]
# v3 v v2;;
- : int array = [|1; 2; 3; 0; 0; 0; 0; 0|]
# let longitud x = Array.length x;;
val longitud : 'a array -> int = <fun>
# longitud v;;
- : int = 3
# longitud v2;;
- : int = 5
# longitud (v3 v2 v);;
- : int = 8
# let v4 = Array.copy v;;
val v4 : int array = [|1; 2; 3|]
# v4;;
- : int array = [|1; 2; 3|]


Compartir valores en un vector.

Todos los elementos de un vector contienen el valor que fue pasado cuando fue creado. Esto implica compartir de este valor, si es un valor estructurado. Por ejemplo, si se crea una matriz (un vector de vectores) m que utiliza la función create del módulo Array.

# let v = Array.create 3 0;;
val v : int array = [|0; 0; 0|]
# let m = Array.create 3 v;;
val m : int array array = [|[|0; 0; 0|]; [|0; 0; 0|]; [|0; 0; 0|]|]


Si se modifica uno de los campos del vector v, que fue utilizado en la creación de m, entonces se modifica automáticamente todo las "filas" de la matriz. Ejemplo:

# v.(1)<-5;;
- : unit = ()
# v;;
- : int array = [|0; 5; 0|]
# m;;
- : int array array = [|[|0; 5; 0|]; [|0; 5; 0|]; [|0; 5; 0|]|]


La duplicación del valor de inicialización (función create) ocurre debido a que el valor que se comparte es un valor estructurado en este caso el vector.

Pueden compararse vectores completos con el operador = (igualdad estructural), pero al igual que con las demás estructura mutables también puede emplearse el operador igualdad física e1 == e2 , que es cierto sólo cuando la modificación física de e1 afecta también a e2. Con el ejemplo anterior m.(0).(0)==m.(1).(0) sería true mientras que:
# let m2=[|0;1;2|];;
val m2 : int array = [|0; 1; 2|]
# m2.(0)==m2.(1);;
- : bool = false
Por su parte la diferencia física se decide con el operador infijo !=

Matrices no-rectagulares.

Una matriz, un vector de vectores, no necesariamente debe ser rectangular. Se puede manejar una matriz cuyas filas (vectores) tengan diferentes número de columnas (longitudes de cada vector). Ejemplo la matriz t construye una matriz triangular para los coeficientes del triángulo del PASCAL.
# let t = [\|
[| 1 |];
[| 1; 1 |];
[|1; 2; 1|];
[|1; 3; 3; 1|];
[|1; 4; 6; 4; 1|];
[|1; 5; 10; 10; 5; 1|]
|] ;; 
val t : int array array = [|[|1|]; [|1; 1|]; [|1; 2; 1|]; [|1; 3; 3; 1|]; [|1; 4; 6; 4; ...|]; ...|]

En donde el contenido de la fila 4 es [|1; 3; 3; 1|]

# t.(3) ;;- : int array = [|1; 3; 3; 1|]

OJO. Usar make en lugar de create
get : 'a array -> int -> 'a
set : 'a array -> int -> 'a -> unit
make : int -> 'a -> 'a array
init : int -> (int -> 'a) -> 'a array
make_matrix : int -> int -> 'a -> 'a array array
append : 'a array -> 'a array -> 'a array
concat : 'a array list -> 'a array
sub : 'a array -> int -> int -> 'a array
copy : 'a array -> 'a array
fill : 'a array -> int -> int -> 'a -> unit
blit : 'a array -> int -> 'a array -> int -> int -> unit
to_list : 'a array -> 'a list
of_list : 'a list -> 'a array
iter : ('a -> unit) -> 'a array -> unit
map : ('a -> 'b) -> 'a array -> 'b array
iteri : (int -> 'a -> unit) -> 'a array -> unit
mapi : (int -> 'a -> 'b) -> 'a array -> 'b array
fold_left : ('a -> 'b -> 'a) -> 'a -> 'b array -> 'a
fold_right : ('a -> 'b -> 'b) -> 'a array -> 'b -> 'b
sort : ('a -> 'a -> int) -> 'a array -> unit
stable_sort : ('a -> 'a -> int) -> 'a array -> unit
fast_sort : ('a -> 'a -> int) -> 'a array -> unit

Cadenas

Una cadena en Ocaml no es lo mismo que un vector de caracteres. Hay una sintaxis especial para acceder a sus elementos:

expr
1 . [ expr2 ]

Los elementos de una cadena pueden ser modificados físicamente:

expr
1 . [ expr2 ] < - expr3

# let s = "hola Colombia";;
val s : string = "hola Colombia"
# s.[2];;
- : char = 'l'
# s.[0]<-'o';;
- : unit = ()
# s;;
- : string = "oola Colombia"


OJO. Todas las funciones del módulo String.

Campos mutables

Los campos de un registro pueden ser declarados mutables. Lo que se debe hacer es a la hora de declarar el tipo del registro se le antepone la palabra clave mutable. Ejemplo:

# type punto2d = { mutable xc : float; mutable yc : float } ;;
type punto2d = { mutable xc : float; mutable yc : float; }
# let p2 = { xc = 1.6; yc = 0.5 } ;;
val p2 : punto2d = {xc = 1.6; yc = 0.5}


En este caso el tipo punto2d esta formado por las coordenadas xc e yc de tipo float y son declarador mutables, de esta forma el valor de un campo que sea declarado mutable se puede modificar usando la sintaxis:



expr
1 . nombre < - expr2

La subexpresión expr1 de la expresión debe ser un tipo de registro que tiene el nombre del campo. Ejemplo:

# p2;; - : punto2d = xc = 1.6; yc = 0.5
# p2.xc <- 4.5;;
- : unit = ()
# p2;;
- : punto2d = xc = 4.5; yc = 0.5


Podemos escribir una función para mover un punto2d modificando sus componentes. Utilizamos un declaración local con la concordancia con el modelo para ordenar los efectos secundarios.

# let mover p dx dy =
let () = p.xc <- p.xc +. dx
in p.yc <- p.yc +. dy ;;
val mover : punto2d -> float -> float -> unit = <fun>
# p2;;
- : punto2d = xc = 4.5; yc = 0.5
# mover p2 1.1 2.2 ;;
- : unit = ()
# p2 ;;
- : punto2d = xc = 5.6; yc = 2.7


Nota: Es posible mezclar campos mutable y non-mutable en la definición de un registro, pero solamente los campos declarador como mutable pueden ser modificados.

Referencias

Ocaml proporciona una referencia polimórfica del tipo ref que se pueda utilizar como apuntador a un valor; en la terminología de Ocaml se le denomina referencia a un valor. Un valor referido puede ser modificado. La referencia del tipo se define como registro con un campo modificable:

type 'a ref = mutable contents:'a

Este tipo se proporciona un atajo sintáctico. Para construir una referencia a un valor se utiliza la función ref. El valor referido se puede tomar usando la función del prefijo (!). La función que modifica el contenido de una referencia es la función (:=). Ejemplo:

# let x = ref 3 ;;
val x : int ref = contents = 3
# x;;
- : int ref = contents = 3
# !x;;
- : int = 3
# x:=4;;
- : unit = ()
# x:= !x+2;;
- : unit = ()
# x;;
- : int ref = contents = 6
# !x;;
- : int = 6


OJO: incr : int ref -> unit, decr : int ref -> unit

Entrada salida: Canales, lectura escritura

Las funciones de entrada-salida calculan un valor (a menudo del tipo unit) pero durante su cálculo causan una modificación del estado de los periféricos (puertos) de entrada-salida: modificación del estado del buffer del teclado, utilizando como salida la pantalla, escribiendo en un archivo, o modificación de un apuntador leído. En Ocaml se definen dos tipos de canales in_channel y out_channel, canal de entrada y canal de salida respectivamente. Cuando se llega al final de un archivo se produce la excepción End_of_file. Finalmente, las tres constantes siguientes corresponden a los canales estándares para entrada, salida, y error al estilo de Unix: stdin, stdout y stderr.

Canales

Las funciones de entrada-salida de la librería estándar de Ocaml para manejar los canales de comunicaciones toman valores de tipo in_channel o out_channel. Aparte de los tres valores predefinidos, la creación de un canal utiliza una de las siguientes funciones:

# open_in;;
- : string -> in_channel = <fun>
# open_out;;
- : string -> out_channel = <fun>


La función open_in abre un archivo si existe, de lo contrario ejecuta la excepción Sys_error.open_out.



# let ic = open_in "koala";;
val ic : in_channel = <abstr>
# let oc = open_out "koala";;
val oc : out_channel = <abstr>


el archivo koala existe

# let ic = open_in "leer";;
Exception: Sys_error "leer: No such file or directory".


el archivo leer no existe.



Las funciones para cerrar un canal son:

# close_in ;;
- : in_channel -> unit = <fun>
# close_out ;;
- : out_channel -> unit = <fun>


OJO. Constantes:
stdin : in_channel
tdout : out_channel
stderr : out_channel

Lectura y escritura

Las funciones más generales para lectura y escritura son: OJO. Mejor separar lectura de escritura.
type open_flag =
| Open_rdonly (* open for reading. *)
| Open_wronly (* open for writing. *)
| Open_append (* open for appending: always write at end of file. *)
| Open_creat (* create the file if it does not exist. *)
| Open_trunc (* empty the file if it already exists. *)
| Open_excl (* fail if the file already exists. *)
| Open_binary (* open in binary mode (no conversion). *)
| Open_text (* open in text mode (may perform conversions). *)
| Open_nonblock (* open in non-blocking mode. *)
open_in_bin : string -> in_channel
open_in_gen : open_flag list -> int -> string -> in_channel
input_char : in_channel -> char
input_line : in_channel -> string
input : in_channel -> string -> int -> int -> int
really_input : in_channel -> string -> int -> int -> unit
input_byte : in_channel -> int
input_binary_int : in_channel -> int
input_value : in_channel -> 'a
seek_in : in_channel -> int -> unit
pos_in : in_channel
in_channel_length : in_channel -> int
close_in : in_channel -> unit
close_in_noerr : in_channel -> unit
set_binary_mode_in : in_channel -> bool -> unit
open_out : string -> out_channel
open_out_bin : string -> out_channel
open_out_gen : open_flag list -> int -> string -> out_channel
flush : out_channel -> unit
flush_all : unit -> unit
output_char : out_channel -> char -> unit
output_string : out_channel -> string -> unit
output : out_channel -> string -> int -> int -> unit
output_byte : out_channel -> int -> unit
output_binary_int : out_channel -> int -> unit
output_value : out_channel -> 'a -> unit
seek_out : out_channel -> int -> unit
pos_out : out_channel -> int
out_channel_length : out_channel -> int
close_out : out_channel -> unit
close_out_noerr : out_channel -> unit
set_binary_mode_out : out_channel -> bool -> unit
# input_line ;;
- : in_channel -> string = <fun>
# input ;;
- : in_channel -> string -> int -> int -> int = <fun>
# output ;;- : out_channel -> string -> int -> int -> unit = <fun>
Las siguientes funciones leen de la entrada estándar o escriben en la salida estándar:

# read_line ;; - : unit -> string = <fun>
# print_string ;;
- : string -> unit = <fun>
# print_newline ;;- : unit -> unit = <fun>




Un ejemplo: Higher/Lower

let rec hilo n =
let () = print_string "type a number: " in let i = read_int () in
if i = n then
let () = print_string "BRAVO" in
let () = print_newline ()
in print_newline ()
else
let () =
if i < n then
let () = print_string "Higher"
in print_newline ()
else
let () = print_string "Lower"
in print_newline ()
in hilo n ;;


# hilo 64;;
type a number: 88
Lower
type a number: 44
Higher
type a number: 64
BRAVO

- : unit = ()


Estructuras de control: secuencias, ciclos

Secuencias.

La primera de las estructuras de control típicamente imprescindible es la secuencia. Esta permite la evaluación de izquierda a derecha de una secuencia de expresiones separadas por puntos y comas.

expr
1 ; ... ; exprn

Una secuencia de expresiones se puede ver como una expresión, donde el valor que se retorna es el de la ultima expresión pasada en la secuencia (exprn). Sin embargo, se evalúan todas las expresiones. Ejemplo:

# let x = ref 4 ;;
val x : int ref = contents=1
# x:=!x+1 ; x:=!x*4 ; !x ;;
- : int = 20

# print_string "2 = "; 1+1 ;;
2 = - : int = 2


Los valores obtenidos antes del punto y coma se desechan.

Como regla general se recomienda tener entre paréntesis las secuencias, para clarificar su alcance. Sintácticamente, los paréntesis puede tomar dos formas:

( expr )

o

begin expr end

Ejemplo: el programa Higher/Lower escrito de forma más natural:

let rec hilo n =
print_string "type a number: ";
let i = read_int () in
if i = n then print_string "BRAVO\n\n"
else
begin
if i < n then print_string "Higher\n" else print_string "Lower\n" ;
hilo n
end ;;


Ciclos

Las estructuras iterativas de control también se encuentran fuera del mundo funcional. La expresión condicional para repetir, o no, un ciclo no tiene sentido a menos que pueda haber una modificación física de la memoria que permite que su valor cambie. Hay dos estructuras iterativas del control en Ocaml: for ciclo para una iteración limitada y while para el ciclo de una iteración no-limitada.

Sintaxis del for

for name = expr1 to expr2 do expr3 done

o bien

for name = expr1 downto expr2 do expr3 done

El expr1 y la expr2 son de tipo int. Si la expr3 no es de tipo unit, el compilador produce un mensaje de alerta.

Ejemplo

# for i=1 to 10 do print_int i;
print_string " " done; print_newline() ;;
1 2 3 4 5 6 7 8 9 10
- : unit = ()
# for i=10 downto 1 do print_int i; print_string " " done; print_newline() ;;
10 9 8 7 6 5 4 3 2 1
-: unit = ()




Para el while la sintaxis es:

while expr1 do expr2 done

La expr1 debe ser del tipo bool y la expr2 del tipo unit, de lo contrario el compilador produce un mensaje de alerta.

Ejemplo:

# let r = ref 1
in while !r < 11 do
print_int !r ;
print_string " " ;
r := !r+1 done ;;
1 2 3 4 5 6 7 8 9 10
- : unit = ()


3.1.2  Ejercicios para afianzar teoría

  1. La función que me permite conocer la longitud de un vector es

  2. La función que me permite conocer el número de filas de una matriz es

  3. Construya un vector v de enteros, de longitud 5 donde los campos del vector este inicializados con -1.

  4. Cree una función c que reciba un entero x y retorne un nuevo vector de caracteres, con x caracteres, cada uno un espacio.

  5. La función Array.copy recibe como parametro.

  6. Escriba un tipo de dato que utilice dos registros mutables y uno no-mutable. Explique el funcionamiento.



  7. Construya una referencia x al valor 5.

  8. Construya una referencia z al valor -5 y calcule el resultado de sumar los valores referenciados por x y por z.

  9. Para abrir un archivo si este no existe que excepción se ejecuta?

  10. Escriba una función que abra un canal de entrada, leyendo del archivo leer.txt .

3.1.3  Ejercicios de programación

  1. En un archivo vectores.ml escriba las siguientes funciones para manejo de arreglos de tipo entero:

  2. En un archivo cat.ml escriba un programa que reciba por línea de comandos el nombre de un archivo y que lo imprima en sálida estándar. Cree un ejecutable y pruebelo. (Ayuda: Para sacar parámetros recibidos por la línea de comandos puede emplear el vector Sys.argv).

  3. En un archivo archivo2.ml escriba un programa que reciba por línea de comandos el nombre de un archivo, un entero a y un entero b. Debe crear un archivo con el nombre recibido en el que cada línea tiene un entero comenzando en b+1 y terminando en a-1, debe generar una excepción definida por usted en caso de que b sea menor o igual a a.

    Ejemplo si el intervalo es de 1 a 7 en el archivo debe quedar los números del 2 al 6 de la siguiente forma:
    2
    3
    4
    5
    6

  4. En las fuentes de repasa revise el tipo repasa_interface que mantiene la información requerida tanto por la interfaz modo texto (archivo repasa.ml) como por la interfaz gráfica (archivo repasatk.ml). En este momento están definidos dos tipos de preguntas choiceQuestion y openQuestion. Analice la implementación de ambas en la interfaz modo texto y después agregue un campoHaga el cambio al registro tanto en defprog.ml como en defprog.mli. para un tercer tipo de pregunta ahorcadoQuestion que presente la pregunta y permita al usuario jugar ahorcado para responderla. Para comenzar a implementarla en la interfaz plana copie el cuerpo de openQuestion_plain en ahorcadoQuestion_plain, y haga los cambios necesarios en defprog.ml para que pueda usarse. Compile y verifique sus modificaciones.

    Opcional: Implemente la funcionalidad real de ahorcadoQuestion_plain.

3.2  Librerías

3.2.1  Teoría

Un módulo se compone de una interfaz donde se declaran las funciones públicas del módulo y un archivo fuente donde se definen las funciones (si no se escribe una interfaz explicita, se asume que todas las funciones definidas son públicas).

Ocaml se distribuye junto con diversos módulos agrupados en librerías que entre otras permiten: manejo de tipos básicos, operaciones aritméticas de diversa precisión, manejo de estructuras de datos, operaciones con archivos, operaciones comunes en sistemas Unix. Todas estas librerías (excepto num en el momento de este escrito) están cubiertas con una licencia que permiten enlazarlas con código de licencias arbitrarias.

Fuera del equipo de desarrolladores de Ocaml, otras personas han escrito librerías con diversos propósitos, en muchos casos con licencias generosas que permiten emplearlas en todo tipo de proyectos.

En esta guía se presentan algunas librerías y algunos ejemplos de su uso.

Librería estándar de Ocaml

La librería estándar de Ocaml (stdlib) incluye el módulo Pervasives (que se abre por defecto) y otros módulos bastante usados (por ejemplo List, String, etc.). Esta librería se encadena automáticamente a programas compilados con ocamlc y es cargada automáticamente cuando se inicia una sesión interactiva con ocaml.

En las guías anteriores se han estudiado tipos y funciones del módulo Pervasives, así como algunas funciones y tipos de otros módulos (por ejemplo Char, String y List). Diversos módulos de la librería estándar se presentan a continuación agrupados por categorias (el módulo Char se presentó completo en la guía 2, mientras que el módulo Array en la guía 6). La mayoría de descripciones son traducción de la documentación de referencia (ver [3]).

Módulos para manejo de tipos básicos

Módulo String Módulo List

Algunas funciones no emplean recursión de cola. Una función que emplea recursión de cola usa espacio constante de pila, mientras que una función que no emplee recursión de cola, usa espacio de pila proporcional a la longitud del argumento que sea lista, lo cual es un problema para listas muy largas. Cuando la función recibe diversos argumentos que sean listas, una fórmula aproximada para el uso de pila (en una unidad constante no especificada) se presenta en paréntesis.

Las consideraciones anteriores usualmente pueden ser ignodara si sus listas no tienen más de 10000 elementos. Iteradores Iteradores sobre dos listas Recorridos en listas Busquedas en listas Listas asociativas Listas de parejas Ordenamiento Módulos para manejo de estructuras de datos

Módulo Hashtbl

Tablas de hash y funciones hash.

Las tablas de hash son tablas de asociación que permiten modificación. Primitiva polimórfica de hash Módulo Queue

Este módulo implementa colas primero en entrar, primero en salir con elemento modificables. Módulo Stack

Este módulo implementa una pila último en entrar, primero en salir, con elementos modificables. Módulo Buffer: Buffer expandible para cadenas

Este módulo implementa un buffer para cadenas que se expande automáticamente cuando lo necesita. Ofrece concatenación acumulativa de cadenas en tiempo casi lineal (en lugar del tiempo cuadrático que requiere la concatenación de un par de cadenas). Módulo Set. Conjuntos sobre tipos ordenados

Este módulo implementa la estructura de datos conjunto, dada una función sobre un conjunto de elementos, que lo ordene totalmente. Todas las operaciones sobre conjuntos son puramente aplicativas (sin efectos laterales). La implementación usa árboles binarios balanceados, y por tanto es razonablemente eficiente: membrecia e inserción toma tiempo logartimico en el tamañno del conjunto, por ejemplo.

Esta estructura de datos está implementada como un functor (i.e una función de estructuras en estructuras), para usarla debe definirse un módulo nuevo instanciando el functor que instancie la estructura de datos a un tipo particular. El nombre del functor es Set.Make, el siguiente ejemplo crea un módulo IdSet1, que es un conjunto de triplas de cadenas:
module IdSet=Set.Make(struct
                type t=string*string*string
                let compare (s1,s2,s3) (sa,sb,sc) =
                        if (s1<sa || (s1=sa && s2<sb)
                                || (s1=sa && s2=sb && s3<sc)) then
                                -1
                        else if (s1=sa && s2=sb && s3=sc) then
                                0
                        else
                                1
                end)
;;
Este módulo IdSet define las funciones para conjuntos que se explicaran a continuación (por ejemplo IdSet.is_empty). Módulo Map: Tablas asociativas sobre tipos ordenado

Este modulo implementa tablas asociativas de forma alicativa, también se conocen como mapeos finitos o diccionarios, dado una función que ordene totalmente las llaves. Todas las operaciones sobre mapeos son puramente aplicativas (sin efectos laterales). La implementaciónn usa árboles binarios balanceados, y por tanto la busqueda y la isnerción toman tiempo logaritimico en el tamaño del mapeo.

De forma análoga a Set, debe instanciarse un mapeo para un tipo particular con el functor Map.Make. El nuevo módulo producido podrá emplearse con las siguientes funciones: Módulos relaciones con aritmética

Módulo Random: Generadores de números seudo-aleatorios

Funciones básicas También es posible manipular el estado del generado, con el modulo Random.State. El estado del generado se obtiene con Random.get_state: unit -> Random.State.t , y se establece con Random.set_state: Random.State.t -> unit .

Módulo Complex : Números complejos

Este módulo ofrece operaciones aritméticas sobre números complejos. Los números complejos se representan por sus partes real e imaginaria (representación cartesiana). Cada parte se representa con un número de punto flontate de doble precisión (tipo float). Módulo Int32: enteros de 32 bits

Este modulo provee operaciones sobre el tipo int32 de enteros con signo de 32 bits. A diferencia del tipo incorporado int, el tipo int32 se garantiza que es exactamente de 32 bits de ancho en toda plataforma. Todas las operaciones aritméticas sobre int32 se toman módulo 231.

Para escribir constantes de este tipo puede emplear el postfijo l. Por ejemplo 0l se refiere al cero de tipo int32.

Nota sobre desempeño: los valores de tipo int32 ocupan más memoria que los valores de tipo int, y las operaciones aritméticas sobre int32 son generalmente más lentas que las operaciones sobre int. Use int32 sólo cuando la aplicación requiera aritmética exacta de 32 bits. Módulo Int64: enteros de 64 bits

El módulo Int64 permite operar con enteros de 64 bits. Tiene funciones análogas y con los mismos nombres que las funciones de Int32.

Para escribir constantes de este tipo puede emplear el postfijo L (e.g. 0L es el cero de tipo int32.

Las funciones novedosas de Int64 con respecto a Int32 son: Módulo NativeInt: enteros nativos para la plataforma de ejecución

Este módulo provee operaciones sobre el tipo nativeint de enteros de 32 bits con signo (en plataformas de 32 bits) o de enteros de 64 bits con signo (en plataformas de 64 bits). Este tipo entero tiene exacatmente el mismo ancho que el del tipo entero long en el compilador de C. Todas las operaciones aritméticas sobre nativeint se tomand módulo 232 o 264 dependiendo del tamaños de la palabra ne arquitectura.

Nota sobre el desempeño: los valores de tipo nativeint ocupan más espacio en meoria que los valores de tipo int, y las operaciones aritméticas sobre nativeint son generalmente más lentas que las de int. Emplee nativeint sólo cuando la aplicación requiera el bit extra de precisión que nativeint da por encima del tipo int.

El módulo NativeInt define funciones con los mismos nombres del módulo Int32, las novedades con respecto a ese son:

3.2.2  Hacía la práctica

Cada unidad de compilación se llama un módulo, estas unidades constan de: declaración de funciones, definición. Nombres de archivos deben comenzar en minúscula.

Pueden usarse funciones definidas en un módulo desde otro de una de las siguientes formas:

1. Se abre el otro módulo con:

open String;;

de forma que todas las funciones y nombres del módulo quedan disponibles o 2. Se emplean funciones o nombres con el nombre del módulo como prefijo.

La excepción a estas reglas es el módulo Pervasives que siempre está abierto por defecto y que incluye la definición de los tipos primitivos y de diversdiad de funciones.

3.2.3  Lecturas recomendadas

Referencía de las librerías de [3].

3.2.4  Ejercicios de programación

  1. Escriba el módulo sust.ml en el que defina una función sust:string -> (string*string) -> Buffer.t, que aplique a una cadena todas las sustituciones que puedan hacerse de una cadena patron por un objetivo y que retorne la cadena resultante en un buffer (ver el módulo Buffer de esta guía). Por ejemplo sust "la vida, Vida" ("ida","erdad") debe retornar un buffer con contenido "la verdad, Verdad" . Escriba también la interfaz sust.mli que sólo debe hacer pública la función sust. Ayuda: puede basarse en la función rep_str del módulo lrepasa.ml de repasa.
  2. Usando el módulo Sust del punto anterior escriba el programa proto-sed.ml que después de compilado, pueda ejecutarse con dos parámetros:
    1. Una sustitución por realizar con la sintaxis s/patron/sustitucion/g.
    2. El nombre de un archivo tipo texto.
    El programa debe presentar por salida estandar el contenido del archivo texto tras aplicar la sustitución a todas las ocurrencias del patrón por la sustitución. Por ejemplo si existe un archivo carta.txt, desde la línea de comandos debe poderse ejecutar: $ proto-sed s/ida/erdad/g carta.txt
  3. En un archivo usoset.ml copie la función ord_words del archivo defprog.ml de repasa así como otras porciones que esta función requiera (por ejemplo el tipo infoword) y las funciones fpol_random, fpol_e, fpol_w y fpol_p. Escriba una función pide_datos que pida al usuario varias 4-tuplas de datos (tantas como el usuario desee), cada una consta de 3 cadenas y un número flotante, la misma función debe agregar la información dada por el usuario a una tabla de Hashing que pueda ser usada por ord_word, después debe llamar la función ord_word usando fpol_random como primer parámetro, 1.0 como segundo parámetro, la tabla de hashing como tercer parámetro y una referencia a una lista vacía como cuarto parámetro, después debe presentar la lista de 4-tuplas que recibe de la función. Experimente cambiando la función que recibe como primer parámetro (por ejemplo por fpol_e) y con base en sus experimentos determine para que se usa la función ord_word en repasa.

  4. Opcional: En que caso se usa la función fpol_pgm con la interfaz plan de repasa? Haga un ejemplo de política que la use.

1
Este ejemplo es tomado del archivo defprog.ml de repasa (ver [7])

Capítulo 4  Solución a los ejercicios

Respuestas a ejercicios de 1.1.4

  1. 3+3
  2. 7
  3. 8
  4. 11
  5. 14
  6. 6.7.4
  7. string->string->string
  8. fun x->(float_of_int x)+.0.5
  9. let dif02=fun f x->dif f x 0.3;;
  10. 0.3

Respuestas a ejercicios de programacion de 1.1.5

  1. areas.ml

Respuestas a ejercicios de 2.1.4

  1. let cuadrado = fun x -> x*x;;
  2. let cubo = fun x -> x*(cuadrado x);;
  3. factorial
  4. No hay diferencia
  5. pattern matching
  6. suma: int -> int -> (int -> int) -> int
    serie: (int -> int) -> int -> int -> int
  7. n>=i
  8. Char.chr
  9. 'a list -> 'a -> bool
  10. .

Respuestas a ejercicios de programacion de 2.1.5

  1. let listavacia l= (l=[]);;
    let rec invertir = function 
    | [] -> []
    | h::t -> (invertir t)@[h]
    ;;
    let rec esta_elemento x = function
    | [] -> false
    | h::t -> x=h || (esta_elemento x t)
    ;;
    let rec num_ocurrencias x = function
    | [] -> 0
    | h::t -> (if h=x then 1 else 0)+(num_ocurrencias x t)
    ;;
    let rec espalindrome l= ((invertir l)=l);;
    

Respuestas a ejercicios de 2.2.4

  1. true
  2. int
  3. [1]
  4. "wq"::"io"::[]
  5. int*int->int*int*(int*int)
  6. (0,[])
  7. el primero cuyo patrón concuerde
  8. ('a -> 'b) -> 'a list -> 'b list
  9. let f x=2*x in map f [3;0;1;8];;
  10. 4

Respuestas a ejercicios de programacion de 2.2.5

  1. -.
  2. let rec filter p = function | [] -> [] | h::t -> (if p h then [h] else [])@(filter p t) ;;

Respuestas a ejercicios de 2.3.6

  1. 23
  2. .
  3. .
  4. .
  5. .
  6. .
  7.  # List.length (List.filter (fun x -> x='a') ['1';'2';'a';'b';'c';'a';'c']);;
    - : int = 2
    
  8. List.map: ('a -> 'b) -> 'a list -> 'b list\\
    List.map2: ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
    
  9. .
  10. .

Respuestas a ejercicios de programacion de 2.3.7

  1. .
  2. .
  3. .

Respuestas a ejercicios de 2.4.4

  1. type trivaluada = Si | No | NoSabe;;
  2. type intinf=Ent of int | InfP | InfN;;
    let sumaii e1 e2= match (e1,e2) with
    | Ent i1, Ent i2 -> Ent (i1+i2)
    | Ent _, InfP -> InfP
    | InfP, Ent _ -> InfP
    | InfN, Ent _ -> InfN
    | Ent _, InfN -> InfN
    | _,_ -> raise (Invalid_argument "No puede sumar InfP con InfN");;
    
  3. racinf -> racinf o intinf * intinf -> intinf * intinf
  4. let mayor p1 p2 = p1.edad > p2.edad;;
    
  5. type exprar = Const of int | InvAd of exprar | Suma of exprar * exprar | 
     Resta of exprar * exprar | Prod of exprar * exprar |
     Div of exprar * exprar;;
    
  6. let rec evalua = function
     | Const i -> i
     | InvAd e -> -(evalua e)
     | Suma (e1,e2) -> (evalua e1)+(evalua e2)
     | Resta (e1,e2) -> (evalua e1)-(evalua e2)
     | Prod (e1,e2) -> (evalua e1)*(evalua e2)
     | Div (e1,e2) -> (evalua e1)/(evalua e2)
    ;;
    evalua (Prod (Suma (Const 3,Const 5),InvAd (Const 4)));;
    
  7. type 'a narb=Vacio | Nodo of 'a*(('a narb) list);;
    
  8. let rec npeso2 = 
     let rec npeso_nnod=function
     | Hoja _ -> 1
     | Sub (_,l) -> (List.fold_left (fun a s -> a+(npeso_nnod s)) 1 l)
     in
     function
     | Vacio2 -> 0
     | Nodo2 n -> npeso_nnod n
    ;;
    
  9. exception VacioComoSubarbol;;
    let rec narbBueno=function
    | Vacio -> ()
    | Nodo (_,l) -> 
     if (List.exists (fun n -> n=Vacio) l) then
      raise VacioComoSubarbol
     else
      List.iter narbBueno l
    ;;
    
  10. let npeso3 a=try
     narbBueno a;
     npeso a
    with
     VacioComoSubarbol -> -1
    ;;
    

Respuestas a ejercicios de programacion de 2.4.5

  1. let sublistas l=
            let rec sublistas_aux = function
            | [] -> ([],[])
            | h::t -> let (ht,rt)=sublistas_aux t in
              ([h]::(List.map (fun l -> h::l) ht)),ht@rt
            in
            let (l1,l2)=sublistas_aux l in
            l1@l2
    ;;
    
  2. (** Types of heuristics to test answers *)
    type heuresp=HCaseSensitive | HCaseInsensitive | HIgnoreSpaces;;
    
    (** [test_equal_answer heur w ans]  tests if the answer [ans] is equal to
            the word [w] by using the heuristics [heur].  *)
    let rec test_equal_answer heur w ans = match heur with
            | HCaseInsensitive ->
                    (String.lowercase w)=(String.lowercase ans)
            | HCaseSensitive ->
                    w=ans
            | HIgnoreSpaces ->
                    trim w = trim ans
    ;;
    
    Esta nueva definición debe moverse para que quede después de la definición de la función trim (aún mejor ponerla antes de la función parse_heur). parse_heur podría ser:
  3. 
    

Respuestas a ejercicios de 3.1.2

  1. Array.length
  2. Array.length
  3. let v = Array.create 5 (-1).
  4. let c x = Array.create x ' ';;
  5. el vector que se desea copiar
  6. abierto
  7. let x = ref 5;;
  8. # (!x)+(!z);; - : int = 0
  9. Sys_error.open.out.
  10. open_in "leer.txt";;.

Respuestas a ejercicios de programacion de 3.1.3

  1. .
  2. .
  3. .
  4. .

Respuestas a ejercicios de programacion de 3.2.4

  1. let sust s (x,y)=
     let res=Buffer.create (String.length s) in
            let rec aux nl nx =
                    if (nx>0 && nx=(String.length x)) then
      begin
       Buffer.add_string res y;
                            aux (nl+nx) 0 ;
      end
                    else if (nl>=(String.length s)) then
                            ()
                    else if (nl+nx>=(String.length s)) then
                            Buffer.add_string res (str_from s nl)
                    else
                    begin
                            let cs=String.get s (nl+nx) in
                            let cx=String.get x nx in
                            if (cs=cx) then
                                    aux nl (nx+1)
                            else
       begin
        Buffer.add_string res (String.sub s nl 1);
                                    aux (nl+1) 0 ;
       end;
                    end
            in aux 0 0 ;
     res
    ;;
    


Bibliografía

[1]
Emmanuel Chailloux, Pascal Manoury, and Bruno Pagano. Developing applications with objective caml. http://caml.inria.fr/oreilly-book/html/index.html, 2002.

[2]
John Harrison. Introduction to functional programming. http://www.cl.cam.ac.uk/Teaching/Lectures/funprog-jrh-1996/index.html, 1996.

[3]
Xavier Leroy. The objective caml system release 3.07. http://caml.inria.fr/ocaml/htmlman/, 2003.

[4]
Vladimir Támara Patiño. Manta 3: Formal description. http://manta.sourceforge.net/doc/formal3/, 1999.

[5]
Basile Starynkevitch. Ocamljitrun. http://cristal.inria.fr/ starynke/ocamljit.html, 2004.

[6]
Gerd Stolpmann. Markup - a library to parse and validate xml in ocaml. http://ocaml-programming.de/markup/, 2004.

[7]
Vladimir Támara. repasa - herramientas y formatos para crear y repasar contenidos. http://structio.sourceforge.net/repasa, 2004.

Index


This document was translated from LATEX by HEVEA.