La Gaceta de Linux ...¡ haciendo a Linux un poco más divertido !
Traducción al español por Rodolfo Franco
el día 21 de Mayo de 2004, para La Gaceta de Linux
Object Caml es un lenguage del tipo de ML. Para los no-gurus: es un lenguage funcional que también puede ser programado en una manera no funcional y orientada a objetos.
Este lenguage es realmente fácil de aprender. Es potente y me sigue impresionando con su velocidad. Los programas escritos en este lenguage son casi siempre estables por defecto. No hay fallos de segmento, sólo bucles infinitos ocasionales para los programadores que todavía se aferran a programar sus propios bucles. En realidad no es necesario programar la mayoría de los bucles, ya que las librerías contienen funciones estándar que son suficientemente buenas en el 99% de los casos. Entonces intente utilizar esas funciones: realmente compensa en términos de estabilidad de sus programas, y, a no ser que usted tenga un conocimiento íntimo del funcionamiento interno del lenguage, tienden a estar mejor optimizadas.
El lenguage se puede obtener del sitio caml.inria.fr. Aquí, ellos proveen RPMs para las distribuciones de RedHat 7.2/8.0/9 y Mandrake 8.0. También están disponibles los binarios para MS Windows, pero no todas las funciones de librería de Unix funcionarán ahí, por alguna razón misteriosa. El archivo tar con las fuentes compila sin problemas para mi. Solamente que el archivo makefile tiene un formato inusual:
# ./configure # make world; make opt; make install
Las librerías normales incluyen muchas estructuras de datos usables como árboles balanceados, tablas de dispersión (hash), y flujos (streams). Su versión de los archivos de cabecera (archivos .mli) contienen toda la documentación básica que usted necesita, y son convertidos directamente a HTML y publicados en la Web en el manual de OCaml. Este manual no es muy usable para estudiar este lenguage, así que trataré de explicar aquí algunas de las construcciones básicas del lenguage. Esto es solamente para darle una impresión de la potencia de este lenguage.
Ahora algunos ejemplos de la vida real. He escrito un programa para ayudar a administrar una computadora. Es un subconjunto de un buscador de archivos normal, pero es una herramienta de línea de comandos y muy rápida. Ayuda a localizar archivos grandes, no recientemente usados para ser eliminados del sistema. Recorre el árbol de directorios y muestra el contenido en diferentes formatos.
Todo módulo en OCaml tiene su propio espacio de nombres. Las definiciones específicas pueden ser encontradas añadiendo el nombre del módulo, con el primer caracter en mayúsculas. También se puede cambiar el espacio de nombres del programa actual para incluir un módulo total. Normalmente, sólo el módulo estándar 'pervasives.mli' es incluído en el espacio de nombres por defecto. El programa de ejemplo 'show.ml' empieza con:
open Basics open Unix open Unix.LargeFile
Esto incluye mi propio juego de funciones 'básicas' y 2 librerías estándar: 'Unix' y 'Unix.LargeFile'. Un módulo consiste en 2 archivos. El primer archivo para exportar las definiciones 'modulo.mli' (como el archivo .h de C), y el segundo para el código real (el archivo 'modulo.ml'). El programa usa la función 'string_sub' que provee una versión a prueba de tontos de la función estándar de 'String.sub' (del módulo string.mli). El archivo basics.mli contiene las líneas:
val string_sub: string -> int -> int -> string (** Obtiene una subcadena de una [cadena(string)] desde la posición [desde(int)] con la [longitud(int)]. Esta es la misma función que String.sub, pero nunca lanzará una excepción. Si el valor [desde] es negativo se cuenta desde la parte derecha de la cadena. *)
Esto da la definición y la descripción de esta función. Hay un generador de documentación automática (ocamldoc) que lee archivos .mli y escribe archivos .html como documentación básica. Los comentarios normales empiezan con (* pero el generador de documentación sólo escribe comentarios que comienzan con (** en los archivos .html. Este documento contiene enlaces a la documentación de los módulos utilizados. Esta documentación es realmente útil para empezar a programar con ocaml. Los archivos .mli estan todos incluídos en la distribución, pero el manual completo y el libro puede ser descargado del sitio Web caml.inria.fr
La función es seguida por su tipo. Recibe 3 parámetros y devuelve una cadena. Normalmente necesitamos escribir 'Basics.string_sub' para usar esta función. Pero después de la instrucción 'open Basics' sólo 'string_sub' es suficiente.
Ahora, volviendo nuevamente al programa principal. La primera función es 'gettype'. Intentará devolver el tipo de un archivo. El tipo del archivo es definido como la parte del nombre a continuación del último '.'. Cuando no hay punto, el tipo es desconocido y devuelve vacío.
let gettype file = try let pos = String.rindex file '.' in String.sub file (pos+1) (String.length file-pos-1) with Not_found -> "" ;;
Esta función sólo usa funciones estándar. Primero, atrapa la excepción Not_found en el código 'try' 'with Not_found -> ""'. Todas las otras excepciones serán pasadas al llamador para que las maneje, y pueda posiblemente detener el programa principal. La variable local pos es asignada con el resultado de la función rindex. Esta función es también la razón para atrapar la excepción; de otra forma, el programa principal podría detenerse en el primer archivo encontrado sin '.' en él. Las variables locales pueden ser declaradas en cualquier lugar dentro de ocaml con 'let <variable> = <valor> in <código>'. Después de la finalización del código dado, la variable está fuera de ámbito y será olvidada. Los datos serán pasados al recolector de basura para ser eliminados de la memoria. Las llamadas a funciones normalmente utilizan paréntesis. La llamada a la función a'String.sub' recibe 3 parámetros: la cadena 'file', el entero '(pos+1)' y el entero '(String.length file-pos-1)'. El último parámetro llama a la función 'String.length' con un único parámetro 'file'. Entonces, las funciones están ansiosas por sus parámetros; los paréntesis son necesarios sólo cuando los parámetros son rellenados con cálculos.
También '(+)' y '(-)' son funciones del módulo pervasives. Es muy fácil definir tus propios operadores; sólo agregar paréntesis alrededor de su definición, y están listos.
La siguiente rutina 'filesize' en el código de ejemplo es mucho más larga, pero presenta en gran medida las sub-funciones y las sentencias 'if <bool-expr> then <expr> else <expr>'. Esta función crea una cadena a partir de un número int64 para que un humano pueda leer los tama�os de archivos y directorios. Los tipos de los parámetros no son dados normalmente; son determinados por ocaml basándose en su uso. Cuando algo no está claro, el compilador o interprete se quejará de eso antes de ejecutar el código.
let filesize s =
let tostr f =
if f>9.9 then
string_of_int (int_of_float (f +. 0.5))
else
let res = string_of_float (floor (f *. 10.0 +. 0.5) /. 10.0) in
if String.length res=2 then
res ^ "0"
else
res
in
let bytes = Int64.to_float s in
if bytes > 512.0 then
let kb = bytes /. 1024.0 in
if kb > 512.0 then
let mb = kb /. 1024.0 in
if mb > 512.0 then
let gb = mb /. 1024.0 in
tostr gb ^ " Gb"
else
tostr mb ^ " Mb"
else
tostr kb ^ " kb"
else
Int64.to_string s
;;
La librería estándar tiene una serie de funciones de conversión. Estas funciones normalmente siguen la forma de 'int_of_float' y 'string_of_float'. Los tipos específicos como 'Int64' usan notaciones abreviadas como 'Int64.to_float'. Las concatenaciones de cadenas se hacen con el operador '(^)'. Normalmente, las funciones son definidas para un tipo específico, por lo que hay nuevos juegos de funciones aritméticas para los float como '(+.)', '(*.)' and '(/.)'. La sub-función 'tostr' incluye algunos cálculos extras para cambiar algo de la forma '5. Gb' a una forma mas bonita como '5.0 Gb'.
La siguiente función, 'converttime', convierte una cadena a un float. OCaml usa floats para las fechas por 2 razones. La primera es para evitar posibles problemas de a�o 2000, y puede ser usado también para medidas de menos de un segundo. La función acepta acrónimos ingleses para los nombres de los meses. Entonces, presentemos la lista y el par para crear una traducción de acrónimos a números.
let month = [("jan", 0); ("feb", 1); ("mar", 2); ("apr", 3); ("may", 4); ("jun", 5);
("jul", 6); ("aug", 7); ("sep", 8); ("oct", 9); ("nov", 10); ("dec", 11)]
;;
Esta lista es totalmente estática, y puede ser usada por la función estándar List.assoc para convertir una cadena en el correspondiente número.
let converttime str =
try
begin match
if str>"a" && str<"z" then
( int_of_string (string_sub str (String.rindex str ' '+1) 99),
List.assoc (string_sub str 0 3) month,
1
)
else
( int_of_string (string_sub str 0 (
try String.index str '-' with Not_found -> 99
)),
( try let pos=String.index str '-'+1 in
int_of_string (string_sub str pos (
try String.index_from str pos '-'-pos with err -> 99
))-1
with err -> 0
),
( try let pos=String.index str '-'+1 in
int_of_string (string_sub str (String.index_from str pos '-'+1) 99)
with err -> 1
)
)
with (yr,mn,md) ->
(* print_string ("Last access before: "^
string_of_int (if yr<50 then yr+2000 else if yr<100 then yr+1900 else yr)^"-"^
string_of_int (mn+1)^"-"^
string_of_int md^"\n");
*)
fst (mktime
{ tm_sec = 0; tm_min = 0; tm_hour = 0;
tm_mday = md; tm_mon = mn;
tm_year = if yr<50 then yr+100 else if yr<100 then yr else yr-1900;
tm_wday = 0; tm_yday = 0; tm_isdst = false
})
end with err ->
print_string ("Cannot decipher this date string '" ^ str ^ "'\n"); max_float
;;
La nueva operación en esta función es 'match <expr> with <template> -> expr'. Esta es una de las instrucciones de ocaml más versátiles. Puede ser usada para examinar los contenidos de variables y obtener la información que se necesite de ellas. Esta función crea la tupla (a�o, mes, dia-del-mes) a partir de dos notaciones de fecha diferentes. Para depurar esta función se incluye la instrucción 'print_string' pero comentada para evitar llenar de información innecesaria la salida del programa. Normalmente hay algún mecanismo de log para hacer que los mensajes extra sean opcionales para el usuario. La instrucción 'print_string' muestra la notación ISO de la fecha dada; crea un a�o de 4 digitos y da un número de mes, tomando Enero como 1 en lugar del uso interno de Unix en el que Enero es el 0.
Eata función también muestra el uso de 'try <expr> with err -> <expr>' que atrapa toda posible excepción y completa la variable 'err' con los detalles de la excepción. Esta This función puede generar una buena cantidad de excepciones diferentes excepciones, y francamente no estoy muy interesado en los detalles. La rutina sólo se queja al usuario sobre la cadena de fecha dada y nada más. Devuelve el mayor float posible para incluir cualquier nombre de archivo.
La principal función estándar es 'Unix.mktime' . Espera recibir un registro lleno con números acerca de la hora actual. Esta función devuelve un par con el float necesario y un registro normalizado. Con la función de pervasives fst devuelve solamente el primer parámetro del par.
El ';' antes de 'max_float' indica que la expresión devuelve un float, pero las instrucciones anteriores al ';' son calculadas primero. Esta es la primera instrucción no-funcional dentro del código de ejemplo. OCaml no es estrictamente funcional, pero tiene toda la potencia de otros lenguajes funcionales.
Ahora es el momento de tratar con estructuras de datos reales que son construidas dinámicamente y que pueden ser usadas de muchas formas diferentes.
type entrytype =
| Dir of entry list (* directory with a list of files *)
| File of string (* a file inside a directory *)
and
entry = {
mutable e_name: string; (* name of a file or directory *)
e_type: entrytype; (* what type is this together with type
related information *)
e_atime: float; (* last access time *)
e_size: int64; (* size of the file or size of all the matching
files in the directory *)
}
La sentencia 'and' es usada para unir las dos definiciones. Son creadas al mismo tiempo por lo que 'entrytype' puede incluir 'entry' y viceversa. 'entrytype' puede consistir de cualquiera de estas 2 cosas: un directorio con una lista de entradas o un archivo con su tipo de archivo. La entrada de directorio tiene un nombre mutable. Esto puede ser utilizado más tarde para cambiar el nombre del archivo por la ruta completa del archivo.
Como con ANSI C, los operadores para álgebra Booleana son '(&&)' y '(||)'.
let rec dirwrite el depth sortfn =
List.iter (
fun e ->
match e.e_type with
| Dir lst ->
if e.e_size <> Int64.of_int 0 then begin
print_string ((String.make (depth*2) ' ') ^ "Directory " ^
e.e_name ^ " = (" ^ filesize e.e_size ^ ")\n");
dirwrite lst (depth+1) sortfn
end
| File string ->
print_string ((String.make (depth*2) ' ') ^ e.e_name ^
" (" ^ filesize e.e_size ^ ")\n")
) (List.sort sortfn el)
;;
Aquí está la función recursiva ('rec') 'dirwrite' que recorre un árbol dado 'el' y que escribe el resultado a la salida estándar. El parámetro 'depth' indica la cantidad de espacios para escribir una estructura de árbol con los nombres de archivos. La función ordena todas las listas con la función dada 'sortfn'. La nueva estructura del lenguaje aquí es 'fun <parm-1> ... <parm-n> -> <expr>'. Esta construcción crea una función sin nombre. Los parámetros de esta construcción con forma de función pueden ser usados como un patrón para corresponder pares.
Esta función suprime los directorios que tienen 0 bytes de tamaño para reducir la salida inútil.
(* List of global variables *)
let min_size = ref (Int64.of_int 0) and (* minimum size of a file in bytes *)
last_access = ref max_float and (* last access time in seconds since 1970 *)
has_type = ref "" and (* type of file to show or empty to
show all *)
name_match = ref "" and (* regular expression to match the filename
with; empty is show all *)
name_regexp = ref (Str.regexp "") and (* pre-calculated regular expression *)
no_symlinks = ref false (* don't follow symbolic links to
directories *)
;;
Esta es una lista de variables que pueden ser cambiadas a causa de una construcción 'ref <expr>'. Normalmente, las definiciones son simplemente una etiqueta para sus contenidos. Estas definiciones son punteros a una dirección de memoria y pueden ser leidos con '!<variable>' y escritos con '<variable> := <expr>'. Los parámetros dados al programa pueden hacer cambios a la forma en que los archivos son leídos.
let rec dirread path =
let list = ref [] and
size = ref (Int64.of_int 0) in
try
let dh = opendir path in
while true do
let file = readdir dh in
if file<>".." && file<>"." && file<>"CVS" && String.sub file 0 1 <> "." then
let s=stat (path^"/"^file) in
if s.st_kind = S_DIR &&
(not !no_symlinks || (lstat (path^"/"^file)).st_kind <> S_LNK)
then
let dir = dirread (path^"/"^file) in
list :=
{ e_name = file;
e_type = Dir (fst dir);
e_atime = s.st_atime;
e_size = snd dir
} :: !list;
size := Int64.add !size (snd dir)
else if
(!has_type = "" || gettype file = !has_type) &&
s.st_size > !min_size &&
s.st_atime < !last_access &&
(!name_match = "" || Str.string_match !name_regexp file 0)
then begin
list :=
{ e_name = file;
e_type = File (gettype file);
e_atime = s.st_atime;
e_size = s.st_size;
} :: !list;
size := Int64.add !size s.st_size
end
done;
(!list, !size)
with
| End_of_file -> (!list, !size)
| Unix_error (EACCES, err, parm) -> (!list, !size)
;;
Las siguientes funciones son introducidas en la función 'dirread':
let rec flat el path =
List.fold_right (
fun e ls ->
match e.e_type with
| Dir lst -> flat lst (path ^ "/" ^ e.e_name) @ ls
| File string ->
e.e_name <- (path ^ "/" ^ e.e_name);
e :: ls
) el []
;;
Esta prolija y pequeña rutina 'flat' aplana el árbol 'el'. Toma cada archivo de cada rama y crea una lista simple con todos los archivos encontrados. Esto es hecho con una de las rutinas estándar más versátiles dentro de ocaml: la rutina 'List.fold_right' . Esta rutina introduce una máquina de estado (scarab) que recorre la lista y opera en cada elemento encontrado. Produce una nueva estructura (droppings) como resultado -- en este caso, una lista plana.
La construcción '<record-field> <- <expr>' cambia los contenidos de un campo de un registro mutable. Sin los campos mutables, se puede mutar registros sólo creando uno nuevo con montones de campos heredados del antiguo. Utilizar esta notación es un atajo para eso.
let name_order a b =
compare a.e_name b.e_name
;;
let type_order a b =
let typea = match a.e_type with Dir ls -> "dir" | File tp -> tp and
typeb = match b.e_type with Dir ls -> "dir" | File tp -> tp in
if compare typea typeb = 0 then
compare a.e_name b.e_name
else compare typea typeb
;;
let atime_order a b =
compare a.e_atime b.e_atime
;;
Un juego de funciones de ordenamiento para usar dentro de 'dirwrite'. La función 'compare' devuelve los ampliamente usados valores -1 para menor que, 0 para igual y +1 para mayor que.
let dir = ref "." and
sort = ref name_order and
show = ref 0
in
Arg.parse [
("-t",Arg.Unit (fun () -> sort := type_order),
"Sort by type and filename");
("-l",Arg.Unit (fun () -> sort := atime_order),
"Sort by last access time");
("-n",Arg.Unit (fun () -> show := 1),
"List filenames");
("-b",Arg.Unit (fun () -> show := 2),
"List both filenames and sizes");
("-s",Arg.Unit (fun () -> no_symlinks := true),
"Don't follow symbolic links");
("--before",Arg.String (fun s -> last_access := converttime s),
"Last access older than give date (format 'yyyy-mm-dd' or 'mmm yyyy')");
("--size",Arg.Int (fun i ->
min_size := Int64.mul (Int64.of_int i) (Int64.of_int (1024*1024))
), "File size bigger than size in Mbytes");
("--type",Arg.String (fun s -> has_type := s),
"File is specific type");
("--name",Arg.String (fun s ->
name_match := s; name_regexp := Str.regexp (s ^ "$")
), "Filename matches regular expression")
] (fun d -> dir := d) "show [DIR]";
let res = dirread !dir in
if !show=0 then begin
dirwrite (fst res) 0 !sort;
print_string ("Total size " ^ filesize (snd res) ^ "\n")
end else
List.iter
(fun e ->
print_endline (e.e_name ^ if !show=2 then " ("^filesize e.e_size^")" else "")
) (List.sort !sort (flat (fst res) !dir))
;;
Y aquí está la rutina principal. Llama a la rutina Arg.parse para separar los parámetros dados al programa. Pero esto es demasiado no-GNU para mí. Escribí mi propia versión de ésta que sigue los estándares de codificación de GNU un poco más que la que viene por defecto (Gnuarg). Como esa otra versión es un poco más complicada entonces sólo incluiré los fuentes que la usan.
El código puede ser obtenido de aquí. Solamente desempaquételo en algún lugar con 'tar -xzf show.tar.gz' y vaya al directorio de las fuentes con 'cd show/src'. Hay también un Makefile que compila a código máquina y que instala todo. Pero los Makefiles son demasiado "duros" para mostrarlos en este artículo. Los detalles están ahí en las fuentes. La forma general de compilación es.
ocamlopt -o show unix.cmxa str.cmxa basics.cmx show.ml
Las únicas librerías no-estándar en uso aquí son unix.cmxa y str.cmxa.
make su make install exit show --help show -s ~ --size 3 --before "apr 2003"
Esto concluye con el programa de ejemplo.
Desarrollador en una pequeña firna de tecnología en Holanda llamada
V&S bv. (www.v-s.nl)
Vendemos firewalls, anti-virus y cajas anti spam basadas en Linux.
Usando más y más el lenguaje OCaml para escribir mis aplicaciones.
Ocupado escribiendo un servidor http liviano con un lenguaje interno de script
(camlserv.sourceforge.net,
código fuente aquí)
Interesado en escribir sobre juegos basados en IA. Siempre intentando escribir uno, nada listo aún.