BookinglyTech News
Software

Un parser de JSON sin AST intermedio: objetos a medio inicializar en Haskell

Arthi Chaud publica un metodo para deserializar JSON saltandose el arbol sintactico intermedio, con un contador de bits que marca que campos del objeto faltan por rellenar.

2 min de lecturaLobsters0 vistas

Casi todos los lenguajes tienen biblioteca para serializar y deserializar JSON, y casi todas pasan por una representacion intermedia: el AST del objeto. Arthi Chaud ha publicado un metodo para saltarselo, escrito en Haskell pero trasladable a Rust, que va rellenando un objeto a medio construir mientras parsea y lleva la cuenta de los campos pendientes con un contador de bits.

El coste del AST

El parser clasico encadena dos pasos. Primero convierte los bytes en un AST de JSON y despues una funcion como fromJSON lo traduce al tipo del dominio, por ejemplo un Album con titulo, artista y numero de canciones. Es comodo de mantener y se puede derivar automaticamente con Template Haskell o con Generics. El precio es una asignacion extra en memoria, porque el AST ocupa al menos tanto como el objeto final, mas el computo de recorrerlo todo otra vez.

La alternativa que plantea el autor es ir pasando un objeto parcialmente inicializado, el propio dato de dominio, y dejar que la funcion de parseo escriba sus campos al vuelo. En Haskell eso significa arrancar con los campos en undefined y confiar en la pereza del lenguaje para no evaluarlos hasta que esten puestos.

Suena horrible y el autor lo dice. El truco esta en acompanar el objeto con un bitset, un Word64 inicializado a maxBound, y limpiar el bit correspondiente a cada campo a medida que se rellena. Cuando el parser termina, basta con comprobar si el bitset llego a cero. Si no, se recorren los bits que quedan para construir un mensaje de error con los campos que faltaban; y si esos campos son de tipo Maybe, se ponen a Nothing y el parseo se puede dar por valido.

Esto obliga a que los campos del tipo de dominio no sean estrictos: evaluar un undefined revienta el programa, asi que no hay forma de comprobar la inicializacion mirando los valores.

Lo que queda abierto

El autor se hace cuatro preguntas y no responde todas. Si se puede eliminar de verdad la representacion intermedia, si la mejora de velocidad es apreciable, si se conserva la posibilidad de derivar el codigo automaticamente y que pasa con la seguridad, entendida aqui como capacidad de recuperarse de un campo ausente o de un tipo que no cuadra con la definicion. A eso se suma que el parser tiene que seguir saltandose los campos desconocidos que aparezcan en el objeto serializado.

El codigo esta en el repositorio serth, con un benchmark aparte para medir el resultado, y el autor cita flatparse y criterion como referencias del terreno. Los lenguajes con tipos algebraicos y meta-programacion por etapas, Rust entre ellos, podrian implementar el mismo enfoque. Falta por ver si el ahorro de memoria y de ciclos compensa la fragilidad de apoyarse en campos no evaluados: es una tecnica que depende del compilador y de las optimizaciones que este decida aplicar.