Después de los primeros pasos, creo que no tengo una base sólida del sistema de tipos en OCaml, y de como manejar de modo correcto los patterns
para, por ejemplo, diferenciar casos en el manejo de listas. Me falta cierta práctica, con lo que hacer ejercicios puede ser una buena idea. Mirando en la web, he visto uno interesante en codewars. Voy a realizar una parte, la idea es la siguiente:
Tenemos cadenas de caracteres con la forma: "01|15|59, 1|47|6, 01|17|20" que se supone una lista de tiempos con horas, minutos y segundos que hay que ordenar de menor a mayor (y en general hay que manejar estos tiempos para hacer operaciones con ellos).
Paso a paso
Lo primero es que una ordenar números va a necesitar transformar la cadena inicial de caracteres en enteros (es posible que exista una función que directamente ordene para caracteres en OCaml, pero en todo caso si queremos hacer operaciones numéricas deberemos hacer esa transformación a un tipo numérico).
Especificando tipos en una función hasta ahora no hemos especificado tipos en las funciones utilizadas de modo explícito. Esto es posible porque OCaml puede inferir los tipos de cada función o expresión, pero también podemos introducirlos siguiendo la sintaxis:
let mysum (x:string) (y:int) : float = (float_of_string x) +. (float_of_int y)
define una función con dos argumentos uno primero string y un segundo int y que da como resultado un float de todos modos examinando el código vemos que esta es la única opción de inputs y outputs al respecto de los tipos y OCaml puede inferirlo de modo preciso. Si la función no es tan restrictiva internamente se puede describir con tipos abstractos. Por ejemplo,List.hd
es una función de OCaml que tomar el primer elemento de una lista, y esta operación se puede hacer de modo genérico para diferentes tipos, por tanto el input es 'a list y el output es 'a, donde a es un tipo arbitario.Signature es una manera de describir los inputs y outputs de una función en OCaml (de manera similar a Haskell). Por ejemplo, la función
mysum
defina antes tendría una signature:string -> int -> float
. Mientras queList.hd
es'a list -> 'a
.
Dividir una cadena de caracteres
En la documentación de OCaml sobre cadenas de caracteres, ver Strings, encontramos la siguiente función String.split_on_char c s
que es una función con signature char -> string -> string list
Recordar que si defino
let mysplit = String.split_on_char "-"
esto es una función con signaturestring -> string list
que en este caso separaría una cadena de caracteres en el caracter "-" produciendo una lista.
Eliminar espacios
Para eliminar espacios podemos usar String.trim
(que es una función de string en string) pero si esto lo tengo que aplicar a una lista, debo usar List.map
con signature List.map ('a -> 'a) -> 'a list -> 'a list
que vemos que toma una función y una lista y devuelve una lista (recordemos que 'a es una forma de denotar un tipo abstracto, por tanto esta función List.map puede usarse sobre diferentes tipos 'a (enteros, strings, ...) pero no mezclarlos.
Podemos por tanto dividir la cadena original en ',' y con List.map eliminar espacios en una sola linea de código:
let get_times a = List.map (String.trim) (String.split_on_char ',' a)
Es útil razonar usado la signature de cada función, es decir teniendo en cuenta los tipos, eso introduce dificultades, pero también te da ideas sobre como programar.
Ordenar listas
Para ordenar una lista tenemos la función List.sort
con ('a -> 'a -> int) -> 'a list -> 'a list)
, es decir que toma una función que se aplica a un par elementos de tipo 'a que da un elemento de tipo int, esta función junto con una lista devuelve una lista. La función que necesitamos ('a -> 'a -> int)
debe dar 0 si ambos son iguales, positivo si el primero es mayor que el segundo, y negativo en caso contrario.
Esto es relativamente sencillo, ahora bien, dada la lista: ['01|15|59' ; '1|47|6' ; '01|17|20']
, crear una función que ordene/compare sus elementos no es tan directo. Una solución puede ser crear una lista de enteros con cada elemento de la lista y comparar [1,15,59] con [1,47,6]. Para ordenar, si se respeta que los segundos y minutos no superan el valor 60, necesitamos comparar el primer elemento, si son iguales, comparar el segundo, y si son iguales el tercero. Y por comparar entendemos generar un entero con valores positivos o negativos. Para dos números una manera de comparar sería sustraer esos números (esto daría el entero que necesitamos), para listas tendremos que volver a la idea de List.map
, pero necesitamos un map que aplique a dos listas que existe y se llama List.map2
, es decir ('a list -> 'a list -> 'a list)
let sort_two a b =
let c = List.map2 (-) a b in
match c with
| x::y::z::[] -> if x <> 0 then x else if y <> 0 then y else z
Para poder aplicar esto correctamente necesitamos dos funciones, una que cree una lista de listas con el string inicial y otra que los transforme en enteros. Estas listas de listas de enteros son las que podemos ordenar:
let prepare (a: string list) : (string list list) =
List.map (fun s -> String.split_on_char '|' s) a
let int_times (a: string list list) : (int list list) =
List.map (List.map int_of_string) a
... y para ordenar será sencillamente: let times_sorted x = List.sort sort_two x
, donde x es el resultado de int_times
, ponemos todo junto:
:::ocaml
open List
let sort_two a b =
let c = List.map2 (-) a b in
match c with
| x::y::z::[] -> if x <> 0 then x else if y <> 0 then y else z
let times_sorted x = List.sort sort_two x
let get_times a = List.map (String.trim) (String.split_on_char ',' a)
let prepare (a: string list) : (string list list) =
List.map (fun s -> String.split_on_char '|' s) a
let int_team (a: string list list) : (int list list) =
List.map (List.map int_of_string) a
let sort_times = times_sorted (int_times (prepare (get_times data)));;
Mostrar los resultados
Ya vimos en el capítulo anterior de estas notas que hay que ser cuidadoso para imprimir resultados en OCaml. Ahora tenemos una lista de listas de enteros ordenada, que queremos imprimir. Vamos a definir dos funciones para ello (una para strings, otra para enteros). En este caso, recordar que List.map
usaba una función de ('a -> a')
, pero para imprimir necesitamos algo similar pero de ('a -> 'a unit)
que es List.iter
:
open Printf
let pslist l = List.iter (fun x -> printf "%s\n " x) l;;
let pdlist l = List.iter (fun x -> printf "%d " x) l;;
El programa final que ordena los tiempos sería:
Programa final
open Printf
open List
let sort_two a b =
let c = List.map2 (-) a b in
match c with
| x::y::z::[] -> if x <> 0 then x else if y <> 0 then y else z
let times_sorted x = List.sort sort_two x
let get_times a = List.map (String.trim) (String.split_on_char ',' a) ;;
let prepare (a: string list) : (string list list) =
List.map (fun s -> String.split_on_char '|' s) a
let int_times (a: string list list) : (int list list) =
List.map (List.map int_of_string) a
let pslist l = List.iter (fun x -> printf "%s ; " x) l;;
let pdlist l = List.iter (fun x -> printf "%d " x) l;;
let pplist l =
let ss = List.map (fun x -> string_of_int x) l in
" " ^ (String.concat "|" ss) ^ ",";;
let data = "01|15|59, 1|47|6, 01|17|20, 1|32|34, 2|3|17"
let list_str_data = get_times data
let list_list_str_data = prepare (get_times data)
let list_list_int_data = int_times (prepare (get_times data))
let sorted_list_list_int_data= times_sorted (int_times (prepare (get_times data)))
let () = printf "\n Cadena inicial en forma de lista\n";;
List.iter (printf "%s ; ") (get_times data);;
printf "\n Cadena inicial separando las cadenas que contiene\n";;
List.iter (fun ll -> pslist ll ) (prepare (get_times data));;
printf "\n Cadena inicial en forma de lista de enteros\n";;
List.iter (fun ll -> pdlist ll ) (int_times (prepare (get_times data)));;
printf "\n Lista de enteros ordenada\n";;
List.iter (fun ll -> pdlist ll ) (times_sorted (int_times (prepare (get_times data))));;
printf "\n Cadena inicial ordenada!\n";;
List.iter (fun x -> printf "%s " (pplist x)) sorted_list_list_int_data;;
Y esta ha sido la segunda sesión de OCaml.