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.
-
LOGRO: Identifica características de Ocaml
- LOGRO: Maneja conceptos de programación funcional pura
- LOGRO: Maneja conceptos de tipos
1.1 Vistazo de programación funcional y Ocaml
Indicadores de logro:
-
INDICADOR: Puede reducir expresiones que incluyan funciones y aplicación funcional
- INDICADOR: Asocia expresiones a nombres empleando let
- INDICADOR: Define funciones sencillas que emplean tipos básicos
- INDICADOR: Deriva tipo de expresiones sencillas que incluyan tipos básicos
- INDICADOR: Emplea la documentación de Ocaml
- INDICADOR: Emplea el entorno interactivo de Ocaml
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:
-
ocamlc que genera bytecode utilizable por la máquina Zinc y portable
a toda arquitectura donde se ha portado la máquina Zinc (incluyendo
x386/OpenBSD, x386/Linux, x386/Windows 95-NT, Mac, Digital, Solaris, IRIX)
- ocamlopt que genera binarios para diversas arquitecturas
(incluyendo procesadores i386 con sistemas operativos OpenBSD y Linux).
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
-
¿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;;
-
¿Qué valor resulta tras hacer la reducción completa de la siguiente expresión?:
(fun var1 -> (fun var2 -> (var1*var1)+var2)) 2 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 ;;
-
¿Qué valor resulta tras reducir por completo la siguiente aplicación?:
(fun f -> 1+(f 5)) (fun y -> 2*y);;
-
¿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;;
-
Consulte en la documentación de Ocaml la lista de operadores. ¿En que sección
del manual está dicha lista?
-
¿Cual es el tipo de la siguiente expresión?
fun s t -> s t
-
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.
-
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.
-
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
-
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
-
LOGRO: Maneja funciones y expresiones
2.1 Introducción
-
INDICADOR: Reconoce los tipos básicos que utiliza Ocaml.
- INDICADOR: Define funciones que emplean tipos agregados como tuplas y listas.
- INDICADOR: Define y emplea funciones recursivas.
- INDICADOR: Define funciones que emplean colecciones de datos como tuplas y listas.
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):
-
- : int -> int o - que retorna inverso
aditivo
- succ: int-> int sucesor
- pred: int-> int predecesor
- (+) : int -> int -> int suma
- (-) : int -> int -> int resta
- (*) : int -> int -> int multiplicación
- (/) : int -> int -> int división entera. Lanza la excepción
Divsion_by_zero si el denominador (i.e segundo parámetro) es 0.
Si tanto numerador
como denominador son positivos retorna el mayor entero menor o igual
a la división. Si ambos operandos son negativos da lo mismo que
la división entera entre sus valores absolutos, si uno de los operandos
es negativo da lo mismo que la división entera entre valores absolutos
pero con signo negativo.
- mod: int -> int -> int residuo de división entera. Si
el denominador (segundo parámetro) es 0 lanza la excepción
Divsion_by_zero. El comportamiento con negativos se presenta
con un ejemplo: 5 mod 2 es 1; -5 mod 2 es -1;
5 mod -2 es 1 y -5 mod -2 es -1. Note que se
cumple: x = (x / y) * y + (x mod y), con
abs(x mod y) <= abs(y)-1.
- abs: int -> int que retorna valor absoluto.
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:
-
land : int -> int -> int conjunción lógica (da 1 sólo si
los dos bits son 1).
- lor : int -> int -> int disyunción (da 0 sólo si los dos
bits son 0).
- lxor : int -> int -> int o exclusivo (da 1 sólo si los dos
bits son diferentes).
- lnot : int -> int negación
- lsl : int -> int -> int corrimiento a la izquierda, el segundo
parámetro indica cuantas posiciones debe correrse el primer parámetro.
El segundo parámetro debe ser mayor o igual a 0 y menor que 32 en
plataformas de 32 bits o que 64 en plataformas de 64 bits (valores fuera
de este rango dan resultados no especificados).
- asr : int -> int -> int corrimiento a la derecha, el segundo
parámte indica el número de posiciones (la misma convención que lsr.
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:
-
0.3
- 2E1 que equivale a 20.0
- -2.2e-1 que equivale a -0.22
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:
-
-. o -. que retorna inverso aditivo (prefijo)
- +. suma (infijo)
- -. resta (infijo)
- *. multiplicación (infijo)
- /. división (infijo)
- ** exponenciación (infijo)
Las siguientes son funciones incluidas en la librería básica2:
-
sqrt: float -> float raíz cuadrada
- exp: float -> float exponenciación
- log: float -> float logaritmo natural
- log10: float -> float logaritmo en base 10
- cos: float -> float coseno
- sin: float -> float seno
- tan: float -> float tangente
- acos: float -> float arcocoseno
- asin: float -> float arcoseno
- atan: float -> float arcotangente (para obtener ángulos precisos, de ser posible,
mejor emplear atan2 que se describe más adelante).
- cosh: float -> float coseno hiperbólico
- sinh: float -> float seno hiperbólico
- tanh: float -> float tangente hiperbólica
- ceil: float -> float techo, i.e el menor entero mayor o igual al argumento
- floor: float -> float piso, i.e el mayor entero menor o igual al argumento
- abs_float: float -> float valor absoluto de un flotante
- atan2: float -> float -> float que recibe cateto opuesto (y) y
cateto adyacente (x) y retorna el ángulo (teniendo en cuenta signos de
x e y).
- mod_float: float -> float -> float resto que queda tras
dividir el primer argumento (a) entre el segundo (b).
Retorna a -. n *. b donde n es el
piso de a /. b.
- frexp : float -> float * int retorna una pareja con la parte
significativa (x) y el exponente (n) del argumento. Cuando el argumento
es x=0 y n=0. Si el argumento (f) no es cero se cumple:
f=x2n y 0.5£ x £ 1.0.
- ldexp: float -> int -> float recibe dos argumentos x:float y n:int y retorna el flotante x2n.
- modf: float -> float * float returna una pareja con la parte
fraccional y entera del argumento.
- float: int ->float o float_of_int: int -> float retorna
el entero que reciben como número de punto flotante.
- truncate: float -> int y int_of_float que trunca la parte
decimal del argumento que recibe. El comportamiento es insospechado si
al truncar se sale del rango representable con enteros.
También pueden emplearse las constantes:
-
infinity que representa infinito positivo
- neg_infinity que representa infinito negativo
- nan (abrevia Not a Number), representa un valor que no es número
- max_float que corresponde el máximo flotante representable
- min_float que corresponde al menor flotante positivo que
no es cero y que no está denormalizado.
- epsilon_float mínimo flotante positivo x tal que
1.0 +. x <> 1.0.
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:
-
int_of_char : char -> int
- char_of_int : int -> char
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.
-
x = y igualdad3
- x <> y x no es igual a y
- x < y x menor que y
- x <= y x menor o igual que y
- x >= y x mayor o igual que y4
- x > y x mayor que y
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:
-
not : bool -> bool que es negación (prefijo)
- (&&) : bool -> bool -> bool que es conjunción corta, si el
primer argumento evalua a false no evalua el segundo sino que retorna false.
- (||) : bool -> bool -> bool que es disyunción corta, si el
primer operador evalua a true retorna true
sin evaluar el segundo.
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:
-
fst : 'a * 'b -> 'a que reotrna el primer elemento de una pareja.
- snd : 'a * 'b -> 'b que retorna el segundo elemento de una pareja.
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:
-
La lista vacía se representa de la forma [ ] .
- Una lista se puede representar entre paréntesis y los elementos separados
con punto y coma (;). Ejemplos:
# [1;2;3];;
- : int list = [1; 2; 3]
# ["Hola";"mundo";"ocaml"];;
- : string list = ["Hola"; "mundo"; "ocaml"]
- Utilizando el operador :: se construyen listas especificando
cabeza y cola por ejemplo:
# "Hola"::"mundo"::"ocaml"::[ ];;
- : string list = ["Hola"; "mundo"; "ocaml"]
# 1::[2;3;4;5;6];;
- : int list = [1; 2; 3; 4; 5; 6]
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
-
Caso trivial: es aquel que si se cumple devuelve un resultado que
puede ser final o parte de la solución.
- Caso recursivo: es aquel que llama de nuevo a la función recursiva.
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
-
Asocie el nombre cuadrado a una función cuyo tipo sea int -> int y que calcule el cuadrado de un entero dado.
-
Utilizando la función cuadrado del punto anterior, realice una función que
calcule el cubo de un entero dado.
-
¿Qué nombre debería recibir la siguiente función?
let rec nombre = function
0 -> 1
| n -> n*(nombre (n-1));;
-
¿Qué diferencia tiene la anterior función a la siguiente ?
let rec nombre n =
if n=0 then 1
else n*(nombre (n-1));;
-
¿Cómo se llama el mecanismo que permite que una función se comporte como un
switch case ? De un ejemplo:
-
¿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));;
-
¿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))
;;
-
¿Qué función permite pasar un número ASCII a caracter ?
-
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 ?
-
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
-
En un archivo de nombre tadlista.ml construya las siguientes funciones:
-
listavacia dice si una lista es vacia o no (true o false).
- invertir devuelve una lista dada invertida.
- esta_elemento dice si un elemento esta en una lista dada.
- num_ocurrencias calcula el número de veces que esta un
elemento en una lista dada.
- lista_ordenada dice si una lista esta ordenada o no.
- espalindrome dice si una lista es palindrome (se lee igual
de derecha a izquierda que de izquierda a derecha).
2.2 Funciones recursivas
-
INDICADOR: Emplea el condicional if.
- INDICADOR: Emplea funciones para realizar operaciones básicas de entrada/sálida (e.g print_endline)
- INDICADOR: Emplea correspondencia de patrones (pattern matching).
- INDICADOR: Emplea recursión mutua y definiciones locales.
- INDICADOR: Emplea comentarios que pueden ser usados por Ocamldoc.
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 :
-
print_endline: string -> unit que imprime por
salida estándar la cadena que recibe con un salto de línea al final.
- prerr_endline: string -> unit análoga a la anterior
pero que imprime por error estándar.
- print_char: char -> unit, print_string: string -> unit,
print_int: int -> unit, print_float: float -> unit que
imprimen en salida estándar valores de los tipos presentados
(sin salto de línea). Así como las funciones análogas para error
estándar que comienzan con prerr en lugar de print.
- print_newline: unit -> unit y prerr_newline: unit -> unit
que hacen en un salto de línea en salida estándar o error
estándar7.
- read_line: unit-> string que lee de entrada estándar hasta
encontrar un carácter de cambio de línea y lee la cadena leída sin el
cambio de línea.
- read_int: unit-> int y read_float: unit -> float que
leen una línea de entrada estándar y la convierten a entero y flotante
respectivamente. Si la línea leída no puede convertirse al tipo esperado,
estas funciones lanzan una excepción (que se explicará como manejar más
adelante).
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ón1 -> expr1
| patrón2 -> expr2
:
| patrónn -> 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 nomfun2 = expr2
:
and nomfunn = 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:
-
(b texto) para emplear negrillas
- (i texto) para emplear itálicas
- (c texto) para centrar
- [texto] estilo de letra de código fuente
- @author texto para establecer autor (de módulo o función)
- @version texto para establecer versión (de módulo o función)
- @param id texto para describir parámetro id de una función
- @return texto para describir valor retornado por una función
- @see url para referenciar un URL (también puede ser un archivo).
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
-
Las funciones de error estándar y salida estándar las encuentra en la
documentación del módulo Pervasives de [3].
- Puede consultar sobre los fundamentos de correspondencia de patrones
(pattern matching en inglés) en la sección 5 de [4].
- La sintaxis completa de patrones puede consultarla en la sección
6.6 de [3].
- Todas las posibilidades de Ocamldoc se documentan en el capítulo
15 de [3].
2.2.4 Ejercicios para afianzar teoría
-
¿A que evalúa la expresión if true=false then false else true ?
-
¿Cual es el tipo de la expresión if x<y then x else y+1 ?
-
¿Con qué objetivos concuerda el patrón [1]?
-
Escriba un patrón que concuerde con todas las listas con los elementos
"wq" e "il" usando solo los constructores :: y []
-
¿Cual es el tipo de la siguiente función (presentada en la lectura)?
function ((a:int),b) as c -> (a,b+1,c)
-
¿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.
-
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.
-
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?
-
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].
-
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:
-
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:
-
String.get s i que retorna el i-ésimo carácter
de la cadena s (el primer carácter tiene índice 0).
- String.sub s i l que retorna la subcadena de s de
longitud l que comienza en el i-esimo carácter.
- String.length s que retorna la longitud de la cadena s.
- int_of_string s que convierte la cadena de dígitos s a
entero (si no puede convertirse lanza una excepción).
-
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
-
INDICADOR: Emplea funciones anónimas.
- INDICADOR: Identifica, emplea y construye funciones polimórficas.
- INDICADOR: Emplea funciones de orden superior
- INDICADOR: Maneja el concepto de Curried.
- INDICADOR: Conoce y aplica la transferencia referencial
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:
-
compare : 'a -> 'a -> int que retorna 0 si x=y, un
entero positivo si x>y y un negativo en otro caso.
- min : 'a -> 'a -> 'a que retorna el menor de los dos
argumentos.
- max : 'a -> 'a -> 'a que retorna el mayor de los dos
argumentos.
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
-
¿A qué evalua la siguiente función?
(fun x y -> (if x>y then x else y)) 23 4;;
-
Escriba la función anónima anexar que recibe dos listas y a la primera
lista le una la segunda.
-
Escriba la función curry que recibe una función que recibe una pareja
(x,y) y que retorne la currificación
-
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.
-
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 ?
-
¿Cuál función de orden superior me sirve para obtener de una lista de enteros el número menor ?
-
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).
-
¿Cuál es el tipo de cada una de las funciones
List.map y List.map2?
-
En cuanto a funcionalidad ¿cuál es la diferencia entre List.map y List.map2?
-
¿Una función de orden superior puede ser polimórfica?
2.3.7 Ejercicios de programación
-
En un archivo llamado funciones.ml escriba las siguiente funciones
-
Función insertar, que, dados una lista no
decreciente ys y un elemento y, devuelva una lista no
decreciente igual a ys más el elemento
y insertado en el lugar correspondiente.
- Función unico, que, dada una cadena devuelva una
cadena que contenga exactamente los elementos que se encuentran
solamente una vez en la cadena dada.
Por ejemplo: unico "Cuales son las letras unicas en esta frase?" debe
dar "oicf?" .
-
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
-
el número 156 representado en entero grande es [1;5;6]
- los números 156 y 32 representados comon enteros grandes
son [1;5;6] y [3;8]. El resultado de la suma es [1;9;4]
- al restar [1;5;0;0;0] de [5;5;5;0] se obtiene [9;4;5;0]
Opcional: Idee una representación para enteros negativos e implemente las
mismas funciones con la nueva representación.
-
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
-
INDICADOR: Define nuevos tipos
- INDICADOR: Emplea nuevos tipos y excepciones
- INDICADOR: Depura y rastrea programas Ocaml
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_tipo1 | 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:
-
Match_failure of (string * int * int)
Assert_failure of (string * int * int)
Invalid_argument of string puede usarse con función
invalid_arg: string -> 'a
Failure of string puede emplearse con función
failwith: string -> 'a
Not_found
Out_of_memory
Stack_overflow
Sys_error of string
End_of_file
Division_by_zero
Sys_blocked_io
Undefined_recursive_module of (string * int * int)
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
-
Defina un tipo variante trivaluada cuyos constructores sean Si,
No y NoSabe.
-
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
-
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 ?
-
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.
-
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).
-
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)
...
;;
-
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)
;;
-
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.
-
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).
-
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
-
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]]
-
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
-
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í:
-
- prefijo
- * / con asociatividad a la izquierda
- + - con asociatividad a la izquierda
- 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
-
LOGRO: Maneja tipos mutables
- LOGRO: Maneja estructuras de control tipicamente imperativas
- LOGRO: Emplea funciones con efectos laterales
- LOGRO: Emplea funciones de diversas librerías de Ocaml
3.1 Características imperativas
-
INDICADOR: Emplea funciones con características imperativas.
- INDICADOR: Emplea estructuras de datos modificables.
- INDICADOR: Identifica y construye funciones utilizando vectores.
- INDICADOR: Identifica y construye funciones utilizando cadenas.
- INDICADOR: Identifica y construye funciones utilizando registros con campos mutables.
- INDICADOR: Identifica y construye funciones utilizando referencias.
- INDICADOR: Implementa funciones utilizando canales de entrada-salida.
- INDICADOR: Identifica y construye funciones utilizando estructuras de control como secuencias o ciclos.
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:
-
complica el sistema del tipo de la lengua, rechazando ciertos
programas que de otra manera serían considerados correctos
- tiene que no perder de vista la representación de la memoria
y el orden de los cálculos.
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:
-
Estructuras de datos modificables como vectores, cadenas,
campos mutables y referencias.
- Operaciones de entrada y salida.
- Estructuras de control como secuencias y ciclos.
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:
expr1 . ( expr2 )
y para modificar una posición:
expr1 . ( 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:
-
create que crea un arreglo de un tamaño dado con un valor inicial dado
- length retorna la longitud de un vector
- append que concatena dos vectores.
- copy para copiar un vector a otro.
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:
expr1 . [ expr2 ]
Los elementos de una cadena pueden ser modificados físicamente:
expr1 . [ 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:
expr1 . 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>
-
input_line ic lee por el canal de entrada ic todos los
caracteres hasta el primer retorno del carro o el fin del archivo,
y los devuelve en la forma de una lista de caracteres (excepto el retorno del
carro).
- entrada ic s p l procura leer 1 carácter del canal de la
entrada ic y los almacena en la lista s iniciando con el carácter p th.
El número de los caracteres leídos son retornados.
- output oc s p l escribe en el canal de salida
oc parte de la lista s , comenzando en el p-esimo -
con longitud l.
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.
expr1 ; ... ; 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
-
La función que me permite conocer la longitud de un vector es
-
La función que me permite conocer el número de filas de una matriz es
-
Construya un vector v de enteros, de longitud 5 donde los campos del
vector este inicializados con -1.
-
Cree una función c que reciba un entero x y retorne
un nuevo vector de caracteres, con x caracteres, cada uno
un espacio.
-
La función Array.copy recibe como parametro.
-
Escriba un tipo de dato que utilice dos registros mutables
y uno no-mutable. Explique el funcionamiento.
Construya una referencia x al valor 5.
-
Construya una referencia z al valor -5 y calcule el
resultado de sumar los valores referenciados por x y por z.
-
Para abrir un archivo si este no existe que excepción se ejecuta?
-
Escriba una función que abra un canal de entrada, leyendo del archivo
leer.txt
.
3.1.3 Ejercicios de programación
-
En un archivo vectores.ml escriba las siguientes funciones para
manejo de arreglos de tipo entero:
-
creararreglo : dada la longitud y el entero inicial.
- invertira : dado un arreglo retorna uno nuevo con los mismos elementos del que recibe pero invertidos.
- sumararreglo : suma dos arreglos (al estilo entero grande), retorna un nuevo arreglo con la suma.
- prefijoa : recibe un arreglo x y un arrreglo z y dice si z es prefijo de x.
- sufijoa : recibe un arreglo x y un arrreglo z y dice si z es sufijo de z.
-
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).
-
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
-
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
-
INDICADOR: Conoce diversas funciones de las librerías de Ocaml
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
-
String.length : string -> int retorna la longitud (número de
caracteres) de la cadena dada.
- String.get : string -> int -> char
String.get s n retorna el carácter numero n en la cadena
s. El primer caracter es el número 0. El último caracter
es el número (String.length s) - 1. Lanza Invalid_argument
si n está fuera del rango 0 a (String.length s) - 1.
También puede escribir s.[n] en lugar de String.get s n.
- String.set : string -> int -> char -> unit
String.set s n c modificar la cadena s,
remplazando el caracter número n por c.
Lanza Invalid_argument si n está fuera del rango
0 a (String.length s - 1).
También puede escribir s.[n] <- c en lugar de
String.set s n c.
- String.create : int -> string
String.create n retorna una nueva cade