He empezado a aprender OCaml (Categorical Abstract Machine Language), aunque podría escribirse OCaML ya que deriva de la familia de lenguajes de programación ML (Meta Language). ML y también OCaml son lenguajes de programación de propósito general dentro del paradigma de lenguajes funcionales. Estrictamente hablando, OCaml es multi-paradigma ya que admite tanto programación imperativa como orientada a objetos. Sin embargo creo que la mayoría de la gente que se aproxima a OCaml lo hace (o lo conoce) como lenguaje funcional. Mi idea es contar aquí mis progresos con este lenguaje y construir una especie de notas accesibles a otras personas. Hago notar que trabajo en Linux, por tanto no puedo detallar su uso en otros sistemas operativos.
INFO He creado un repositorio en sourcehut donde irán apareciendo poco a poco los códigos finales de cada entrada del tutorial. Todos ellos podrán lanzarse con un
make test
.
Usando OCaml
OCaml ofrece muchas posibilidades a la hora de ejecutar código, puede usarse como un intérprete (en realidad es un bytecode compiler), puede compilarse a código máquina o también usar dentro de un entorno REPL (Read–Eval–Print Loop).
REPL
En la terminal podemos invocar ocaml en modo REPL escribiendo en nuestra shell
> ocaml
lo que abre un entorno similar, conceptualmente, a python, julia, ghci ... Al principio es más flexible y más intuitivo usar utop (puedes instalarlo usando opam install utop
).
> utop
En mi caso, tras jugar 5 min dentro de utop he pasado a usar el compilador o el interprete, pero no REPL (parece que también existen opciones similares a Jupyter, incluyendo Jupyter, para usar OCaml lo que puede dar a muchos una funcionalidad similar a Python).
Compilador bytecode
La mayoría de los tutoriales usan el REPL, posiblemente porque te devuelve información acerca de los tipos cuando usas variables o defines funciones. A mi me interesa más el compilador y aprender desde cero el uso de módulos, librerías y creación de programas más complejos. Así que todo lo que viene usará (en su mayoría) el compilador. Existe, una herramienta llamada dune, que automatiza muchos pasos relacionados con la compilación, documentación, debug o test automáticos (parece el análogo al cabal en Haskell), pero creo importante empezar con la compilación básica y aprender como funcionan los módulos y librerías antes de aprender/usar este tipo de herramientas.
Como decía aquí por compilador me refiero también al bytecode compiler (pero no REPL). Si tenemos un programa sencillo llamado code.ml deben funcionar:
> ocaml code.ml # ejecuta código como bytecode compiler
> ocamlc code.ml # compila código y crea un ejecutable a.out
> ocamlc code.ml -o code.out # compila codigo y crea un ejecutable code.out
Si nuestro programa necesita N argumentos tendríamos:
> ocaml code.ml arg1 arg2 ... argN
> ocamlc code.ml -o code.out
> ./code.out arg1 arg2 ... argN
Ambas opciones deben dar el mismo resultado.
Compilador propiamente dicho
En este caso procederíamos:
> ocamlopt -o code.out code.ml
más adelante veremos el uso de módulos, pero podemos mencionar ahora que sí usaramos módulos de una librería externa (no parte de la librería estándar/base/core) instalada, por ejemplo, con opam, tenemos que usar el comando ocamlfind
que localiza esta librería. Imaginemos que nuestro programa usa: ocaml-yaml y dibujar-svg tendríamos que compilar así:
> ocamlfind ocamlopt -o code.ml -linkpkg -package ocaml-yaml,dibujar-svg code.ml
Primer programa
Nuestro primer programa va a consistir en calcular el cuadrado de una lista de números introducidos por el usuario como argumentos, primero veamos como escribir expresiones y funciones en este lenguaje:
let x = 10.0;;
let add2 x = x + 2;;
como vemos en OCaml las expresiones suelen definirse usando let
tanto para funciones como para asignar variables. Retornando al primer programa que queremos, la función que calcula el cuadrado de un número podría ser:
let square x = x * x ;;
y un programa inicial que de un resultado:
let square x = x*x ;;
let a=10 ;;
let () = print_int (square a);;
recordemos que si guardamos este código como code.ml
lo veremos el resultado 100 haciendo ocaml code.ml
. Ahora bien, el código anterior podríamos escribirlo también así,
let square x = x*x;;
let () = let a=10 in
print_int (square a);;
pero cuidado con una combinación inconsistente de ambos códigos:
let a=10;;
let square x = x*x;;
let () = let a=5 in
print_int (square a);;
print_endline " " ;;
print_int a
daría como resultados 25 y 10, ya que el primer print esta dentro de un let donde se ha redefinido a
con valor 5, pero fuera de este bloque a
tiene el valor que habíamos asignado previamente. Es importante recordar el campo de uso (scope) de cada variable y como let
redefine una variable en una parte concreta del código. Tenemos que tener cuidado también con esto:
square x = x*x;;
let () = let a=10.5 in
print_int (square a);;
si lo ejecutamos vemos que produce un error. La razón es que en OCaml hay operadores diferentes para enteros (int) y coma flotante (floats). El siguiente programa es correcto:
let square x = x *. x;;
let () = let a=10.5 in
print_float (square a);;
esto se debe a que *
y *.
son dos operadores diferentes que aplican a diferentes tipos, y cuando definimos square
con uno de ellos OCaml detecta esto y la función solo se aplica al tipo correspondiente. Voy a intentar algo más general.
Comentar lineas Para comentar texto hemos de encapsularlo entre ( y ) y puede ser una sola linea o un bloque de lineas, es decir, podemos introducir el carácter '\n' entre ( y ) y sigue siendo todo parte del comentario.
Variants Hay una manera de definir un conjunto de valores para un tipo llamado variants, pero este conjunto de valores puede ser algo concreto (por ejemplo 1, 2 y 3) o puede ser algo más abstracto por un tipo unión de otros tipos. Esto puede ser útil para crear lo que vamos a llamar number que podrá ser o bien un entero o bien un float.
Match x with permite crear lo que se denomina pattern match. Es decir, decidir que expresiones vamos a usar dependiendo de x. Es importante notar que OCaml va a comprobar que el conjunto de opciones es exhaustivo y sino es así va a lanzar un error durante la compilación.
Veamos el codigo:
(* esto es un comentario *)
(* definimos un nuevo tipo con un formato que parece que on *)
(* OCaml se llama Variants *)
type number = Int of int | Float of float ;;
(* definimos una funcion para este tipo/variant number *)
(* la diferenciación por casos se hace usando match x with *)
let square x =
match x with
| Int i = Int(i * i)
| Float f = Float(f *. f)
Esto ha definido la función square
para el nuevo tipo number
que es una combinación de ambos Int
y Float
. Para hacer el programa equivalente que de resultados deberíamos definir un print_number
(antes hemos usado print_int
y print_float
):
let print_number x =
match x with
| Int i = print_int i
| Float f = print_float f
El programa completo que muestra el uso de number
y la square
para este tipo podría ser:
type number = Int of int | Float of float ;;
let square x =
match x with
| Int i = Int(i * i)
| Float f = Float(f *. f)
let print_number x =
match x with
| Int i = print_int i
| Float f = print_float f
let () = let a: number = Float(5.5) in
print_number (square a)
let a: number = Int(5) in
print_number (square a)
Recordemos ahora que nuestro objetivo es realizar un programa que calcule el cuadrado de todos los números que el usuario introduzca como argumentos del programa. En este caso tendremos que hacer un loop en los elementos de Sys.argv que es el array donde OCaml almacena por defecto estos argumentos. Sys.argv.(0) es el nombre del programa, Sys.argv(1) es el primer argumento introducido etc... La dificultad ahora es que estos números introducidos por el usuario es almacenan con tipo String, así que debemos convertir este tipo a nuestro tipo number. Esto lo puede hacer una nueva función que llamaremos number_to_string
y que veremos que utiliza la función int_of_string_opt
que da como resultado un tipo llamado option
que es:
type 'a t = 'a option =
| None
| Some of 'a
Donde 'a
es significa un tipo genérico (que en nuestro caso veremos que es Int). Ahora la salida de la función int_of_string_opt
será Some Int
si la string (cadena de caracteres) puede convertirse a Int y es None
si no puede hacerlo. Para evaluar que obtengo usamos la función is_some
del módulo Option
. Por tanto introducimos primero open Option
y con la definición denumber_to_string
el programa completo queda como:
open Option
type number = Int of int | Float of float ;;
let square x =
match x with
| Int i -> Int(i * i)
| Float f -> Float(f *. f)
let print_number x =
match x with
| Int i -> print_int i
| Float f -> print_float f
let number_of_string x =
let a = is_some (int_of_string_opt x) in
if a then
Int(int_of_string x)
else
Float(float_of_string x)
let () = for i=1 to Array.length Sys.argv - 1 do
print_number (square (number_of_string Sys.argv.(i) ) ) ;
print_endline " "
done
Y esta ha sido la primera sesión de OCaml. Creo que conforme aprenda OCaml con mayor profundidad, este programa va a ser mejorado: a primera vista parece muy largo para algo tan sencillo, pero también vemos que OCaml es más estricto en cuanto al tipo que usamos para cada operación y en consecuencia los programas tienden a ser más correctos. La forma final en que utilizo un loop es típico de programación imperativa, en programación funcional debería haber construido una función que se aplicara sobre el array y produjera la salida. Veremos si más adelante puedo incluir dicha función. He aprendido:
- Diferentes maneras de ejecutar un código en OCaml
- Como definir funciones y tipos en OCaml
- El tipo llamado
option
que me recuerda al tipo llamado Maybe en Haskell. - Como realizar lo equivalente a definir en matemáticas una función por casos. Un ejemplo:
f(x) = 1 si x >= 0
f(x) = -1 si x < 0
- Como se utilizan de forma básica los argumentos de un programa en OCaml ejecutado en la línea de comandos.