Haskell con debugger interactivo en Doom Emacs Y en IntelliJ Ultimate. Hito doble.
El lunes lo anuncie, hoy lo he hecho. Dos veces.
Hace tres dias publique
un post celebrando que Well-Typed habia sacado hdb, el debugger DAP para Haskell, y
prometiendole a Pascual que el lunes lo arrancabamos. Pues eso. Hoy,
martes 5 de mayo, lo tenemos funcionando en dos
editores: Doom Emacs (lo previsto) y
IntelliJ Ultimate 2026.1 (lo que paso
de bonus a hito propio cuando Pascual dijo "aventura, hombre,
aventura").
Breakpoint, ejecutar, pausa en el thunk, panel de variables, step over. Las dos.
Y como todo en NixOS, el dia ha sido una ofrenda de tropezones hilarantes que merecen estar escritos para el proximo que se meta. Esto es el postmortem alegre.
primerosCincoParesAlCuadrado = {[Int]} _: ese guion
bajo es un thunk SIN EVALUAR, en vivo, en el panel del IDE.Que es lo que se ve cuando funciona
Para que el lector que no haya pisado Doom todavia se haga una idea:
+--------------------------------------+ +-----------------------+ | Debugging101.hs | | DAP Variables | | | | | | primerosCincoParesAlCuadrado :: ...| | > xs :| | primerosCincoParesAlCuadrado = | | casos : | | take 5 $ | | n : 16 | | map (^ (2 :: Int)) $ | | | | filter even [1..1000000] | +-----------------------+ | | | foldrLazyDemo :: IO () | +-----------------------+ | foldrLazyDemo = do | | Stack frames | | >>>let xs = primerosCinc... | | | | putStrLn $ "Resultado: " ++... | | -> foldrLazyDemo | | | | main | +--------------------------------------+ +-----------------------+ F10 step over F11 step in F12 step out Espacio-d-d para arrancar
Lo magico no es la UI. Es lo que pone en el panel de variables: xs : <thunk>. Estas viendo, en tiempo
real, una expresion sin evaluar. Si haces step over a la siguiente
linea, xs se convierte en [4,16,36,64,100] delante de tus ojos. Eso es
Haskell, y eso ahora se puede MIRAR.
Por que esto era dificil hasta esta semana
Tres condiciones tenian que cumplirse a la vez:
- GHC 9.14, que lleva los stop points semanticos para lazy code. Salio hace pocas semanas, no esta en ninguna distribucion estable todavia.
haskell-debuggercompilado contra esa misma version exacta de GHC. No vale 9.14.0 vs 9.14.1: tiene que ser bit a bit la misma.- Un cliente DAP en tu editor que
sepa hablar con el. Para Doom eso es
dape, el cliente DAP nativo en Emacs Lisp.
Cualquiera de los tres descuadrado y nada arranca. Yo me he tropezado con los tres.
Trampa numero 1: el flake no tenia GHC 9.14
El proyecto pensando-en-haskell que
abri hace meses tenia esto en su flake.nix:
ghcWithPackages = pkgs.haskell.packages.ghc98.ghcWithPackagesGHC 9.8. Insuficiente. Cambio a 9.14:
ghcWithPackages = (pkgs.haskell.compiler.ghc914.override {
# ...
})nix develop y a esperar. Aurin, Dual
Xeon, 72 hilos, ciento veintiocho gigas de RAM, se peta
compilando servant-server. Que pasa?
Trampa numero 2: servant-server bloquea base 4.22
GHC 9.14 trae base-4.22.0.0. servant-server todavia tiene un build-depends: base < 4.22 en su cabal. Cabal
solver se come la restriccion, intenta downgradearlo todo, no encuentra
plan, explota. Mensaje de error: una pared de texto de cuatrocientos
paquetes con dependencias circulares.
Solucion brutalista: si yo solo necesito el Debugging101.hs que ESCRIBI HACE 10 MINUTOS, no
necesito servant. Comento el target executable pensando-en-haskell-exe en pensando-en-haskell.cabal, añado un target
nuevo:
executable debug101
main-is: Debugging101.hs
hs-source-dirs: app
build-depends: base
default-language: Haskell2010
Solo base. Compila en seis
segundos.
Trampa numero 3: HLS no soporta 9.14
haskell-language-server (el LSP normal
de Haskell, lo que te da autocompletado y errores en linea)
NO soporta GHC 9.14 todavia. Sus
dependencias tampoco. Asi que durante esta epoca de transicion, si
quieres usar 9.14 para el debugger, renuncias a
HLS.
Es un trade que tienes que aceptar. Te quedas sin autocompletado pero
ganas el debugger. En unos meses HLS se actualizara y volvera todo a la
normalidad. Mientras tanto, haskell-mode
bruto, font-lock, y fourmolu como
formatter para no llorar.
Trampa numero 4: el GHC que ve hdb tiene que ser el mismo
Compilo hdb dentro del nix develop (donde GHC 9.14.1 esta en PATH). Lo
instalo en ~/.local/bin/hdb. Vale.
Doom arranca hdb a traves de dape. El proceso Emacs hereda el PATH de mi
sesion grafica, NO el PATH del shell de nix develop. Resultado: hdb se lanza, busca ghc, encuentra el ghc 9.8 que tengo instalado globalmente,
detecta que no es 9.14, y aborta con
error.
La solucion es un wrapper:
#!/usr/bin/env bash
# ~/.local/bin/hdb-dap
set -euo pipefail
# Sube buscando flake.nix
dir="$(pwd)"
while [ "$dir" != "/" ]; do
[ -f "$dir/flake.nix" ] && { cd "$dir"; break; }
dir="$(dirname "$dir")"
done
# Entra en nix develop antes de exec hdb
exec nix develop --command hdb "$@"Doom llama a hdb-dap en lugar de hdb. El wrapper sube hasta encontrar el flake.nix, entra en nix develop (que pone GHC 9.14 en PATH), y exec
hdb. Magia transparente.
Trampa
numero 5: dape timeout default es 10
segundos
Configurado el adaptador, hago espacio-d-d. Aparece el menu, elijo
haskell-hdb, le pongo Enter, y a los DIEZ
SEGUNDOS exactos:
Command "launch" timed out after 10 seconds
hdb en su primer arranque tarda quince
a treinta segundos descubriendo flags de compilacion. Llama a cabal path, llama a ghc --print-libdir, parsea el cabal del
proyecto. Eso es legitimo. Pero dape por
defecto solo le da diez segundos antes de matar la conexion.
Una linea:
(setq dape-request-timeout 60)Y arrancado.
Trampa
numero 6: flycheck-haskell-stack-ghc
petando con unicode
Mi Debugging101.hs tiene comentarios en
español sin tildes pero con ñ. flycheck, que viene activado por defecto en Doom
para haskell-mode, intenta correr el
checker haskell-stack-ghc sobre el buffer
cada vez que pulso una tecla. stack no
existe en mi flake (yo uso cabal), asi que
falla mil veces por segundo, llenando el *Messages* buffer y comiendo CPU.
Apaga y vamonos:
(after! flycheck
(add-to-list 'flycheck-disabled-checkers 'haskell-stack-ghc))La config final de Doom
Esto es lo que va en ~/.config/doom/config.el si quieres replicar el
montaje:
(after! flycheck
(add-to-list 'flycheck-disabled-checkers 'haskell-stack-ghc))
(use-package! dape
:config
(setq dape-request-timeout 60)
(add-to-list 'dape-configs
`(haskell-hdb
modes (haskell-mode haskell-ts-mode haskell-cabal-mode)
ensure (lambda (config)
(unless (executable-find "hdb")
(user-error "hdb no encontrado en PATH")))
fn (dape-config-autoport)
host "localhost"
port :autoport
command "hdb"
command-args ("server" "--port" :autoport)
:type "haskell-debugger"
:request "launch"
:projectRoot dape-cwd-fn
:entryFile dape-buffer-default
:entryPoint "main"
:entryArgs []
:extraGhcArgs []
:internalInterpreter t)))Tres detalles:
command-args ("server" "--port" :autoport).hdbpor defecto arranca en modo CLI. Le pasasserver --port <n>para que escuche TCP.:autoportes un placeholder dedapeque se sustituye por un puerto libre en runtime.:internalInterpreter t.hdbpuede arrancar el interprete GHCi en su mismo proceso (rapido) o spawnar un subprocess (mas aislado, mas lento). Para programas pequeños el internal va sobrado y arranca antes.Si no tienes wrapper
hdb-dap, cambiacommand "hdb"porcommand "hdb-dap". Yo prefiero la indireccion porque me asegura que el flake del proyecto se carga.
El precio: cada arranque recompila
Honestidad brutal: cada espacio-d-d tarda quince a veinte segundos.
hdb arranca un GHC fresco con bytecode
interpreter para poder pausar dentro de codigo perezoso. No hay cache.
Ese es el precio de tener debugger en un lenguaje lazy.
Es lo mismo en VSCode con la extension oficial. Ese precio se paga en todos los editores. No es una limitacion de Doom.
Truco: en vez de cerrar y abrir el debugger constantemente, deja la
sesion abierta y usa dape-evaluate-expression o el REPL embebido para
probar cosas sin reiniciar.
Lo que sigue
Ahora viene lo divertido: meterme en sesiones reales de aprendizaje con esto encendido.
- Sesion 1:
foldrsobre listas. Breakpoint dentro del lambda y ver comotake 5corta la evaluacion antes de procesar el millon de elementos. - Sesion 2: monada
Maybe. Ver como>>=propaga elNothingsaltandose pasos. - Sesion 3:
Eithercon error tipado. Lo mismo queMaybepero llevando informacion de fallo.
Todo eso esta ya escrito en app/Debugging101.hs del repo pensando-en-haskell. Tres ejercicios, tres
conceptos, todo con breakpoint listo. Cuando termine las sesiones
escribire un post por cada una.
La aventura IntelliJ Ultimate (lo que iba a ser un punto y coma)
Cuando le ensene a Pascual el debugger en Doom, se quedo pensando: "perfecto, pero IntelliJ Ultimate es donde brilla la depuracion para PHP, ¿por que no Haskell?". Y dijo lo magico: **"aventura, hombre, aventura"**.
Le advierto: Haskell e IntelliJ no se llevan. El plugin clasico de Rik van der Kleij esta congelado desde 2021 y NO aparece en el Marketplace de IntelliJ 2026.1 (Marketplace lo oculta cuando un plugin no actualiza compatibilidad). Pero IntelliJ Ultimate trae plugin MCP server y JetBrains tiene LSP/DAP via plugins externos. Habia camino. Solo habia que pelearlo.
Asi que peleamos. Aqui van las trampas en orden cronologico, para que el siguiente que se meta no pierda las dos horas que perdimos nosotros.
Trampa IntelliJ uno: LSP4IJ es el plugin que necesitas
JetBrains publica un plugin oficial llamado "Debug Adapter Protocol" (Fleet/IntelliJ shared) pero es para developers de plugins, no user-facing. Si lo instalas y buscas "Debug Adapter" en Run/Debug Configurations, no aparece.
Lo que necesitas es LSP4IJ de Red
Hat. Marketplace ID 23257-lsp4ij. Plugin
distinto. Trae cliente LSP y cliente DAP user-defined:
te deja registrar adapters arbitrarios desde la UI.
Settings → Plugins → Marketplace → busca "lsp4ij" → Install + restart.
Trampa IntelliJ
dos: PATH no incluye ~/.local/bin
Configurada la primera version del adapter en LSP4IJ:
Server tab:
Name: Haskell hdb
Command: hdb-dap server --port ${port}
Connect by waiting: Log pattern: "Running DAP server on"
Pulso Debug. Cannot run program "hdb-dap": No such file.
IntelliJ Ultimate se lanza desde el JetBrains Toolbox y NO hereda el
PATH del shell de usuario. ~/.local/bin no
esta. Solucion: ruta absoluta:
Command: /home/passh/.local/bin/hdb-dap server --port ${port}
Trampa IntelliJ tres: el cwd es el de IntelliJ, no el del proyecto
Pulso Debug. Ahora hdb-dap si arranca,
pero falla con "no flake.nix encontrado subiendo desde /home/passh/.local/share/JetBrains/Toolbox/ apps/intellij-idea-ultimate/bin".
LSP4IJ NO respeta el "Working directory" del Configuration tab para
el server (solo para el debuggee). Lanza el adapter en el cwd de
IntelliJ. El wrapper hdb-dap sube buscando
flake.nix y obviamente no lo encuentra
alli.
Solucion: bash -c con cd explicito:
Command: bash -c "cd /home/passh/src/pensando-en-haskell && exec /home/passh/.local/bin/hdb-dap server --port ${port}"
Comillas dobles para que LSP4IJ siga sustituyendo ${port} antes de pasar el string a bash.
Trampa
IntelliJ cuatro: dentro de nix develop,
hdb tampoco esta
Pulso Debug. Avanza, entra en el flake, banner del shell, y peta:
/tmp/nix-shell.79PY8I: line 2188: exec: hdb: not found
El wrapper hdb-dap hacia nix develop --command hdb. Pero el shell de
nix develop NO incluye ~/.local/bin en PATH (donde cabal install deja hdb). En Doom funcionaba porque direnv ya
cargaba el env del flake en el buffer Emacs y hdb se resolvia con el PATH del padre. Aqui
no.
Solucion: pasar la ruta absoluta de hdb
al nix develop. Modifico el wrapper:
HDB_BIN="${HDB_BIN:-$HOME/.local/bin/hdb}"
exec nix develop --command "$HDB_BIN" "$@"Trampa IntelliJ
cinco: ${file} no se sustituye
Pulso Debug. hdb arranca, conecta, lee
el JSON de launch… y peta con:
filepath: /home/passh/src/pensando-en-haskell/${file}
LSP4IJ NO expande ${file} como hace
VSCode. Pasa el placeholder literal al adapter. Workaround: ruta
absoluta hardcodeada en el JSON del Configuration tab:
{
"type": "haskell-debugger",
"request": "launch",
"projectRoot": "/home/passh/src/pensando-en-haskell",
"entryFile": "/home/passh/src/pensando-en-haskell/app/Debugging101.hs",
"entryPoint": "main",
"internalInterpreter": true
}Trampa IntelliJ seis: el programa corre entero sin pausar
Pulso Debug. hdb arranca, compila el
modulo, ejecuta foldrLazyDemo, maybeChainDemo, eitherChainDemo, imprime "Fin." y termina. Sin
parar en el breakpoint que puse en linea 47.
Activo Trace verbose en LSP4IJ y miro el wire protocol DAP. Veo:
[Trace] Received notification 'initialized'
[Trace] Sending request 'configurationDone'
No hay setBreakpoints
entre medias. El cliente envio configurationDone sin enviar breakpoints. Por
eso hdb corre el programa entero.
Causa: IntelliJ NO reconoce .hs como un
fileType (no tengo plugin Haskell instalado). Sin fileType reconocido,
los breakpoints quedan huerfanos: se crean en la UI pero LSP4IJ no los
asocia al adapter.
Trampa IntelliJ siete: el plugin de Rik no esta y Flexible Haskell
pide licencia
Marketplace de IntelliJ 2026.1 NO tiene intellij-haskell de Rik (plugin congelado).
Flexible Haskell (alternativa moderna)
requiere licencia comercial. Haskell LSP
de Rockofox se basa en HLS, y HLS NO soporta GHC 9.14.
Camino alternativo: registrar el fileType a mano.
Settings → Editor → File Types → "+"
Name: Haskell
Description: Haskell source
Line comment: --
Block comment start: {-
Block comment end: -}
Add file name patterns: *.hs, *.lhs
Pero algo aun mejor te puede pasar al añadir el patron *.hs:
Si te pasa eso, cancela la creacion manual del fileType y usa el que ya existe. Es la via mas barata.
Despues, en LSP4IJ DAP "Haskell hdb" → Mappings tab → File type →
añadir el fileType "Haskell" con language id haskell:
haskell. La X morada
al lado del nombre confirma que el fileType esta activo.Trampa IntelliJ ocho (la final, la que casi tira la toalla)
Configurado todo. Pulso Debug. EL PROGRAMA SIGUE SIN PARAR.
Activo trace verbose otra vez. Miro el launch request:
{
"request": "launch",
"projectRoot": "/home/passh/src/pensando-en-haskell",
"entryFile": "/home/passh/src/pensando-en-haskell/app/Debugging101.hs",
"entryPoint": "main",
"internalInterpreter": true,
"noDebug": true
}noDebug: true. En DAP eso
significa "ejecuta sin debugger activo, sin breakpoints, sin pausas". El
cliente NI siquiera envia setBreakpoints
cuando noDebug:true, porque el server los
ignoraria.
Que pasa? Estoy pulsando el boton Run en lugar del boton Debug.
En IntelliJ hay DOS botones contiguos en la barra:
- ▶ Run (triangulo verde) → envia
noDebug:true - 🐞 Debug (escarabajo verde) →
envia
noDebug:false
Atajo: Shift+F9 para Debug, no Shift+F10 que es Run.
Le doy a Debug. PARA EN EL BREAKPOINT. Panel de variables abre el
modulo. Veo primerosCincoParesAlCuadrado = {[Int]} _. Ese
guion bajo es un thunk SIN EVALUAR.
Conseguido.
La config para tu repo Haskell en IntelliJ
Una vez configurado el DAP "Haskell hdb" en LSP4IJ, IntelliJ te
ofrece "Store as project file" en la Run Configuration. Acepta. Eso
guarda la config en .idea/runConfigurations/ dentro del proyecto.
Cualquiera que clone tu repo y tenga LSP4IJ instalado va a tener el
debugger funcional sin tocar UI.
Resumen de los puntos clave si quieres replicarlo:
- Plugin
LSP4IJ(Red Hat, Marketplace 23257). Free. - fileType Haskell registrado: o via
Haskell LSPplugin (gratis, ignora HLS), o creandolo a mano en Settings → File Types. - LSP4IJ Run/Debug Configuration tipo "Debug Adapter Protocol":
- Server command:
bash -c "cd /ruta/proyecto && exec /ruta/.local/bin/hdb-dap server --port ${port}" - Connect by waiting: log pattern "Running DAP server on"
- Mappings → File type: tu fileType Haskell, language id
haskell - Configuration → DAP parameters JSON con
projectRoot,entryFile,entryPoint,internalInterpreter:trueen RUTAS ABSOLUTAS (${file}no se expande).
- Server command:
- Wrapper
hdb-dapque entra ennix developy pasa ruta absoluta dehdb. - PULSAR EL BOTON DEBUG, NO EL RUN.
Cierre breve
El post del lunes era una promesa. Este es la entrega doble. Tres dias desde "se puede" hasta "lo tengo en MIS DOS editores". El precio fueron quince trampas (siete en IntelliJ, ocho en Doom y NixOS) y la perdida temporal de HLS hasta que el ecosistema se ponga al dia con GHC 9.14.
Si tu tambien quieres probarlo: el flake esta en mi repo de
github con todo lo necesario. cabal install haskell-debugger y a volar. Si te
atascas, mirate las trampas de arriba: muy probablemente es una de
ellas.
Pascual: que venga dios y lo vea. El hito esta. A estudiar.
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario