Monday, 5 December 2016

Gigamonkeys Binary Options

Copyright copy 2003-2005, Peter Seibel 14. Archivos y archivos I / O Common Lisp proporciona una rica biblioteca de funcionalidad para tratar archivos. En este capítulo, me concentraré en algunas tareas básicas relacionadas con los archivos: lectura y escritura de archivos y lista de archivos en el sistema de archivos. Para estas tareas básicas, Common Lisps I / O instalaciones son similares a los de otros idiomas. Common Lisp proporciona una abstracción de flujo para leer y escribir datos y una abstracción, llamada pathnames. Para manipular nombres de archivos de una manera independiente del sistema operativo. Además, Common Lisp proporciona otros bits de funcionalidad exclusiva de Lisp, como la capacidad de leer y escribir expresiones s. Lectura de datos de archivo La tarea de E / S de archivo más básica es leer el contenido de un archivo. Obtiene un flujo desde el que puede leer un contenido de archivos con la función OPEN. Por defecto OPEN devuelve un flujo de entrada basado en caracteres que puede pasar a una variedad de funciones que leen uno o más caracteres de texto: READ-CHAR lee un solo carácter READ-LINE lee una línea de texto, devolviéndola como una cadena con el Caracteres de fin de línea eliminados y READ lee una sola expresión s, devolviendo un objeto Lisp. Cuando termines con el flujo, puedes cerrarlo con la función CERRAR. El único argumento requerido para OPEN es el nombre del archivo a leer. Como veremos en la sección quotFilenames, Common Lisp proporciona un par de maneras de representar un nombre de archivo, pero lo más sencillo es usar una cadena que contenga el nombre en la sintaxis local de nomenclatura de archivos. Asumiendo que /some/file/name. txt es un archivo, puede abrirlo de esta manera: Puede utilizar el objeto devuelto como el primer argumento para cualquiera de las funciones de lectura. Por ejemplo, para imprimir la primera línea del archivo, puede combinar OPEN. LÍNEA DE LECTURA . Y CERRAR de la siguiente manera: Por supuesto, una serie de cosas pueden salir mal al intentar abrir y leer de un archivo. Es posible que el archivo no exista. O puede inesperadamente golpear el final del archivo mientras lee. Por defecto, OPEN y las funciones READ - indicarán un error en estas situaciones. En el capítulo 19, hablaré de cómo recuperarse de esos errores. Por ahora, sin embargo, hay una solución más ligera: cada una de estas funciones acepta argumentos que modifican su comportamiento en estas situaciones excepcionales. Si desea abrir un archivo posiblemente inexistente sin que OPEN señale un error, puede usar el argumento de palabra clave: if-does-not-exist para especificar un comportamiento diferente. Los tres valores posibles son: error. El valor predeterminado: create. Que le dice a seguir adelante y crear el archivo y luego proceder como si ya hubiera existido y NIL. Que le dice que devuelva NIL en lugar de una secuencia. Por lo tanto, puede cambiar el ejemplo anterior para hacer frente a la posibilidad de que el archivo no puede existir. Las funciones de lectura-- READ-CHAR. LÍNEA DE LECTURA . Y READ - todos toman un argumento opcional, que por defecto es true, que especifica si deben señalar un error si se llama al final del archivo. Si ese argumento es NIL. En su lugar devuelven el valor de su tercer argumento, cuyo valor predeterminado es NIL. Por lo tanto, puede imprimir todas las líneas en un archivo como este: De las tres funciones de lectura de texto, READ es exclusivo de Lisp. Esta es la misma función que proporciona el R en la REPL y que se utiliza para leer el código fuente Lisp. Cada vez que se llama, lee una sola expresión s, omite espacios en blanco y comentarios y devuelve el objeto Lisp denotado por la expresión s. Por ejemplo, supongamos que /some/file/name. txt tiene el siguiente contenido: En otras palabras, contiene cuatro expresiones s: una lista de números, un número, una cadena y una lista de listas. Puede leer esas expresiones como esta: Como se vio en el Capítulo 3, puede utilizar PRINT para imprimir objetos Lisp en formato quotreadablequot. Por lo tanto, siempre que necesite almacenar un poco de datos en un archivo, PRINT y READ proporcionan una manera fácil de hacerlo sin tener que diseñar un formato de datos o escribir un analizador. Incluso - como el ejemplo anterior demostrado - te dan comentarios de forma gratuita. Y debido a que las expresiones s fueron diseñadas para ser editable por humanos, también es un formato fino para cosas como archivos de configuración. 1 Lectura de datos binarios De forma predeterminada, OPEN devuelve secuencias de caracteres, que traducen los bytes subyacentes a caracteres de acuerdo con un esquema de codificación de caracteres particular. 2 Para leer los bytes sin procesar, es necesario pasar el argumento OPEN a: element-type de (unsigned-byte 8). 3 Puede pasar el flujo resultante a la función READ-BYTE. Que devolverá un número entero entre 0 y 255 cada vez que se llama. READ-BYTE. Como las funciones de lectura de caracteres, también acepta argumentos opcionales para especificar si debe señalar un error si se llama al final del archivo y qué valor devolver si no. En el Capítulo 24 construirás una biblioteca que te permitirá leer cómodamente los datos binarios estructurados usando READ-BYTE. 4 Lectura masiva Una última función de lectura, READ-SEQUENCE. Funciona con corrientes binarias y de caracteres. Se pasa una secuencia (normalmente un vector) y una secuencia, e intenta llenar la secuencia con datos de la secuencia. Devuelve el índice del primer elemento de la secuencia que no se llenó o la longitud de la secuencia si fue capaz de llenarla completamente. También puede pasar: start y: end argumentos de palabra clave para especificar una subsecuencia que debe llenarse en su lugar. El argumento de secuencia debe ser un tipo que puede contener elementos del tipo de elemento arroyos. Dado que la mayoría de los sistemas operativos soportan alguna forma de E / S de bloque, es probable que READ-SEQUENCE sea bastante más eficiente que llenar una secuencia llamando repetidamente READ-BYTE o READ-CHAR. Archivo de salida Para escribir datos en un archivo, necesita un flujo de salida, que obtiene llamando a OPEN con un argumento de dirección: direction de: output. Al abrir un archivo para la salida, OPEN asume que el archivo ya no existe y señalará un error si lo hace. Sin embargo, puede cambiar ese comportamiento con el argumento de palabra clave if-exists. Pasando el valor: supersede le indica a OPEN que reemplace el archivo existente. Passing: append hace que OPEN abra el archivo existente de forma que se escriban nuevos datos al final del archivo, mientras que: overwrite devuelve un flujo que sobrescribirá los datos existentes desde el principio del archivo. Y pasar NIL hará que OPEN devuelva NIL en lugar de una secuencia si el archivo ya existe. Un uso típico de OPEN para la salida tiene este aspecto: Common Lisp también proporciona varias funciones para escribir datos: WRITE-CHAR escribe un solo carácter en el flujo. WRITE-LINE escribe una cadena seguida de una nueva línea, que se mostrará como el carácter o caracteres de final de línea apropiados para la plataforma. Otra función, WRITE-STRING. Escribe una cadena sin añadir ningún carácter de fin de línea. Dos funciones diferentes pueden imprimir sólo una nueva línea: TERPRI --short para quotterminate printquot - imprime incondicionalmente un carácter de nueva línea y FRESH-LINE imprime un carácter de nueva línea a menos que la secuencia esté al principio de una línea. FRESH-LINE es útil cuando se desea evitar falsas líneas en blanco en la salida textual generada por diferentes funciones llamadas en secuencia. Por ejemplo, supongamos que tiene una función que genera una salida que siempre debe ser seguida por un salto de línea y otra que debe comenzar en una nueva línea. Pero supongamos que si las funciones se llaman una tras otra, no desea una línea en blanco entre los dos bits de salida. Si utiliza FRESH-LINE al principio de la segunda función, su salida siempre comenzará en una nueva línea, pero si se llama justo después de la primera, no emitirá un salto de línea adicional. Varias funciones emiten datos Lisp como expresiones s: PRINT imprime una expresión s precedida por un final de línea y seguida por un espacio. PRIN1 imprime sólo la expresión s. Y la función PPRINT imprime s-expresiones como PRINT y PRIN1 pero usando la impresora quotpretty, quot que intenta imprimir su salida de una manera estéticamente agradable. Sin embargo, no todos los objetos se pueden imprimir en un formulario que READ entienda. La variable PRINT-READABLY controla lo que sucede si intenta imprimir tal objeto con PRINT. PRIN1. O PPRINT. Cuando su NIL. Estas funciones imprime el objeto en una sintaxis especial thats garantizado para causar READ a señalar un error si intenta leerlo de otra manera señalarán un error algo que imprimen el objeto. Otra función, PRINC. También imprime objetos Lisp, pero de una manera diseñada para el consumo humano. Por ejemplo, PRINC imprime cadenas sin comillas. Puede generar salida de texto más elaborada con la increíblemente flexible aunque algo arcana función FORMAT. Hablaré de algunos de los detalles más importantes de FORMAT. Que básicamente define un mini-lenguaje para emitir salida formateada, en el Capítulo 18. Para escribir datos binarios en un archivo, debes ABRIR el archivo con el mismo argumento: element-type como hiciste para leerlo: (unsigned-byte 8 ). A continuación, puede escribir bytes individuales en el flujo con WRITE-BYTE. La función de salida masiva WRITE-SEQUENCE acepta secuencias binarias y de caracteres siempre y cuando todos los elementos de la secuencia sean de un tipo apropiado para el flujo, ya sea caracteres o bytes. Al igual que con READ-SEQUENCE. Esta función es probablemente bastante más eficiente que escribir los elementos de la secuencia uno a la vez. Cierre de archivos Como cualquier persona que ha escrito código que se ocupa de un montón de archivos sabe, es importante para cerrar los archivos cuando se hace con ellos, porque los manejadores de archivos tienden a ser un recurso escaso. Si abre archivos y no los cierra, pronto descubrirá que no puede abrir más archivos. 5 Podría parecer lo suficientemente simple como para estar seguro de que cada OPEN tiene un CERRAR coincidente. Por ejemplo, siempre podría estructurar su archivo utilizando código como este: Sin embargo, este enfoque sufre de dos problemas. Uno es simplemente que su error propenso - si se olvida de la CERRAR. El código emitirá un identificador de archivo cada vez que se ejecuta. El otro problema - y más importante - es que no hay garantía de que llegue al CLOSE. Por ejemplo, si el código anterior a CLOSE contiene un RETURN o RETURN-FROM. Usted podría dejar el LET sin cerrar la corriente. O bien, como se verá en el Capítulo 19, si alguno de los códigos anteriores a CLOSE señala un error, el control puede saltar del LET a un controlador de errores y nunca volver a cerrar el flujo. Common Lisp proporciona una solución general al problema de cómo asegurarse de que siempre se ejecuta cierto código: el operador especial UNWIND-PROTECT. Que discutiremos en el Capítulo 20. Sin embargo, debido a que el patrón de apertura de un archivo, hacer algo con el flujo resultante y, a continuación, cerrar el flujo es tan común, Common Lisp proporciona una macro, WITH-OPEN-FILE. Construido en la parte superior de UNWIND-PROTECT. Para encapsular este patrón. Esta es la forma básica: Los formularios en body-forms son evaluados con stream-var enlazado a un flujo de archivos abierto por una llamada a OPEN con open-arguments como sus argumentos. WITH-OPEN-FILE asegura entonces que el flujo en stream-var se cierra antes de devolver el formulario WITH-OPEN-FILE. Por lo tanto, puede escribir esto para leer una línea de un archivo: Para crear un nuevo archivo, puede escribir algo como esto: Probablemente utilice WITH-OPEN-FILE para el 90-99 por ciento del archivo I / O que usted hace - La única vez que necesita usar las llamadas ABIERTA y CERRADAS sin procesar es si necesita abrir un archivo en una función y mantener el flujo alrededor después de que la función regrese. En ese caso, debe tener cuidado de cerrar la secuencia de usted mismo, o los descriptores de archivos de fugas youll y, finalmente, puede llegar a ser incapaz de abrir más archivos. Nombres de archivo Hasta ahora has utilizado cadenas para representar nombres de archivo. Sin embargo, el uso de cadenas como nombres de archivos vincula su código a un sistema operativo y sistema de archivos particular. Del mismo modo, si programaticamente construye nombres de acuerdo con las reglas de un esquema de nomenclatura determinado (separar directorios con /, por ejemplo), también vincula su código a un sistema de archivos en particular. Para evitar este tipo de nonportability, Common Lisp proporciona otra representación de nombres de archivo: pathname objects. Los nombres de ruta representan los nombres de archivo de una manera estructurada que los hace fáciles de manipular sin atarlos a una sintaxis particular del nombre de archivo. Y la carga de traducir hacia adelante y hacia atrás entre cadenas en la sintaxis local - denominada namestrings - y pathnames se coloca en la implementación Lisp. Desafortunadamente, como con muchas abstracciones diseñadas para ocultar los detalles de sistemas subyacentes fundamentalmente diferentes, la abstracción de la ruta de acceso introduce sus propias complicaciones. Cuando se diseñaron las rutas de acceso, el conjunto de sistemas de archivos en uso general era bastante más abigarrado que los de uso común hoy en día. En consecuencia, algunos rincones y recovecos de la abstracción de la ruta de acceso no tienen mucho sentido si todo lo que te preocupa es representar los nombres de archivo de Unix o Windows. Sin embargo, una vez que entienda qué partes de la abstracción de ruta de acceso puede ignorar como artefactos de la historia evolutiva de los nombres de ruta, proporcionan una manera conveniente de manipular nombres de archivo. 6 La mayoría de los lugares en los que se llama un nombre de archivo, puede utilizar una cadena de nombres o una ruta de acceso. Cuál utilizar depende sobre todo de donde el nombre se originó. Los nombres de archivo proporcionados por el usuario - por ejemplo, como argumentos o como valores en archivos de configuración - suelen ser cadenas de nombres, ya que el usuario sabe en qué sistema operativo se están ejecutando y no debería preocuparse por los detalles de cómo Lisp representa los nombres de archivo. Pero los nombres de archivo generados mediante programación serán nombres de ruta, ya que los puede crear de forma portátil. Un flujo devuelto por OPEN también representa un nombre de archivo, es decir, el nombre de archivo que se utilizó originalmente para abrir el flujo. En conjunto, estos tres tipos se denominan colectivamente designadores de ruta. Todas las funciones integradas que esperan un argumento de nombre de archivo aceptan los tres tipos de designador de ruta. Por ejemplo, todos los lugares de la sección anterior en los que usó una cadena para representar un nombre de archivo, también podría haber pasado un objeto de ruta de acceso o una secuencia. Cómo llegamos aquí La diversidad histórica de los sistemas de archivos existentes en los años 70 y 80 puede ser fácil de olvidar. Kent Pitman, uno de los principales editores técnicos de la norma Common Lisp, describió la situación una vez en comp. lang. lisp (Message-ID: sfwzo74np6w. fsfworld. std) así: Los sistemas de archivos dominante en el momento en el diseño de Common Lisp Se realizó TOPS-10, TENEX, TOPS-20, VAX VMS, ATampT Unix, MIT Multics, MIT ITS, por no mencionar un montón de sistemas operativos mainframe. Algunos eran mayúsculas solamente, algunos mezclados, algunos eran sensibles a mayúsculas y minúsculas pero traducían casos (como CL). Algunos tenían dirs como archivos, otros no. Algunos tenían caracteres de cita para caracteres de archivo divertidos, otros no. Algunos tenían comodines, otros no. Algunos tenían: hasta en caminos relativos, algunos no. Algunos tenían dirs de la raíz namable, algunos didnt. Había sistemas de archivos sin directorios, sistemas de archivos con directorios no jerárquicos, sistemas de archivos sin tipos de archivos, sistemas de archivos sin versiones, sistemas de archivos sin dispositivos y así sucesivamente. Si nos fijamos en la abstracción de la ruta de acceso desde el punto de vista de cualquier sistema de archivos único, parece barroco. Sin embargo, si usted toma incluso dos sistemas de archivos similares como Windows y Unix, ya puede empezar a ver diferencias que el sistema de ruta de acceso puede ayudar a abstraer - Los nombres de archivo de Windows contienen una letra de unidad, por ejemplo, mientras que los nombres de archivo Unix no. La otra ventaja de tener la abstracción de ruta diseñada para manejar la amplia variedad de sistemas de archivos que existían en el pasado es que es más probable que sea capaz de manejar sistemas de archivos que puedan existir en el futuro. Si, por ejemplo, los sistemas de archivos de versiones vuelven a estar de moda, Common Lisp estará listo. Cómo los nombres de ruta representan nombres de archivo Un nombre de ruta es un objeto estructurado que representa un nombre de archivo que utiliza seis componentes: host, dispositivo, directorio, nombre, tipo y versión. La mayoría de estos componentes toman valores atómicos, normalmente las cadenas sólo el componente de directorio se estructura más, conteniendo una lista de nombres de directorio (como cadenas) prefaciado con la palabra clave: absolute o: relative. Sin embargo, no todos los componentes de la ruta de acceso son necesarios en todas las plataformas, esta es una de las razones por las que los nombres de ruta afectan a muchos nuevos Lispers como gratuitos. Por otro lado, usted realmente no necesita preocuparse acerca de qué componentes pueden o no pueden usarse para representar nombres en un sistema de archivos en particular a menos que necesite crear un nuevo objeto de nombre de ruta desde cero, lo cual casi nunca necesitará hacer. En su lugar, usualmente obtendrá los objetos pathname dejando que la implementación analice una cadena de nombres específica de sistema de archivos en un objeto pathname o creando una nueva ruta que tome la mayoría de sus componentes de una ruta existente. Por ejemplo, para traducir una cadena de nombres a una ruta de acceso, utilice la función PATHNAME. Se necesita un designador de ruta de acceso y devuelve un objeto de nombre de ruta equivalente. Cuando el designador ya es una ruta de acceso, simplemente se devuelve. Cuando es una secuencia, el nombre de archivo original es extraído y devuelto. Sin embargo, cuando el designador es una cadena de nombres, se analiza de acuerdo con la sintaxis del nombre de archivo local. El estándar de lenguaje, como un documento de plataforma neutral, no especifica ninguna asignación particular de la cadena de nombres a la ruta de acceso, pero la mayoría de las implementaciones siguen las mismas convenciones en un sistema operativo dado. En sistemas de archivos Unix, sólo se usan los componentes de directorio, nombre y tipo. En Windows, un componente más, normalmente el dispositivo o el host, contiene la letra de unidad. En estas plataformas, se analiza una cadena de nombres dividiéndola primero en elementos en el separador de ruta - una barra en Unix y una barra diagonal o barra invertida en Windows. La letra de unidad en Windows se colocará en el dispositivo o en el componente host. Todos excepto el último de los otros elementos de nombre se colocan en una lista que comienza con: absoluto o: relativo dependiendo de si el nombre (ignorando la letra de unidad, si la hubo) comenzó con un separador de ruta. Esta lista se convierte en el componente de directorio del nombre de ruta. El último elemento se divide en el punto más a la derecha, si lo hay, y las dos partes se ponen en el nombre y el tipo de componentes de la ruta. 7 Puede examinar estos componentes individuales de un pathname con las funciones PATHNAME-DIRECTORY. PATHNAME-NAME. Y PATHNAME-TYPE. Tres otras funciones-- PATHNAME-HOST. PATHNAME-DEVICE. Y PATHNAME-VERSION - le permiten obtener los otros tres componentes de la ruta, aunque es poco probable que tengan valores interesantes en Unix. En Windows, PATHNAME-HOST o PATHNAME-DEVICE devolverán la letra de la unidad. Como muchos otros objetos incorporados, los pathnames tienen su propia sintaxis de lectura, p seguida de una cadena de comillas dobles. Esto le permite imprimir y leer s-expresiones que contienen objetos pathname, pero debido a que la sintaxis depende del algoritmo de análisis de cadena de nombres, tales datos no son necesariamente portátiles entre sistemas operativos. Para traducir un nombre de ruta a una cadena de nombres - por ejemplo, para presentar al usuario - puede utilizar la función NAMESTRING. Que toma un designador de ruta y devuelve una cadena de nombres. Otras dos funciones, DIRECTORY-NAMESTRING y FILE-NAMESTRING. Devuelve una cadena de nombres parcial. DIRECTORY-NAMESTRING combina los elementos del componente de directorio en un nombre de directorio local y FILE-NAMESTRING combina los componentes de nombre y tipo. 8 Construcción de nuevos nombres de rutas Puede construir nombres de rutas arbitrarios mediante la función MAKE-PATHNAME. Se necesita un argumento de palabra clave para cada componente de nombre de ruta y devuelve un nombre de ruta con los componentes suministrados rellenados y el resto NIL. Sin embargo, si desea que sus programas sean portátiles, probablemente no desee crear nombres de ruta completamente desde cero: a pesar de que la abstracción de ruta de acceso le protege de la sintaxis de nombre de archivo no transportable, los nombres de archivo pueden ser no contables de otras maneras. Por ejemplo, el nombre de archivo /home/peter/foo. txt no es bueno en un cuadro OS X donde / home / se llama / Users /. Otra razón para no hacer nombres de ruta completamente desde cero es que implementaciones diferentes usan los componentes de ruta de acceso de forma ligeramente diferente. Por ejemplo, como se mencionó anteriormente, algunas implementaciones de Lisp basadas en Windows almacenan la letra de unidad en el componente de dispositivo mientras que otras la almacenan en el componente de host. Si escribe código como este: será correcto en algunas implementaciones pero no en otras. En lugar de crear nombres desde cero, puede crear un nuevo nombre de ruta basado en una ruta de acceso existente con el parámetro de palabra clave MAKE-PATHNAME s: defaults. Con este parámetro puede proporcionar un designador de ruta, que proporcionará los valores para cualquier componente no especificado por otros argumentos. Por ejemplo, la siguiente expresión crea un nombre de ruta con una extensión. html y todos los demás componentes son iguales que el nombre de ruta en el archivo de entrada de variable: Asumiendo que el valor en el archivo de entrada era un nombre proporcionado por el usuario, este código será robusto en La cara del sistema operativo y las diferencias de implementación, como si los nombres de archivo tienen letras de unidad en ellos y donde se almacenan en una ruta de acceso si lo hacen. 10 Puede utilizar la misma técnica para crear una ruta con un componente de directorio diferente. Sin embargo, esto creará una ruta de acceso cuyo componente de directorio completo es el directorio relativo backups /. Independientemente de cualquier archivo de entrada de componente de directorio que pueda haber tenido. Por ejemplo: A veces, sin embargo, desea combinar dos nombres de ruta, al menos uno de los cuales tiene un componente de directorio relativo, combinando sus componentes de directorio. Por ejemplo, suponga que tiene una ruta de acceso relativa como pquotfoo / bar. htmlquot que desea combinar con una ruta absoluta como pquot / www / html / quot para obtener pquot / www / html / foo / bar. htmlquot. En ese caso, MAKE-PATHNAME no lo hará en su lugar, desea MERGE-PATHNAMES. MERGE-PATHNAMES toma dos nombres de ruta y los fusiona, rellenando cualquier componente NIL del primer nombre de ruta con el valor correspondiente del segundo nombre de ruta, al igual que MAKE-PATHNAME rellena cualquier componente no especificado con componentes del argumento: defaults. Sin embargo, MERGE-PATHNAMES trata especialmente el componente de directorio: si el primer directorio de nombres de rutas es relativo, el componente de directorio de la ruta de acceso resultante será el primer directorio de nombres de rutas relativo al segundo directorio de nombres de rutas. Así: El segundo nombre de ruta también puede ser relativo, en cuyo caso la ruta resultante también será relativa. Para invertir este proceso y obtener un nombre de archivo relativo a un directorio raíz concreto, puede utilizar la función útil ENOUGH-NAMESTRING. A continuación, puede combinar ENOUGH-NAMESTRING con MERGE-PATHNAMES para crear una ruta que represente el mismo nombre pero en una raíz diferente. MERGE-PATHNAMES también se utiliza internamente por las funciones estándar que realmente tienen acceso a archivos en el sistema de archivos para completar nombres de ruta incompletos. Por ejemplo, supongamos que haces un pathname con sólo un nombre y un tipo. Si intenta utilizar esta ruta de acceso como un argumento a OPEN. Los componentes que faltan, como el directorio, deben rellenarse antes de que Lisp pueda traducir la ruta a un nombre de archivo real. Common Lisp obtendrá valores para los componentes faltantes al fusionar la ruta de acceso dada con el valor de la variable DEFAULT-PATHNAME-DEFAULTS. El valor inicial de esta variable es determinado por la implementación, pero normalmente es un nombre de ruta con un componente de directorio que representa el directorio donde Lisp se inició y los valores apropiados para el host y los componentes del dispositivo, si es necesario. Si se invoca con un solo argumento, MERGE-PATHNAMES combinará el argumento con el valor de DEFAULT-PATHNAME-DEFAULTS. Por ejemplo, si DEFAULT-PATHNAME-DEFAULTS es pquot / home / peter / quot. Entonces youd obtener lo siguiente: Dos Representaciones de Nombres de Directorio Cuando se trata de nombres de ruta que el nombre de directorios, es necesario tener en cuenta una arruga. Los nombres de ruta separan los componentes de directorio y nombre, pero Unix y Windows consideran los directorios sólo otro tipo de archivo. Por lo tanto, en esos sistemas, cada directorio tiene dos representaciones de nombre de ruta diferentes. Una representación, la cual forma de archivo de llamada enferma. Trata un directorio como cualquier otro archivo y coloca el último elemento de la cadena de nombres en el nombre y los componentes de tipo. La otra representación, forma de directorio. Coloca todos los elementos del nombre en el componente de directorio, dejando el nombre y los componentes de tipo NIL. Si / foo / bar / es un directorio, ambos nombres de rutas lo llaman. Cuando crea rutas de acceso con MAKE-PATHNAME. Usted puede controlar qué forma usted consigue, pero usted necesita ser cuidadoso al ocuparse de las cadenas de nombres. Todas las implementaciones actuales crean nombres de ruta de formulario de archivo a menos que la cadena de nombres termine con un separador de ruta. Pero no puedes confiar en las cadenas de nombres suministradas por el usuario necesariamente en una forma u otra. Por ejemplo, supongamos que ha solicitado al usuario un directorio para guardar un archivo e introdujeron quot / home / peterquot. Si pasa ese valor como el argumento defaults de MAKE-PATHNAME así: terminará guardando el archivo en /home/foo. txt en lugar del /home/peter/foo. txt deseado porque el quotpeterquot en la cadena de nombres será Se coloca en el componente de nombre cuando el nombre proporcionado por el usuario se convierte en una ruta de acceso. En la biblioteca de portabilidad de nombres de rutas que trataremos en el próximo capítulo, escribirá una función denominada pathname-as-directory que convierte una ruta de acceso en forma de directorio. Con esta función puede guardar el archivo de forma fiable en el directorio indicado por el usuario. Interacción con el sistema de archivos Aunque la interacción más común con el sistema de archivos es probablemente OPEN archivos de lectura y escritura, también desea ocasionalmente probar si existe un archivo, listar el contenido de un directorio, borrar y renombrar archivos, crear directorios, Y obtener información acerca de un archivo como quién la posee, cuándo fue modificada por última vez y su longitud. Aquí es donde la generalidad de la abstracción de pathname comienza a causar un poco de dolor: porque el estándar de lenguaje no especifica cómo las funciones que interactúan con el mapa de sistema de archivos a cualquier sistema de archivos específico, los ejecutores se quedan con un poco de margen de maniobra. Dicho esto, la mayoría de las funciones que interactúan con el sistema de archivos siguen siendo bastante sencillas. Ill discutir las funciones estándar aquí y señalar a los que sufren de nonportability entre las implementaciones. En el capítulo siguiente, desarrollará una biblioteca de portabilidad de rutas para suavizar algunos de esos problemas de no compatibilidad. Para probar si existe un archivo en el sistema de archivos correspondiente a un designador de ruta de acceso - un nombre de ruta, una cadena de nombres o una secuencia de archivos - puede utilizar la función PROBE-FILE. Si existe el archivo nombrado por el designador de ruta, PROBE-FILE devuelve los archivos truename. Un nombre de ruta con cualquier traducciones a nivel de sistema de archivos, como la resolución de enlaces simbólicos realizados. De lo contrario, devuelve NIL. Sin embargo, no todas las implementaciones admiten utilizar esta función para probar si existe un directorio. Además, Common Lisp no proporciona una forma portátil para probar si un archivo dado que existe es un archivo regular o un directorio. En el capítulo siguiente, se insertará PROBE-FILE con una nueva función, file-exists-p. Que puede probar si existe un directorio y decirle si un nombre dado es el nombre de un archivo o directorio. Del mismo modo, la función estándar para la lista de archivos en el sistema de archivos, DIRECTORY. Funciona bien para casos simples, pero las diferencias entre las implementaciones hacen que sea difícil de usar de forma portátil. En el capítulo siguiente definirá una función de directorio de listas que suaviza algunas de estas diferencias. DELETE-FILE y RENAME-FILE hacen lo que sus nombres sugieren. DELETE-FILE toma un designador de ruta y elimina el archivo con nombre, devolviendo true si tiene éxito. De lo contrario, señala un FILE-ERROR. 11 RENAME-FILE toma dos designadores de ruta y cambia el nombre del archivo con el nombre al segundo nombre. Puede crear directorios con la función ENSURE-DIRECTORIES-EXIST. Se necesita un designador de ruta y se asegura de que todos los elementos del componente de directorio existan y son directorios, creándolos según sea necesario. Devuelve el nombre de ruta que pasó, lo que hace que sea conveniente utilizar en línea. Tenga en cuenta que si pasa ENSURE-DIRECTORIES-EXIST un nombre de directorio, debe estar en forma de directorio, o el directorio de hoja no será creado. Las funciones FILE-WRITE-DATE y FILE-AUTHOR toman un designador de ruta. FILE-WRITE-DATE devuelve la hora en número de segundos desde la medianoche del 1 de enero de 1900, la hora media de Greenwich (GMT), que el archivo fue escrito por última vez y FILE-AUTHOR devuelve, en Unix y Windows, el propietario del archivo. 12 Para encontrar la longitud de un archivo, puede utilizar la función FILE-LENGTH. Por razones históricas FILE-LENGTH toma una secuencia como un argumento en lugar de un pathname. En teoría, esto permite que FILE-LENGTH devuelva la longitud en términos del tipo de elemento del flujo. Sin embargo, dado que en la mayoría de los sistemas operativos actuales, la única información disponible sobre la longitud de un archivo, a falta de leer el archivo completo para medirlo, es su longitud en bytes, eso es lo que devuelve la mayoría de las implementaciones, incluso cuando FILE-LENGTH Se pasa un flujo de caracteres. Sin embargo, el estándar no requiere este comportamiento, por lo que para obtener resultados predecibles, la mejor manera de obtener la longitud de un archivo es utilizar un flujo binario. 13 Una función relacionada que también toma un flujo de archivo abierto como su argumento es FILE-POSITION. Cuando se llama con sólo una secuencia, esta función devuelve la posición actual en el archivo: el número de elementos que se han leído o escrito en la secuencia. Cuando se llama con dos argumentos, el flujo y un designador de posición, establece la posición del flujo en la posición designada. El designador de posición debe ser la palabra clave: start. La palabra clave: end. O un entero no negativo. Las dos palabras clave establecen la posición de la secuencia al principio o al final del archivo mientras un número entero se mueve a la posición indicada en el archivo. Con un flujo binario la posición es simplemente un desplazamiento de bytes en el archivo. Sin embargo, para las corrientes de caracteres las cosas son un poco más complicadas debido a problemas de codificación de caracteres. Su mejor apuesta, si necesita saltar dentro de un archivo de datos de texto, es sólo pasar, como un segundo argumento a la versión de dos argumentos de FILE-POSITION. Un valor devuelto previamente por la versión de un argumento de FILE-POSITION con el mismo argumento de flujo. Otros tipos de E / S Además de los flujos de archivos, Common Lisp soporta otros tipos de flujos, que también se pueden utilizar con las diversas funciones de lectura, escritura e impresión de E / S. Por ejemplo, puede leer datos de, o escribir datos en, una cadena utilizando STRING-STREAM s, que puede crear con las funciones MAKE-STRING-INPUT-STREAM y MAKE-STRING-OUTPUT-STREAM. MAKE-STRING-INPUT-STREAM toma una cadena y los índices de inicio y fin opcionales para enlazar el área de la cadena a partir de la cual se deben leer los datos y devuelve una secuencia de caracteres que puede pasar a cualquiera de las funciones de entrada basadas en caracteres como READ - CHAR. LÍNEA DE LECTURA . O LEER. Por ejemplo, si tiene una cadena que contiene un literal de coma flotante en la sintaxis Common Lisps, puede convertirla en un flotador como este: De manera similar, MAKE-STRING-OUTPUT-STREAM crea una secuencia que puede utilizar con FORMAT. IMPRESIÓN . ESCRIBIR-CHAR. LÍNEA DE ESCRITURA . y así. No toma argumentos. Sea lo que sea que escriba, una secuencia de salida de cadena se acumulará en una cadena que luego se obtendrá con la función GET-OUTPUT-STREAM-STRING. Cada vez que llame GET-OUTPUT-STREAM-STRING. the streams internal string is cleared so you can reuse an existing string output stream. However, youll rarely use these functions directly, because the macros WITH-INPUT-FROM-STRING and WITH-OUTPUT-TO-STRING provide a more convenient interface. WITH-INPUT-FROM-STRING is similar to WITH-OPEN-FILE --it creates a string input stream from a given string and then executes the forms in its body with the stream bound to the variable you provide. For instance, instead of the LET form with the explicit UNWIND-PROTECT . youd probably write this: The WITH-OUTPUT-TO-STRING macro is similar: it binds a newly created string output stream to a variable you name and then executes its body. After all the body forms have been executed, WITH-OUTPUT-TO-STRING returns the value that would be returned by GET-OUTPUT-STREAM-STRING . The other kinds of streams defined in the language standard provide various kinds of stream quotplumbing, quot allowing you to plug together streams in almost any configuration. A BROADCAST-STREAM is an output stream that sends any data written to it to a set of output streams provided as arguments to its constructor function, MAKE-BROADCAST-STREAM . 14 Conversely, a CONCATENATED-STREAM is an input stream that takes its input from a set of input streams, moving from stream to stream as it hits the end of each stream. CONCATENATED-STREAM s are constructed with the function MAKE-CONCATENATED-STREAM . which takes any number of input streams as arguments. Two kinds of bidirectional streams that can plug together streams in a couple ways are TWO-WAY-STREAM and ECHO-STREAM . Their constructor functions, MAKE-TWO-WAY-STREAM and MAKE-ECHO-STREAM . both take two arguments, an input stream and an output stream, and return a stream of the appropriate type, which you can use with both input and output functions. In a TWO-WAY-STREAM every read you perform will return data read from the underlying input stream, and every write will send data to the underlying output stream. An ECHO-STREAM works essentially the same way except that all the data read from the underlying input stream is also echoed to the output stream. Thus, the output stream of an ECHO-STREAM stream will contain a transcript of both sides of the conversation. Using these five kinds of streams, you can build almost any topology of stream plumbing you want. Finally, although the Common Lisp standard doesnt say anything about networking APIs, most implementations support socket programming and typically implement sockets as another kind of stream, so you can use all the regular I/O functions with them. 15 Now youre ready to move on to building a library that smoothes over some of the differences between how the basic pathname functions behave in different Common Lisp implementations. 1 Note, however, that while the Lisp reader knows how to skip comments, it completely skips them. Thus, if you use READ to read in a configuration file containing comments and then use PRINT to save changes to the data, youll lose the comments. 2 By default OPEN uses the default character encoding for the operating system, but it also accepts a keyword parameter. external-format. that can pass implementation-defined values that specify a different encoding. Character streams also translate the platform-specific end-of-line sequence to the single character Newline . 3 The type (unsigned-byte 8) indicates an 8-bit byte Common Lisp quotbytequot types arent a fixed size since Lisp has run at various times on architectures with byte sizes from 6 to 9 bits, to say nothing of the PDP-10, which had individually addressable variable-length bit fields of 1 to 36 bits. 4 In general, a stream is either a character stream or a binary stream, so you cant mix calls to READ-BYTE and READ-CHAR or other character-based read functions. However, some implementations, such as Allegro, support so-called bivalent streams, which support both character and binary I/O. 5 Some folks expect this wouldnt be a problem in a garbage-collected language such as Lisp. It is the case in most Lisp implementations that a stream that becomes garbage will automatically be closed. However, this isnt something to rely on--the problem is that garbage collectors usually run only when memory is low they dont know about other scarce resources such as file handles. If theres plenty of memory available, its easy to run out of file handles long before the garbage collector runs. 6 Another reason the pathname system is considered somewhat baroque is because of the inclusion of logical pathnames . However, you can use the rest of the pathname system perfectly well without knowing anything more about logical pathnames than that you can safely ignore them. Briefly, logical pathnames allow Common Lisp programs to contain references to pathnames without naming specific files. Logical pathnames could then be mapped to specific locations in an actual file system when the program was installed by defining a quotlogical pathname translationquot that translates logical pathnames matching certain wildcards to pathnames representing files in the file system, so-called physical pathnames. They have their uses in certain situations, but you can get pretty far without worrying about them. 7 Many Unix-based implementations treat filenames whose last element starts with a dot and dont contain any other dots specially, putting the whole element, with the dot, in the name component and leaving the type component NIL . However, not all implementations follow this convention some will create a pathname with quotquot as the name and emacs as the type. 8 The name returned by FILE-NAMESTRING also includes the version component on file systems that use it. 9 The host component may not default to NIL . but if not, it will be an opaque implementation-defined value. 10 For absolutely maximum portability, you should really write this: Without the :version argument, on a file system with built-in versioning, the output pathname would inherit its version number from the input file which isnt likely to be right--if the input file has been saved many times it will have a much higher version number than the generated HTML file. On implementations without file versioning, the :version argument should be ignored. Its up to you if you care that much about portability. 11 See Chapter 19 for more on handling errors. 12 For applications that need access to other file attributes on a particular operating system or file system, libraries provide bindings to underlying C system calls. The Osicat library at common-lisp. net/project/osicat/ provides a simple API built using the Universal Foreign Function Interface (UFFI), which should run on most Common Lisps that run on a POSIX operating system. 13 The number of bytes and characters in a file can differ even if youre not using a multibyte character encoding. Because character streams also translate platform-specific line endings to a single Newline character, on Windows (which uses CRLF as its line ending) the number of characters will typically be smaller than the number of bytes. If you really have to know the number of characters in a file, you have to bite the bullet and write something like this: or maybe something more efficient like this: 14 MAKE-BROADCAST-STREAM can make a data black hole by calling it with no arguments. 15 The biggest missing piece in Common Lisps standard I/O facilities is a way for users to define new stream classes. There are, however, two de facto standards for user-defined streams. During the Common Lisp standardization, David Gray of Texas Instruments wrote a draft proposal for an API to allow users to define new stream classes. Unfortunately, there wasnt time to work out all the issues raised by his draft to include it in the language standard. However, many implementations support some form of so-called Gray Streams, basing their API on Grays draft proposal. Another, newer API, called Simple Streams, has been developed by Franz and included in Allegro Common Lisp. It was designed to improve the performance of user-defined streams relative to Gray Streams and has been adopted by some of the open-source Common Lisp implementations. Recruitment Challenges Staffing Firms faces and Opportunities that Automated ATS Brings With the dynamism that each industry is turning to and the paradigm shift in customer demand and needs, it becomes imperative to automate the applicant tracking system. Cada agencia de reclutamiento tiene su propio conjunto de necesidades empresariales únicas que un ATS personalizado puede cumplir. Albeit, the ultimate objective remains same Increase in productivity by automating workflows, business processing and faster and accurate communication channels to catapult bottom line profitability No two businesses have similar business needs There is no one-size-fits-all solution in any industry and recruitment is no exception. To start with, lets have a sneak peek at the challenges involved in current staffing industry and the opportunities that an automated ATS can offer Challenges Involve and Opportunities Increasing the database repository of candidates in general There are several staffing companies still striving to build an adequate Candidate Portal that can hold the candidates resumes via the staffing companys website. It takes lot of effort, time and manual productive hours leaving less time for other activities. A well automated and feature-rich ATS like TargetRecruit can make the task simpler and smoother. It can develop a candidate portal (UI) and start collecting resumes, cover letters and other related documents instantly from the website and automatically generates candidate profiles without requiring any manual intervention. También puede publicar órdenes de trabajo abiertas desde su sistema a varias tarjetas de trabajo en línea en cuestión de segundos. Effective candidate search in specific global markets The entire staffing industry is facing tough time in searching for right candidate in given target market. This is where customized ATS can come to rescue. It requires powerful, accurate and delineated system that can instantly find the potential talent in seconds. Several refined and Boolean searches can instantly bring the right talents before you leveraging enhanced search filters. Fast end-to-end placement time Staffing professionals are facing tough time in expediting the entire process of recruitment these days. Effective ATS can alleviate this processing with a fully automated system including well defined search functionality, multiple job board integration, and streamlined communications. Candidatos abiertos a la competencia externa Los candidatos pueden ser atraídos por sus competidores en caso de que su ATS no sea efectivo. This can again be consolidated by completely foolproof and automated applicant tracking systems that can quickly search and contact potential talents through automated voice and text messaging system. One-click functionality can instantly search, schedule interviews, and notify active and passive candidates for newly acquired open jobs keeping them in your system pipeline. This is yet a far-fetched dream for several staffing firms to provide quality talents to their clients. The right ATS can automatically sync key skill sets of specific candidates with given job criteria and can bring the most suitable talent for specific jobs. Buscar y ponerse en contacto con posibles candidatos Estar en contacto con el talento adecuado es una tarea hercúlea dada la gran cantidad de datos de los candidatos. This can well be done using an effective tracking system that can enable you easily track and automatically sent messages utilizing social media outreach features. This is what competent and automated ATS can do for you. Accurate data mining, analysis, instant communication and bringing fast and accurate search results are the key to automated and modern ATS. It can save your precious time and efforts that you can spend in other more productive activities. So, are you ready to take this opportunity, leaving behind all time consuming and tiring recruitment processes Time is now Post navigationCopyright copy 2003-2005, Peter Seibel 24. Practical: Parsing Binary Files In this chapter Ill show you how to build a library that you can use to write code for reading and writing binary files. Youll use this library in Chapter 25 to write a parser for ID3 tags, the mechanism used to store metadata such as artist and album names in MP3 files. This library is also an example of how to use macros to extend the language with new constructs, turning it into a special-purpose language for solving a particular problem, in this case reading and writing binary data. Because youll develop the library a bit at a time, including several partial versions, it may seem youre writing a lot of code. But when all is said and done, the whole library is fewer than 150 lines of code, and the longest macro is only 20 lines long. Binary Files At a sufficiently low level of abstraction, all files are quotbinaryquot in the sense that they just contain a bunch of numbers encoded in binary form. However, its customary to distinguish between text files . where all the numbers can be interpreted as characters representing human-readable text, and binary files . which contain data that, if interpreted as characters, yields nonprintable characters. 1 Binary file formats are usually designed to be both compact and efficient to parse--thats their main advantage over text-based formats. To meet both those criteria, theyre usually composed of on-disk structures that are easily mapped to data structures that a program might use to represent the same data in memory. 2 The library will give you an easy way to define the mapping between the on-disk structures defined by a binary file format and in-memory Lisp objects. Using the library, it should be easy to write a program that can read a binary file, translating it into Lisp objects that you can manipulate, and then write back out to another properly formatted binary file. Binary Format Basics The starting point for reading and writing binary files is to open the file for reading or writing individual bytes. As I discussed in Chapter 14, both OPEN and WITH-OPEN-FILE accept a keyword argument. element-type. that controls the basic unit of transfer for the stream. When youre dealing with binary files, youll specify (unsigned-byte 8). An input stream opened with such an :element-type will return an integer between 0 and 255 each time its passed to READ-BYTE . Conversely, you can write bytes to an (unsigned-byte 8) output stream by passing numbers between 0 and 255 to WRITE-BYTE . Above the level of individual bytes, most binary formats use a smallish number of primitive data types--numbers encoded in various ways, textual strings, bit fields, and so on--which are then composed into more complex structures. So your first task is to define a framework for writing code to read and write the primitive data types used by a given binary format. To take a simple example, suppose youre dealing with a binary format that uses an unsigned 16-bit integer as a primitive data type. To read such an integer, you need to read the two bytes and then combine them into a single number by multiplying one byte by 256, a. k.a. 28, and adding it to the other byte. For instance, assuming the binary format specifies that such 16-bit quantities are stored in big-endian 3 form, with the most significant byte first, you can read such a number with this function: However, Common Lisp provides a more convenient way to perform this kind of bit twiddling. The function LDB . whose name stands for load byte, can be used to extract and set (with SETF ) any number of contiguous bits from an integer. 4 The number of bits and their position within the integer is specified with a byte specifier created with the BYTE function. BYTE takes two arguments, the number of bits to extract (or set) and the position of the rightmost bit where the least significant bit is at position zero. LDB takes a byte specifier and the integer from which to extract the bits and returns the positive integer represented by the extracted bits. Thus, you can extract the least significant octet of an integer like this: To get the next octet, youd use a byte specifier of (byte 8 8) like this: You can use LDB with SETF to set the specified bits of an integer stored in a SETF able place. Thus, you can also write read-u2 like this: 5 To write a number out as a 16-bit integer, you need to extract the individual 8-bit bytes and write them one at a time. To extract the individual bytes, you just need to use LDB with the same byte specifiers. Of course, you can also encode integers in many other ways--with different numbers of bytes, with different endianness, and in signed and unsigned format. Strings in Binary Files Textual strings are another kind of primitive data type youll find in many binary formats. When you read files one byte at a time, you cant read and write strings directly--you need to decode and encode them one byte at a time, just as you do with binary-encoded numbers. And just as you can encode an integer in several ways, you can encode a string in many ways. To start with, the binary format must specify how individual characters are encoded. To translate bytes to characters, you need to know both what character code and what character encoding youre using. A character code defines a mapping from positive integers to characters. Each number in the mapping is called a code point . For instance, ASCII is a character code that maps the numbers from 0-127 to particular characters used in the Latin alphabet. A character encoding, on the other hand, defines how the code points are represented as a sequence of bytes in a byte-oriented medium such as a file. For codes that use eight or fewer bits, such as ASCII and ISO-8859-1, the encoding is trivial--each numeric value is encoded as a single byte. Nearly as straightforward are pure double-byte encodings, such as UCS-2, which map between 16-bit values and characters. The only reason double-byte encodings can be more complex than single-byte encodings is that you may also need to know whether the 16-bit values are supposed to be encoded in big-endian or little-endian format. Variable-width encodings use different numbers of octets for different numeric values, making them more complex but allowing them to be more compact in many cases. For instance, UTF-8, an encoding designed for use with the Unicode character code, uses a single octet to encode the values 0-127 while using up to four octets to encode values up to 1,114,111. 6 Since the code points from 0-127 map to the same characters in Unicode as they do in ASCII, a UTF-8 encoding of text consisting only of characters also in ASCII is the same as the ASCII encoding. On the other hand, texts consisting mostly of characters requiring four bytes in UTF-8 could be more compactly encoded in a straight double-byte encoding. Common Lisp provides two functions for translating between numeric character codes and character objects: CODE-CHAR . which takes an numeric code and returns as a character, and CHAR-CODE . which takes a character and returns its numeric code. The language standard doesnt specify what character encoding an implementation must use, so theres no guarantee you can represent every character that can possibly be encoded in a given file format as a Lisp character. However, almost all contemporary Common Lisp implementations use ASCII, ISO-8859-1, or Unicode as their native character code. Because Unicode is a superset ofISO-8859-1, which is in turn a superset of ASCII, if youre using a Unicode Lisp, CODE-CHAR and CHAR-CODE can be used directly for translating any of those three character codes. 7 In addition to specifying a character encoding, a string encoding must also specify how to encode the length of the string. Three techniques are typically used in binary file formats. The simplest is to not encode it but to let it be implicit in the position of the string in some larger structure: a particular element of a file may always be a string of a certain length, or a string may be the last element of a variable-length data structure whose overall size determines how many bytes are left to read as string data. Both these techniques are used in ID3 tags, as youll see in the next chapter. The other two techniques can be used to encode variable-length strings without relying on context. One is to encode the length of the string followed by the character data--the parser reads an integer value (in some specified integer format) and then reads that number of characters. Another is to write the character data followed by a delimiter that cant appear in the string such as a null character. The different representations have different advantages and disadvantages, but when youre dealing with already specified binary formats, you wont have any control over which encoding is used. However, none of the encodings is particularly more difficult to read and write than any other. Here, as an example, is a function that reads a null-terminated ASCII string, assuming your Lisp implementation uses ASCII or one of its supersets such as ISO-8859-1 or full Unicode as its native character encoding: The WITH-OUTPUT-TO-STRING macro, which I mentioned in Chapter 14, is an easy way to build up a string when you dont know how long itll be. It creates a STRING-STREAM and binds it to the variable name specified, s in this case. All characters written to the stream are collected into a string, which is then returned as the value of the WITH-OUTPUT-TO-STRING form. To write a string back out, you just need to translate the characters back to numeric values that can be written with WRITE-BYTE and then write the null terminator after the string contents. As these examples show, the main intellectual challenge--such as it is--of reading and writing primitive elements of binary files is understanding how exactly to interpret the bytes that appear in a file and to map them to Lisp data types. If a binary file format is well specified, this should be a straightforward proposition. Actually writing functions to read and write a particular encoding is, as they say, a simple matter of programming. Now you can turn to the issue of reading and writing more complex on-disk structures and how to map them to Lisp objects. Composite Structures Since binary formats are usually used to represent data in a way that makes it easy to map to in-memory data structures, it should come as no surprise that composite on-disk structures are usually defined in ways similar to the way programming languages define in-memory structures. Usually a composite on-disk structure will consist of a number of named parts, each of which is itself either a primitive type such as a number or a string, another composite structure, or possibly a collection of such values. For instance, an ID3 tag defined in the 2.2 version of the specification consists of a header made up of a three-character ISO-8859-1 string, which is always quotID3quot two one-byte unsigned integers that specify the major version and revision of the specification eight bits worth of boolean flags and four bytes that encode the size of the tag in an encoding particular to the ID3 specification. Following the header is a list of frames . each of which has its own internal structure. After the frames are as many null bytes as are necessary to pad the tag out to the size specified in the header. If you look at the world through the lens of object orientation, composite structures look a lot like classes. For instance, you could write a class to represent an ID3 tag. An instance of this class would make a perfect repository to hold the data needed to represent an ID3 tag. You could then write functions to read and write instances of this class. For example, assuming the existence of certain other functions for reading the appropriate primitive data types, a read-id3-tag function might look like this: The write-id3-tag function would be structured similarly--youd use the appropriate write - functions to write out the values stored in the slots of the id3-tag object. Its not hard to see how you could write the appropriate classes to represent all the composite data structures in a specification along with read-foo and write-foo functions for each class and for necessary primitive types. But its also easy to tell that all the reading and writing functions are going to be pretty similar, differing only in the specifics of what types they read and the names of the slots they store them in. Its particularly irksome when you consider that in the ID3 specification it takes about four lines of text to specify the structure of an ID3 tag, while youve already written eighteen lines of code and havent even written write-id3-tag yet. What youd really like is a way to describe the structure of something like an ID3 tag in a form thats as compressed as the specifications pseudocode yet that can also be expanded into code that defines the id3-tag class and the functions that translate between bytes on disk and instances of the class. Sounds like a job for a macro. Designing the Macros Since you already have a rough idea what code your macros will need to generate, the next step, according to the process for writing a macro I outlined in Chapter 8, is to switch perspectives and think about what a call to the macro should look like. Since the goal is to be able to write something as compressed as the pseudocode in the ID3 specification, you can start there. The header of an ID3 tag is specified like this: In the notation of the specification, this means the quotfile identifierquot slot of an ID3 tag is the string quotID3quot in ISO-8859-1 encoding. The version consists of two bytes, the first of which--for this version of the specification--has the value 2 and the second of which--again for this version of the specification--is 0. The flags slot is eight bits, of which all but the first two are 0, and the size consists of four bytes, each of which has a 0 in the most significant bit. Some information isnt captured by this pseudocode. For instance, exactly how the four bytes that encode the size are to be interpreted is described in a few lines of prose. Likewise, the spec describes in prose how the frame and subsequent padding is stored after this header. But most of what you need to know to be able to write code to read and write an ID3 tag is specified by this pseudocode. Thus, you ought to be able to write an s-expression version of this pseudocode and have it expanded into the class and function definitions youd otherwise have to write by hand--something, perhaps, like this: The basic idea is that this form defines a class id3-tag similar to the way you could with DEFCLASS . but instead of specifying things such as :initarg and :accessors. each slot specification consists of the name of the slot-- file-identifier. major-version. and so on--and information about how that slot is represented on disk. Since this is just a bit of fantasizing, you dont have to worry about exactly how the macro define-binary-class will know what to do with expressions such as (iso-8859-1-string :length 3). u1. id3-tag-size. and (id3-frames :tag-size size) as long as each expression contains the information necessary to know how to read and write a particular data encoding, you should be okay. Making the Dream a Reality Okay, enough fantasizing about good-looking code now you need to get to work writing define-binary-class --writing the code that will turn that concise expression of what an ID3 tag looks like into code that can represent one in memory, read one off disk, and write it back out. To start with, you should define a package for this library. Heres the package file that comes with the version you can download from the books Web site: The COM. GIGAMONKEYS. MACRO-UTILITIES package contains the with-gensyms and once-only macros from Chapter 8. Since you already have a handwritten version of the code you want to generate, it shouldnt be too hard to write such a macro. Just take it in small pieces, starting with a version of define-binary-class that generates just the DEFCLASS form. If you look back at the define-binary-class form, youll see that it takes two arguments, the name id3-tag and a list of slot specifiers, each of which is itself a two-item list. From those pieces you need to build the appropriate DEFCLASS form. Clearly, the biggest difference between the define-binary-class form and a proper DEFCLASS form is in the slot specifiers. A single slot specifier from define-binary-class looks something like this: But thats not a legal slot specifier for a DEFCLASS . Instead, you need something like this: Easy enough. First define a simple function to translate a symbol to the corresponding keyword symbol. Now define a function that takes a define-binary-class slot specifier and returns a DEFCLASS slot specifier. You can test this function at the REPL after switching to your new package with a call to IN-PACKAGE . Se ve bien. Now the first version of define-binary-class is trivial. This is simple template-style macro-- define-binary-class generates a DEFCLASS form by interpolating the name of the class and a list of slot specifiers constructed by applying slot-gtdefclass-slot to each element of the list of slots specifiers from the define-binary-class form. To see exactly what code this macro generates, you can evaluate this expression at the REPL. The result, slightly reformatted here for better readability, should look familiar since its exactly the class definition you wrote by hand earlier: Reading Binary Objects Next you need to make define-binary-class also generate a function that can read an instance of the new class. Looking back at the read-id3-tag function you wrote before, this seems a bit trickier, as the read-id3-tag wasnt quite so regular--to read each slots value, you had to call a different function. Not to mention, the name of the function, read-id3-tag. while derived from the name of the class youre defining, isnt one of the arguments to define-binary-class and thus isnt available to be interpolated into a template the way the class name was. You could deal with both of those problems by devising and following a naming convention so the macro can figure out the name of the function to call based on the name of the type in the slot specifier. However, this would require define-binary-class to generate the name read-id3-tag. which is possible but a bad idea. Macros that create global definitions should generally use only names passed to them by their callers macros that generate names under the covers can cause hard-to-predict--and hard-to-debug--name conflicts when the generated names happen to be the same as names used elsewhere. 8 You can avoid both these inconveniences by noticing that all the functions that read a particular type of value have the same fundamental purpose, to read a value of a specific type from a stream. Speaking colloquially, you might say theyre all instances of a single generic operation. And the colloquial use of the word generic should lead you directly to the solution to your problem: instead of defining a bunch of independent functions, all with different names, you can define a single generic function, read-value. with methods specialized to read different types of values. That is, instead of defining functions read-iso-8859-1-string and read-u1. you can define read-value as a generic function taking two required arguments, a type and a stream, and possibly some keyword arguments. By specifying ampkey without any actual keyword parameters, you allow different methods to define their own ampkey parameters without requiring them to do so. This does mean every method specialized on read-value will have to include either ampkey or an amprest parameter in its parameter list to be compatible with the generic function. Then youll define methods that use EQL specializers to specialize the type argument on the name of the type you want to read. Then you can make define-binary-class generate a read-value method specialized on the type name id3-tag. and that method can be implemented in terms of calls to read-value with the appropriate slot types as the first argument. The code you want to generate is going to look like this: So, just as you needed a function to translate a define-binary-class slot specifier to a DEFCLASS slot specifier in order to generate the DEFCLASS form, now you need a function that takes a define-binary-class slot specifier and generates the appropriate SETF form, that is, something that takes this: and returns this: However, theres a difference between this code and the DEFCLASS slot specifier: it includes a reference to a variable in --the method parameter from the read-value method--that wasnt derived from the slot specifier. It doesnt have to be called in. but whatever name you use has to be the same as the one used in the methods parameter list and in the other calls to read-value. For now you can dodge the issue of where that name comes from by defining slot-gtread-value to take a second argument of the name of the stream variable. The function normalize-slot-spec normalizes the second element of the slot specifier, converting a symbol like u1 to the list (u1) so the DESTRUCTURING-BIND can parse it. It looks like this: You can test slot-gtread-value with each type of slot specifier. With these functions youre ready to add read-value to define-binary-class. If you take the handwritten read-value method and strip out anything thats tied to a particular class, youre left with this skeleton: All you need to do is add this skeleton to the define-binary-class template, replacing ellipses with code that fills in the skeleton with the appropriate names and code. Youll also want to replace the variables type. corriente. and object with gensymed names to avoid potential conflicts with slot names, 9 which you can do with the with-gensyms macro from Chapter 8. Also, because a macro must expand into a single form, you need to wrap some form around the DEFCLASS and DEFMETHOD . PROGN is the customary form to use for macros that expand into multiple definitions because of the special treatment it gets from the file compiler when appearing at the top level of a file, as I discussed in Chapter 20. So, you can change define-binary-class as follows: Writing Binary Objects Generating code to write out an instance of a binary class will proceed similarly. First you can define a write-value generic function. Then you define a helper function that translates a define-binary-class slot specifier into code that writes out the slot using write-value. As with the slot-gtread-value function, this helper function needs to take the name of the stream variable as an argument. Now you can add a write-value template to the define-binary-class macro. Adding Inheritance and Tagged Structures While this version of define-binary-class will handle stand-alone structures, binary file formats often define on-disk structures that would be natural to model with subclasses and superclasses. So you might want to extend define-binary-class to support inheritance. A related technique used in many binary formats is to have several on-disk structures whose exact type can be determined only by reading some data that indicates how to parse the following bytes. For instance, the frames that make up the bulk of an ID3 tag all share a common header structure consisting of a string identifier and a length. To read a frame, you need to read the identifier and use its value to determine what kind of frame youre looking at and thus how to parse the body of the frame. The current define-binary-class macro has no way to handle this kind of reading--you could use define-binary-class to define a class to represent each kind of frame, but youd have no way to know what type of frame to read without reading at least the identifier. And if other code reads the identifier in order to determine what type to pass to read-value. then that will break read-value since its expecting to read all the data that makes up the instance of the class it instantiates. You can solve this problem by adding inheritance to define-binary-class and then writing another macro, define-tagged-binary-class. for defining quotabstractquot classes that arent instantiated directly but that can be specialized on by read-value methods that know how to read enough data to determine what kind of class to create. The first step to adding inheritance to define-binary-class is to add a parameter to the macro to accept a list of superclasses. Then, in the DEFCLASS template, interpolate that value instead of the empty list. However, theres a bit more to it than that. You also need to change the read-value and write-value methods so the methods generated when defining a superclass can be used by the methods generated as part of a subclass to read and write inherited slots. The current way read-value works is particularly problematic since it instantiates the object before filling it in--obviously, you cant have the method responsible for reading the superclasss fields instantiate one object while the subclasss method instantiates and fills in a different object. You can fix that problem by splitting read-value into two parts--one responsible for instantiating the correct kind of object and another responsible for filling slots in an existing object. On the writing side its a bit simpler, but you can use the same technique. So youll define two new generic functions, read-object and write-object. that will both take an existing object and a stream. Methods on these generic functions will be responsible for reading or writing the slots specific to the class of the object on which theyre specialized. Defining these generic functions to use the PROGN method combination with the option :most-specific-last allows you to define methods that specialize object on each binary class and have them deal only with the slots actually defined in that class the PROGN method combination will combine all the applicable methods so the method specialized on the least specific class in the hierarchy runs first, reading or writing the slots defined in that class, then the method specialized on next least specific subclass, and so on. And since all the heavy lifting for a specific class is now going to be done by read-object and write-object. you dont even need to define specialized read-value and write-value methods you can define default methods that assume the type argument is the name of a binary class. Note how you can use MAKE-INSTANCE as a generic object factory--while you normally call MAKE-INSTANCE with a quoted symbol as the first argument because you normally know exactly what class you want to instantiate, you can use any expression that evaluates to a class name such as, in this case, the type parameter in the read-value method. The actual changes to define-binary-class to define methods on read-object and write-object rather than read-value and write-value are fairly minor. Keeping Track of Inherited Slots This definition will work for many purposes. However, it doesnt handle one fairly common situation, namely, when you have a subclass that needs to refer to inherited slots in its own slot specifications. For instance, with the current definition of define-binary-class. you can define a single class like this: The reference to size in the specification of data works the way youd expect because the expressions that read and write the data slot are wrapped in a WITH-SLOTS that lists all the objects slots. However, if you try to split that class into two classes like this: youll get a compile-time warning when you compile the generic-frame definition and a runtime error when you try to use it because there will be no lexically apparent variable size in the read-object and write-object methods specialized on generic-frame . What you need to do is keep track of the slots defined by each binary class and then include inherited slots in the WITH-SLOTS forms in the read-object and write-object methods. The easiest way to keep track of information like this is to hang it off the symbol that names the class. As I discussed in Chapter 21, every symbol object has an associated property list, which can be accessed via the functions SYMBOL-PLIST and GET . You can associate arbitrary key/value pairs with a symbol by adding them to its property list with SETF of GET . For instance, if the binary class foo defines three slots-- x. Y. and z --you can keep track of that fact by adding a slots key to the symbol foo s property list with the value (x y z) with this expression: You want this bookkeeping to happen as part of evaluating the define-binary-class of foo. However, its not clear where to put the expression. If you evaluate it when you compute the macros expansion, itll get evaluated when you compile the define-binary-class form but not if you later load a file that contains the resulting compiled code. On the other hand, if you include the expression in the expansion, then it wont be evaluated during compilation, which means if you compile a file with several define-binary-class forms, none of the information about what classes define what slots will be available until the whole file is loaded, which is too late. This is what the special operator EVAL-WHEN I discussed in Chapter 20 is for. By wrapping a form in an EVAL-WHEN . you can control whether its evaluated at compile time, when the compiled code is loaded, or both. For cases like this where you want to squirrel away some information during the compilation of a macro form that you also want to be available after the compiled form is loaded, you should wrap it in an EVAL-WHEN like this: and include the EVAL-WHEN in the expansion generated by the macro. Thus, you can save both the slots and the direct superclasses of a binary class by adding this form to the expansion generated by define-binary-class : Now you can define three helper functions for accessing this information. The first simply returns the slots directly defined by a binary class. Its a good idea to return a copy of the list since you dont want other code to modify the list of slots after the binary class has been defined. The next function returns the slots inherited from other binary classes. Finally, you can define a function that returns a list containing the names of all directly defined and inherited slots. When youre computing the expansion of a define-generic-binary-class form, you want to generate a WITH-SLOTS form that contains the names of all the slots defined in the new class and all its superclasses. However, you cant use all-slots while youre generating the expansion since the information wont be available until after the expansion is compiled. Instead, you should use the following function, which takes the list of slot specifiers and superclasses passed to define-generic-binary-class and uses them to compute the list of all the new classs slots: With these functions defined, you can change define-binary-class to store the information about the class currently being defined and to use the already stored information about the superclasses slots to generate the WITH-SLOTS forms you want like this: Tagged Structures With the ability to define binary classes that extend other binary classes, youre ready to define a new macro for defining classes to represent quottaggedquot structures. The strategy for reading tagged structures will be to define a specialized read-value method that knows how to read the values that make up the start of the structure and then use those values to determine what subclass to instantiate. Itll then make an instance of that class with MAKE-INSTANCE . passing the already read values as initargs, and pass the object to read-object. allowing the actual class of the object to determine how the rest of the structure is read. The new macro, define-tagged-binary-class. will look like define-binary-class with the addition of a :dispatch option used to specify a form that should evaluate to the name of a binary class. The :dispatch form will be evaluated in a context where the names of the slots defined by the tagged class are bound to variables that hold the values read from the file. The class whose name it returns must accept initargs corresponding to the slot names defined by the tagged class. This is easily ensured if the :dispatch form always evaluates to the name of a class that subclasses the tagged class. For instance, supposing you have a function, find-frame-class. that will map a string identifier to a binary class representing a particular kind of ID3 frame, you might define a tagged binary class, id3-frame. like this: The expansion of a define-tagged-binary-class will contain a DEFCLASS and a write-object method just like the expansion of define-binary-class. but instead of a read-object method itll contain a read-value method that looks like this: Since the expansions of define-tagged-binary-class and define-binary-class are going to be identical except for the read method, you can factor out the common bits into a helper macro, define-generic-binary-class. that accepts the read method as a parameter and interpolates it. Now you can define both define-binary-class and define-tagged-binary-class to expand into a call to define-generic-binary-class. Heres a new version of define-binary-class that generates the same code as the earlier version when its fully expanded: And heres define-tagged-binary-class along with two new helper functions it uses: Primitive Binary Types While define-binary-class and define-tagged-binary-class make it easy to define composite structures, you still have to write read-value and write-value methods for primitive data types by hand. You could decide to live with that, specifying that users of the library need to write appropriate methods on read-value and write-value to support the primitive types used by their binary classes. However, rather than having to document how to write a suitable read-value / write-value pair, you can provide a macro to do it automatically. This also has the advantage of making the abstraction created by define-binary-class less leaky. Currently, define-binary-class depends on having methods on read-value and write-value defined in a particular way, but thats really just an implementation detail. By defining a macro that generates the read-value and write-value methods for primitive types, you hide those details behind an abstraction you control. If you decide later to change the implementation of define-binary-class. you can change your primitive-type-defining macro to meet the new requirements without requiring any changes to code that uses the binary data library. So you should define one last macro, define-binary-type. that will generate read-value and write-value methods for reading values represented by instances of existing classes, rather than by classes defined with define-binary-class . For a concrete example, consider a type used in the id3-tag class, a fixed-length string encoded in ISO-8859-1 characters. Ill assume, as I did earlier, that the native character encoding of your Lisp is ISO-8859-1 or a superset, so you can use CODE-CHAR and CHAR-CODE to translate bytes to characters and back. As always, your goal is to write a macro that allows you to express only the essential information needed to generate the required code. In this case, there are four pieces of essential information: the name of the type, iso-8859-1-string the ampkey parameters that should be accepted by the read-value and write-value methods, length in this case the code for reading from a stream and the code for writing to a stream. Heres an expression that contains those four pieces of information: Now you just need a macro that can take apart this form and put it back together in the form of two DEFMETHOD s wrapped in a PROGN . If you define the parameter list to define-binary-type like this: then within the macro the parameter spec will be a list containing the reader and writer definitions. You can then use ASSOC to extract the elements of spec using the tags :reader and :writer and then use DESTRUCTURING-BIND to take apart the REST of each element. 10 From there its just a matter of interpolating the extracted values into the backquoted templates of the read-value and write-value methods. Note how the backquoted templates are nested: the outermost template starts with the backquoted PROGN form. That template consists of the symbol PROGN and two comma-unquoted DESTRUCTURING-BIND expressions. Thus, the outer template is filled in by evaluating the DESTRUCTURING-BIND expressions and interpolating their values. Each DESTRUCTURING-BIND expression in turn contains another backquoted template, which is used to generate one of the method definitions to be interpolated in the outer template. With this macro defined, the define-binary-type form given previously expands to this code: Of course, now that youve got this nice macro for defining binary types, its tempting to make it do a bit more work. For now you should just make one small enhancement that will turn out to be pretty handy when you start using this library to deal with actual formats such as ID3 tags. ID3 tags, like many other binary formats, use lots of primitive types that are minor variations on a theme, such as unsigned integers in one-, two-, three-, and four-byte varieties. You could certainly define each of those types with define-binary-type as it stands. Or you could factor out the common algorithm for reading and writing n - byte unsigned integers into helper functions. But suppose you had already defined a binary type, unsigned-integer. that accepts a :bytes parameter to specify how many bytes to read and write. Using that type, you could specify a slot representing a one-byte unsigned integer with a type specifier of (unsigned-integer :bytes 1). But if a particular binary format specifies lots of slots of that type, itd be nice to be able to easily define a new type--say, u1 --that means the same thing. As it turns out, its easy to change define-binary-type to support two forms, a long form consisting of a :reader and :writer pair and a short form that defines a new binary type in terms of an existing type. Using a short form define-binary-type. you can define u1 like this: which will expand to this: To support both long - and short-form define-binary-type calls, you need to differentiate based on the value of the spec argument. If spec is two items long, it represents a long-form call, and the two items should be the :reader and :writer specifications, which you extract as before. On the other hand, if its only one item long, the one item should be a type specifier, which needs to be parsed differently. You can use ECASE to switch on the LENGTH of spec and then parse spec and generate an appropriate expansion for either the long form or the short form. The Current Object Stack One last bit of functionality youll need in the next chapter is a way to get at the binary object being read or written while reading and writing. More generally, when reading or writing nested composite objects, its useful to be able to get at any of the objects currently being read or written. Thanks to dynamic variables and :around methods, you can add this enhancement with about a dozen lines of code. To start, you should define a dynamic variable that will hold a stack of objects currently being read or written. Then you can define :around methods on read-object and write-object that push the object being read or written onto this variable before invoking CALL-NEXT-METHOD . Note how you rebind in-progress-objects to a list with a new item on the front rather than assigning it a new value. This way, at the end of the LET . after CALL-NEXT-METHOD returns, the old value of in-progress-objects will be restored, effectively popping the object of the stack. With those two methods defined, you can provide two convenience functions for getting at specific objects in the in-progress stack. The function current-binary-object will return the head of the stack, the object whose read-object or write-object method was invoked most recently. The other, parent-of-type. takes an argument that should be the name of a binary object class and returns the most recently pushed object of that type, using the TYPEP function that tests whether a given object is an instance of a particular type. These two functions can be used in any code that will be called within the dynamic extent of a read-object or write-object call. Youll see one example of how current-binary-object can be used in the next chapter. 11 Now you have all the tools you need to tackle an ID3 parsing library, so youre ready to move onto the next chapter where youll do just that. 1 In ASCII, the first 32 characters are nonprinting control characters originally used to control the behavior of a Teletype machine, causing it to do such things as sound the bell, back up one character, move to a new line, and move the carriage to the beginning of the line. Of these 32 control characters, only three, the newline, carriage return, and horizontal tab, are typically found in text files. 2 Some binary file formats are in-memory data structures--on many operating systems its possible to map a file into memory, and low-level languages such as C can then treat the region of memory containing the contents of the file just like any other memory data written to that area of memory is saved to the underlying file when its unmapped. However, these formats are platform-dependent since the in-memory representation of even such simple data types as integers depends on the hardware on which the program is running. Thus, any file format thats intended to be portable must define a canonical representation for all the data types it uses that can be mapped to the actual in-memory data representation on a particular kind of machine or in a particular language. 3 The term big-endian and its opposite, little-endian . borrowed from Jonathan Swifts Gullivers Travels . refer to the way a multibyte number is represented in an ordered sequence of bytes such as in memory or in a file. For instance, the number 43981, or abcd in hex, represented as a 16-bit quantity, consists of two bytes, ab and cd. It doesnt matter to a computer in what order these two bytes are stored as long as everybody agrees. Of course, whenever theres an arbitrary choice to be made between two equally good options, the one thing you can be sure of is that everybody is not going to agree. For more than you ever wanted to know about it, and to see where the terms big-endian and little-endian were first applied in this fashion, read quotOn Holy Wars and a Plea for Peacequot by Danny Cohen, available at khavrinen. lcs. mit. edu/wollman/ien-137.txt . 4 LDB and DPB . a related function, were named after the DEC PDP-10 assembly functions that did essentially the same thing. Both functions operate on integers as if they were represented using twos-complement format, regardless of the internal representation used by a particular Common Lisp implementation. 5 Common Lisp also provides functions for shifting and masking the bits of integers in a way that may be more familiar to C and Java programmers. For instance, you could write read-u2 yet a third way, using those functions, like this: which would be roughly equivalent to this Java method: The names LOGIOR and ASH are short for LOGical Inclusive OR and Arithmetic SHift . ASH shifts an integer a given number of bits to the left when its second argument is positive or to the right if the second argument is negative. LOGIOR combines integers by logically or ing each bit. Another function, LOGAND . performs a bitwise and . which can be used to mask off certain bits. However, for the kinds of bit twiddling youll need to do in this chapter and the next, LDB and BYTE will be both more convenient and more idiomatic Common Lisp style. 6 Originally, UTF-8 was designed to represent a 31-bit character code and used up to six bytes per code point. However, the maximum Unicode code point is x10ffff. so a UTF-8 encoding of Unicode requires at most four bytes per code point. 7 If you need to parse a file format that uses other character codes, or if you need to parse files containing arbitrary Unicode strings using a non-Unicode-Common-Lisp implementation, you can always represent such strings in memory as vectors of integer code points. They wont be Lisp strings, so you wont be able to manipulate or compare them with the string functions, but youll still be able to do anything with them that you can with arbitrary vectors. 8 Unfortunately, the language itself doesnt always provide a good model in this respect: the macro DEFSTRUCT . which I dont discuss since it has largely been superseded by DEFCLASS . generates functions with names that it generates based on the name of the structure its given. DEFSTRUCT s bad example leads many new macro writers astray. 9 Technically theres no possibility of type or object conflicting with slot names--at worst theyd be shadowed within the WITH-SLOTS form. But it doesnt hurt anything to simply GENSYM all local variable names used within a macro template. 10 Using ASSOC to extract the :reader and :writer elements of spec allows users of define-binary-type to include the elements in either order if you required the :reader element to be always be first, you could then have used (rest (first spec)) to extract the reader and (rest (second spec)) to extract the writer. However, as long as you require the :reader and :writer keywords to improve the readability of define-binary-type forms, you might as well use them to extract the correct data. 11 The ID3 format doesnt require the parent-of-type function since its a relatively flat structure. This function comes into its own when you need to parse a format made up of many deeply nested structures whose parsing depends on information stored in higher-level structures. For example, in the Java class file format, the top-level class file structure contains a constant pool that maps numeric values used in other substructures within the class file to constant values that are needed while parsing those substructures. If you were writing a class file parser, you could use parent-of-type in the code that reads and writes those substructures to get at the top-level class file object and from there to the constant pool.


No comments:

Post a Comment