Muchos Paréntesis

1. De emacs a Clojure

Podría decirse que empecé a aprender sobre Lisp con Emacs Lisp. Aunque no he escrito mucho, todo lo que he necesitado ha estado disponible en los 'paquetes' de Emacs. Diría que la configuración del editor es ahora más fácil que nunca y, con el tiempo, se vuelve cada vez más sencilla.

Empecé a usar Emacs hace aproximadamente un año. Eran tiempos oscuros: estaba del lado de Vim y Neovim, pero nunca me sentí completamente cómodo con sus bindings ni con sus modos. Muchas veces necesitaba una u otra funcionalidad que requería demasiado tiempo de configuración. Fue entonces cuando, una tarde, descubrí Emacs. Se sintió natural mientras navegaba por el tutorial, quizá porque no tengo una buena postura de manos al escribir y presiono las teclas con los dedos más alejados en lugar de los más cercanos. De cualquier manera, ese fue mi primer acercamiento a Lisp.

Todo eso ocurrió durante unas vacaciones, en un periodo de entre uno y dos meses entre semestres.

Al volver a la universidad, por alguna razón dejé de usar Emacs y Linux. Instalé IntelliJ IDEA y me puse a grindear LeetCode y Java (necesito un trabajo). Lo disfruté: aprendí sobre streams y programación funcional en Java. Tengo algunos algoritmos de práctica guardados en algún lado, y la diferencia se nota. Pasar de varias líneas de código a un bloque compuesto por funciones es una experiencia interesante.

Cuando terminó el semestre, decidí aprender Haskell para mejorar mis habilidades en el paradigma funcional. Sin embargo, por alguna razón lo abandoné y empecé a jugar con Guile Scheme, SBCL y Clojure. Son mis principales intereses por ahora. Me gusta decir que compongo código, porque en estos lenguajes siento que cada función es como una pieza que encaja con las demás, creando estructuras elegantes y expresivas.

Cada vez que escribo en Lisp, me doy cuenta de lo diferente que es la forma en que pienso el código. Quizá por eso sigo explorando estos lenguajes, buscando esa claridad y belleza que no siempre encuentro en otros paradigmas.

2. Algo de clojure y Guile Scheme

He estado jugando con Clojure y algunas de sus librerias, intente hacer un CRUD, aca escribo como incie. Primero se necesita una BD, en mi caso use mariadb; para activar el servicio en mi sistema operativo tengo que declarar el servicio en mi archivo de configuracion ~/.config/guix/config.scm (Si, estoy usando GNU Guix btw)

(services mysql-service-type)

Tambien debe usarse en el header

(use-package-modules databases)

El archivo de configuracion base se ve asi:

(use-modules (gnu)
             (gnu packages lisp))

(use-package-modules fonts wm)
(use-service-modules cups desktop networking ssh xorg)
(use-service-modules databases)    ;; <--- Modulo que provee los servicios

 ;; Below is the list of system services.  To search for available
 ;; services, run 'guix system search KEYWORD' in a terminal.
 (services
  (append
   (list
    (service gnome-desktop-service-type)
    (service mysql-service-type)       ;; <-- Servicio MYSQL
    (set-xorg-configuration
     (xorg-configuration (keyboard-layout keyboard-layout)))) 
... )))

Esa es solo una parte del archivo que se genera en la instalacion de Gnu Guix. Probablemente exista una mejor manera de activar los servicios, pero yo solo conozco esa, reconfiguro el sistema y ya esta el servicio up.

Sigue la instalacion de openjdk, clojure y leiningen, ez

2.1. Generación de la Aplicación

Este comando genera un 'esqueleto' de la aplicación:

lein new app noob-crud

La estructura se ve de esta manera:

noob-crud/
├── CHANGELOG.md
├── doc/
│   └── intro.md
├── LICENSE
├── project.clj
├── README.md
├── resources/
├── src/
│   └── noob_crud/
│       └── core.clj
└── test/
    └── noob_crud/
        └── core_test.clj

Es necesario agregar el connector para la bd en las dependencias, usé next-jdbc y el driver para mariadb:

[com.github.seancorfield/next.jdbc "1.3.994"]

Para mas información sobre next-jdbc, pueden consultarse los docs, tambien hay ejemplos.

2.2. Código en Clojure:

Dividi el proyecto en 2 core.clj es donde manejo las queries y un menu para interactuar con la bd y createdb.clj donde creo la bd y aseguro la conexion con las credenciales necesarias.

2.2.1. createdb.clj

(ns crud.createdb
  (:require [next.jdbc :as jdbc]))

;; **db-spec**
;; Contiene la configuración para conectar a la base de datos MariaDB.
;;
;; Parámetros:
;; - `:dbtype`: El tipo de base de datos (en este caso, "mariadb").
;; - `:dbname`: El nombre de la base de datos (en este caso, "clojure").
;; - `:host`: La dirección IP o el host del servidor de base de datos (en este caso, "127.0.0.1").
;; - `:port`: El puerto de conexión al servidor de base de datos (en este caso, el puerto por defecto para MariaDB, 3306).
;; - `:user`: El nombre de usuario para autenticarse con la base de datos.
;; - `:password`: La contraseña asociada al usuario.
(def db-spec
  {:dbtype "mariadb"
   :dbname "clojure"
   :host "127.0.0.1"
   :port 3306
   :user "ashajaa"
   :password "I(s44c)d"})

;; **ds**
;; Crea un datasource usando la configuración de `db-spec`, que se utilizará para realizar las operaciones en la base de datos.
;;
;; Retorna:
;; - Un objeto datasource de `next.jdbc` para interactuar con la base de datos.
(def ds (jdbc/get-datasource db-spec))

;; **create-table**
;; Crea la tabla `personajes` en la base de datos si no existe.
;;
;; Parámetros:
;; - Ninguno.
;;
;; Retorna:
;; - Una lista de mapas de resultados de la operación de creación de la tabla. Si la operación es exitosa, la lista estará vacía.
(defn create-table []
  (jdbc/execute! ds ["
CREATE TABLE personajes (
  id int auto_increment primary key,
  nombre varchar (100),
  anime varchar (100)
)"]))

;; **insert-data**
;; Inserta datos iniciales en la tabla `personajes`. Los personajes se insertan con nombre y anime.
;;
;; Parámetros:
;; - Ninguno.
;;
;; Retorna:
;; - Una lista de mapas de resultados de la operación de inserción. Si la operación es exitosa, la lista estará vacía.
(defn insert-data []
  (jdbc/execute! ds ["
INSERT INTO personajes (nombre, anime) 
VALUES 
  (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?), (?, ?)
" "Mai" "Dragon Ball"
   "Asuna" "Sword Art Online"
   "Rem" "Re:Zero"
   "Saber" "Fate/stay night"
   "Mikasa" "Attack on Titan"
   "Hinata" "Naruto"
   "Emilia" "Re:Zero"]))

2.2.2. core.clj

  (ns crud.core
    (:require [next.jdbc :as jdbc]
              [crud.createdb :refer [ds]]))

  ;; **insert-character**
  ;; Inserta un nuevo personaje en la base de datos.
  ;;
  ;; Parámetros:
  ;; - `nombre`: un string que representa el nombre del personaje.
  ;; - `anime`: un string que representa el anime al que pertenece el personaje.
  ;;

  ;; Retorna:
  ;; - Una lista de mapas de resultados de la operación de inserción. Si la operación es exitosa, la lista estará vacía.
  (defn insert-character [nombre anime]
    (jdbc/execute! ds ["INSERT INTO personajes (nombre, anime) VALUES (?, ?)" nombre anime]))

  ;; **get-all-characters**
  ;; Obtiene todos los personajes de la base de datos.
  ;;
  ;; Parámetros:
  ;; - Ninguno.
  ;;
  ;; Retorna:
  ;; - Una lista de mapas, donde cada mapa representa un personaje con sus columnas (id, nombre, anime) de la tabla `personajes`.
  (defn get-all-characters []
    (jdbc/execute! ds ["SELECT * FROM personajes"]))

  ;; **get-character-by-id**
  ;; Obtiene un personaje de la base de datos mediante su ID.
  ;;
  ;; Parámetros:
  ;; - `id`: el identificador único del personaje en la base de datos.
  ;;
  ;; Retorna:
  ;; - Un mapa que contiene los detalles del personaje (id, nombre, anime) si el personaje existe.
  ;; - `nil` si no se encuentra un personaje con el ID proporcionado.
  (defn get-character-by-id [id]
    (jdbc/execute-one! ds ["SELECT * FROM personajes WHERE id=?" id]))

  ;; **update-character**
  ;; Actualiza el nombre y/o el anime de un personaje en la base de datos según su ID.
  ;;
  ;; Parámetros:
  ;; - `id`: el identificador único del personaje en la base de datos.
  ;; - `new-name`: el nuevo nombre que se asignará al personaje.
  ;; - `new-anime`: el nuevo anime que se asignará al personaje.
  ;;
  ;; Retorna:
  ;; - Una lista de mapas de resultados de la operación de actualización. Si la operación es exitosa, la lista estará vacía.
  (defn update-character [id new-name new-anime]
    (jdbc/execute! ds ["UPDATE personajes SET nombre=?, anime=? WHERE id=?" new-name new-anime id]))

  ;; **delete-character**
  ;; Elimina un personaje de la base de datos según su ID.
  ;;
  ;; Parámetros:
  ;; - `id`: el identificador único del personaje en la base de datos.
  ;;
  ;; Retorna:
  ;; - Una lista de mapas de resultados de la operación de eliminación. Si la operación es exitosa, la lista estará vacía.
  (defn delete-character [id]
    (jdbc/execute! ds ["DELETE FROM personajes WHERE id=?" id]))

  ;; **menu**
;; Muestra el menú principal interactivo para el CRUD de personajes. Permite al usuario seleccionar entre las diferentes opciones para agregar, listar, buscar, actualizar, eliminar personajes o salir.
;;
;; Parámetros:
;; - Ninguno.
;;
;; Retorna:
;; - Ninguno (La función ejecuta un bucle interactivo, solicitando la entrada del usuario hasta que elija la opción de salir).
(defn menu []
  (loop []
    (println "\nMenu CRUD:")
    (println "1. Agregar personaje")
    (println "2. Listar personajes")
    (println "3. Buscar personaje por ID")
    (println "4. Actualizar personaje")
    (println "5. Eliminar personaje")
    (println "6. Salir")
    (print "Elige una opción: ")
    (flush)
    (let [opcion (read-line)]
      (cond
        ;; Opción 1: Agregar un nuevo personaje
        (= opcion "1") (do (print "Nombre: ") (flush)
                           (let [nombre (read-line)]
                             (print "Anime: ") (flush)
                             (let [anime (read-line)]
                               (insert-character nombre anime)
                               (println "Personaje agregado."))))
        ;; Opción 2: Listar todos los personajes
        (= opcion "2") (do (println "Personajes:")
                           (doseq [personaje (get-all-characters)]
                             (println personaje)))
        ;; Opción 3: Buscar un personaje por ID
        (= opcion "3") (do (print "ID: ") (flush)
                           (let [id (Integer/parseInt (read-line))]
                             (println (get-character-by-id id))))
        ;; Opción 4: Actualizar un personaje existente
        (= opcion "4") (do (print "ID: ") (flush)
                           (let [id (Integer/parseInt (read-line))]
                             (print "Nuevo Nombre: ") (flush)
                             (let [nombre (read-line)]
                               (print "Nuevo Anime: ") (flush)
                               (let [anime (read-line)]
                                 (update-character id nombre anime)
                                 (println "Personaje actualizado.")))))
        ;; Opción 5: Eliminar un personaje por ID
        (= opcion "5") (do (print "ID: ") (flush)
                           (let [id (Integer/parseInt (read-line))]
                             (delete-character id)
                             (println "Personaje eliminado.")))
        ;; Opción 6: Salir del programa
        (= opcion "6") (println "Saliendo...\n") 
        ;; Opción inválida
        :else (do (println "Opción no válida. Intenta de nuevo.") (recur)))))

;; **-main**
;; Función principal que inicia el menú interactivo.
;;
;; Parámetros:
;; - Ninguno.
;;
;; Retorna:
;; - Ninguno (Llama a la función `menu` para iniciar la interacción).
(defn -main []
  (menu))

¿Es el mejor CRUD jamás creado? No, probablemente no. Pero, me siento realmente orgulloso de él. Cada línea de código refleja el tiempo, esfuerzo.

Author: Isaac Narvaez <isaac.rkt@proton.me>

Created: 2025-02-03 Mon 01:51