Piensa Perl 6
Piensa Perl 6
Laurent Rosenfeld,
con Allen B. Downey
Piensa en Perl 6
Cómo Pensar Como un Científico de la Computación
Se concede permiso para copiar, distribuir y/o modificar este documento bajo los términos de
Creative Commons Attribution-NonCommercial 3.0 Unported License, la cual está disponible en
http://creativecommons.org/licenses/by-nc/3.0/.
La forma original de este libro es código fuente LATEX . La compilación de este código LATEX tiene el
efecto de generar una representación independiente a cualquier dispositivo de este libro de texto, la
cual puede ser convertida a otros formatos e imprimida.
Introducción xxi
3 Funciones 31
3.1 Llamadas de Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.2 Funciones y Métodos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.3 Funciones Matemáticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.4 Composición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.5 Agregar Nuevas Funciones (o Subrutinas) . . . . . . . . . . . . . . . . . . . 37
3.6 Definiciones y Usos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.7 Flujo de Ejecución . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.8 Parámetros y Argumentos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.9 Las Variables y los Parámetros son Locales . . . . . . . . . . . . . . . . . . 41
3.10 Diagramas de Pila . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.11 Funciones Fructuosas y Funciones Voids . . . . . . . . . . . . . . . . . . . . 42
3.12 Signaturas de Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.13 Parámetros Inmutables y Mutables . . . . . . . . . . . . . . . . . . . . . . . 45
3.14 Funciones y Subrutinas: Ciudadanos de Primera Clase . . . . . . . . . . . 46
3.15 ¿Por qué Funciones y Subrutinas? . . . . . . . . . . . . . . . . . . . . . . . . 48
3.16 Depuración de Programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.17 Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.18 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5 Subrutinas Fructuosas 75
5.1 Valores de Retorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.2 Desarrollo Incremental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
5.3 Composición . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
5.4 Funciones Booleanas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
5.5 Un Lenguaje de Programación Completo . . . . . . . . . . . . . . . . . . . 81
5.6 Más Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
5.7 Acto de Fe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.8 Otro Ejemplo Más . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
5.9 Chequeo de Tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
5.10 Sobrecargas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.11 Depuración de programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
5.12 Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
5.13 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
6 Iteración 93
6.1 Asignación Versus Igualdad . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
6.2 Reasignación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
6.3 Actualización de las Variables . . . . . . . . . . . . . . . . . . . . . . . . . . 94
6.4 La Sentencia while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.5 Variables Locales y Ámbito de una Variable . . . . . . . . . . . . . . . . . . 97
6.6 Sentencias de Flujo de Control (last, next, etc.) . . . . . . . . . . . . . . . 100
xii Tabla de Contenido
10 Hashes 177
10.1 Un Hash es un Mapeo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
10.2 Operaciones Comunes con Hashes . . . . . . . . . . . . . . . . . . . . . . . 180
10.3 Un Hash como una Colección de Contadores . . . . . . . . . . . . . . . . . 181
10.4 Bucles y Hashes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
10.5 Búsqueda Inversa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
10.6 Comprobando la Existencia . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
10.7 Las Claves de un Hash son Únicas . . . . . . . . . . . . . . . . . . . . . . . 186
10.8 Los Hashes y los Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
10.9 Memos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
10.10 Los Hashes como Tablas de Despacho . . . . . . . . . . . . . . . . . . . . . 190
10.11 Variables Globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191
10.12 Depuración de Programas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
10.13 Glosario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
10.14 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Este libro está destinado para principiantes y no requiere ningún conocimiento previo de
programación, aunque espera que aquellos con experiencia de programación puedan aún
así beneficiarse.
Esto significa que no abarcaré cada aspecto de Perl 6, pero solo un subconjunto (relativa-
mente grande pero incompleto) del lenguaje. Este libro no pretende ser una referencia del
lenguaje.
Hay más de mil ejemplos de código en este libro; estúdialos, asegúrate de entenderlos,
y ejecútalos. Si es posible, trata de cambiarlos y observa que sucede. Es probable que
aprendas mucho de este proceso.
xxii Capítulo 0. Introducción
Reconocimientos
Realmente no sé cómo podría agradecerle a Larry Wall al nivel de gratitud que él se merece
por haber creado Perl en primer lugar, y más recientemente Perl 6. ¡Qué seas bendecido
por toda la eternidad, Larry!
1 Ver por ejemplo http://perl.developpez.com/cours/#TutorielsPerl6.
2 Ver http://greenteapress.com/wp/think-python-2e/.
3 Lo sé, es acerca de Python, no Perl. Sin embargo, no creo en las “guerras de lenguajes“ y pienso que todos
debemos aprender de otros lenguajes; para mí, el lema de Perl, “hay más de una forma para hacer algo,“ también
significa que hacerlo en Python (o cualquier otro lenguaje) es realmente una posibilidad aceptable.
4 Ver http://allen-downey.developpez.com/livres/python/pensez-python/.
xxiii
Y gracias a todos ustedes que fueron parte de esta aventura (en ningún orden en particular),
Tom, Damian, chromatic, Nathan, brian, Jan, Jarkko, John, Johan, Randall, Mark Jason,
Ovid, Nick, Tim, Andy, Chip, Matt, Michael, Tatsuhiko, Dave, Rafael, Chris, Stevan, Saraty,
Malcolm, Graham, Leon, Ricardo, Gurusamy, Scott y muchísimos otros.
Todas mis gracias también a aquellos quienes creyeron en el proyecto de Perl 6 y que lo
hicieron posible, incluyendo a aquellos que abandonaron en algún momento pero que con-
tribuyeron por algún tiempo; sé que no fue siempre fácil.
Muchas gracias a Allen Downey, quien amablemente apoyó la idea de adaptar su libro
para Perl 6 y me ayudó en muchos aspectos, pero también se abstuvo de interferir en lo
que yo ponía en este libro.
Le agradezco sinceramente a la gente de O’Reilly quienes aceptaron la idea de este li-
bro y sugirieron muchas correcciones o mejoras. Quiero agradecer especialmente a Dawn
Schanafelt, mi editor de O’Reilly, cuyos consejos han contribuido a hacer este un mejor li-
bro. Muchas gracias también a Charles Roumeliotis, el editor de copia, y Kristen Brown, la
editora de producción, quien arregló muchos problemas tipográficos y faltas ortográficas.
De antemano le doy gracias a todos los lectores quienes ofrecerán comentarios o someterán
sugerencias o correcciones, al igual que palabras de aliento.
Si ves algo que necesita ser corregido o que podría ser mejorado, por favor amablemente
envía tus comentarios a think.perl6(at)gmail.com.
Nota del traductor: Alternativamente, puedes enviar tus correos electrónicos amablemente
a uzluisf(arroba)protonmail.com si se trata de algo relacionado directamente con esta
traducción.
Lista de Contribuciones
Me gustaría agradecer especialmente a Moritz Lenz y Elizabeth Mattijsen, quienes revis-
aron los detalles en los borradores de este libro y sugirieron un sin número de mejoras y
correcciones. Liz invirtió mucho tiempo en una revisión detallada del contenido completo
de este libro y le agradezco eternamente por sus numerosos y muy útiles comentarios.
Gracias también a Timo Paulssen y ryanschoppe quienes también revisaron los primeros
borradores y proveyeron algunos comentarios útiles. Muchísimas gracias también a Uri
Guttman, quien revisó este libro y sugirió un gran número de pequeñas correcciones y
mejoras un poco antes de la publicación.
Kamimura, James Lenz, y Jeff McClelland cada uno sometieron algunas correcciones en la
lista de errata en el sitio web de O’Reilly. zengargoyle señaló un carácter falso en un regex
y lo arregló en el repositorio Github del libro. zengargoyle también sugirió una aclaración
en el capítulo acerca de la programación funcional. Otro James (segundo nombre que no
conozco) sometió una errata al sitio web de O’Reilly. Mikhail Korenev sugirió correcciones
precisas para tres fragmento de código. Sébastien Dorey, Jean Forget, y Roland Schmitz
enviaron algunos correos electrónicos sugiriendo algunas correcciones o mejoras útiles.
Luis F. Uceta corrigió varios errores tipográficos en el repositorio de Github. Gil Magno,
zoffixznet y Joaquin Ferrero también sugirieron varias correcciones en Github.
xxiv Capítulo 0. Introducción
Parte I
Este libro está dividido en dos partes. La razón principal por esta decisión es que yo quería
hacer una distinción entre las nociones relativamente básicas que cualquier programador
necesita al usar Perl 6 y los conceptos avanzados que un buen programador necesita saber
y que pueden ser necesarios en el día a día de un programador.
Los primeros once capítulos (aproximadamente 200 páginas) que constituyen la primera
parte están diseñados para enseñar los conceptos básicos que cada programador debe
conocer: variables, expresiones, condicionales, recursión, precedencia de operadores, bu-
cles, etc., y también las estructuras de datos básicas usadas comúnmente, y los algoritmos
más útiles. Creo que estos capítulos pueden ser la base de un curso introductorio a la
programación para un semestre.
Por supuesto, el profesor o instructor que desee utilizar este material es libre de saltar
algunos detalles en la Parte 1 (y también incluir de la Parte 2). Por lo menos, he incluido al-
gunas recomendaciones sobre como este libro podría ser usado para enseñar programación
usando el lenguaje Perl 6.
La segunda parte se enfoca en paradigmas diferentes y algunas técnicas avanzadas de pro-
gramación que son en mi opinión de gran importancia, pero que deberían ser estudiadas
en el contexto de un segundo semestre más avanzado.
Por ahora, comencemos con lo básico. Espero que disfrutes la travesía.
4
1
La Forma del Programa
En un nivel, aprenderás a programar, la cual es una habilidad útil por sí misma. En otro
nivel, aprenderás a usar la programación como un medio para un fin. A medida que
avancemos, este fin se esclarecerá.
Los detalles lucen diferentes en lenguajes distintos, pero hay algunas instrucciones básicas
que aparecen en casi todos los lenguajes:
6 Capítulo 1. La Forma del Programa
Entrada Obtener datos desde el teclado, un archivo, la red, un sensor, un chip de GPS o
algún otro dispositivo.
Lo creas o no, eso es todo. Cada programa que has usado, sin importar que tan compli-
cado sea, está compuesto de instrucciones que lucen exactamente como estas. Por lo tanto,
puedes imaginarte la programación como el proceso de fragmentar una tarea grande y
compleja en piezas más pequeñas las cuales se pueden ejecutar con una de estas opera-
ciones básicas.
Al usar o llamar estas piezas, es posible crear varios niveles de abstracción. Probablemente
te han dicho que las computadoras solamente usan 0s y 1s al nivel más fundamental; pero
usualmente no nos preocupamos acerca de esto. Cuando usamos un procesador de texto
para escribir una carta o un reporte, estamos interesados en archivos con textos y algunas
instrucciones de formato, y con comandos para cambiar el archivo o imprimirlo; afortu-
nadamente, no tenemos que preocuparnos sobre los 0s y 1s; el procesador de texto nos
ofrece una vista más general (archivos, comandos, etc.) que oculta todos los detalles in-
significantes del usuario.
Similarmente, cuando escribimos un programa, usualmente usamos y/o creamos varias
capas de abstracción, para que, por ejemplo, una vez que hayamos creado una tarea pe-
queña que consulta una base de datos y guarda la información relevante en la memoria, no
tengamos que preocuparnos sobre los detalles técnicos de la tarea. Podemos usarla como
una caja negra la cual realizará la operación deseada para nosotros. La esencia de la pro-
gramación es en gran parte este arte de crear estas capas sucesivas de abstracción de tal
manera que realizar tareas de niveles más altos se vuelva relativamente más fácil.
La manera más fácil de instalar Perl 6 en tu sistema es descargar Rakudo Star (una dis-
tribución de Perl 6 que contiene el compilador Rakudo de Perl 6, documentación y mó-
dulos útiles): sigue las instrucciones para tu sistema operativo en http://rakudo.org/
how-to-get-rakudo/ y en https://perl6.org/downloads/.
Al momento de escribir este libro, la especificación más reciente del lenguaje es Perl 6
version 6c (v6.c), y el lanzamiento más reciente disponible para la descarga es Rakudo Star
2016.07; los ejemplos en este libro deberían funcionar con esta versión. Puedes encontrar
la versión instalada al ejecutar el siguiente comando en el intérprete de comandos de tu
sistema operativo:
$ perl6 -v
This is Rakudo version 2016.07.1 built on MoarVM version 2016.07
implementing Perl 6.c.
No obstante, deberías descargar e instalar las versión más reciente que puedas encontrar.
La salida (advertencias, mensajes de errores, etc.) que obtendrás de tu versión de Perl 6
podría en algunos casos diferir de la que se encuentra en este libro, pero estas diferencias
deberían ser esencialmente cosméticas.
Comparado con Perl 5, Perl 6 no es sólo una nueva versión de Perl. Es como la nueva
hermana menor de Perl 5. Su objetivo no es reemplazar a Perl 5. Perl 6 es realmente
un nuevo lenguaje de programación, con una sintaxis que es similar a las otras versiones
de Perl (tal como Perl 5), pero considerablemente diferente. Al menos que se indique lo
contrario, este libro es sobre Perl 6 solamente, no acerca de Perl 5 y versiones anteriores del
lenguaje de programación Perl. Desde aquí adelante, cuando hablemos de Perl sin ninguna
cualificación, nos referimos a Perl 6.
El interpretador de Perl 6 es un programa que lee y ejecuta código de Perl 6. Algunas veces
se le llama REPL (por “read, evaluate, print, loop” en inglés). Dependiendo de tu entorno,
podría iniciar el interpretador al cliquear un ícono, o al escribir perl6 en el intérprete de
comandos.
La última línea con > es un prompt que indica que el REPL está listo para que entres código.
Si tecleas una línea de código y presiona la tecla Enter, el interpretador muestra el resul-
tado:
> 1 + 1
2
>
Ahora estás listo para comenzar. De aquí en adelante, asumimos que sabes como iniciar el
REPL de Perl 6 y ejecutar código.
8 Capítulo 1. La Forma del Programa
> 40 + 2
42
> 43 - 1
42
> 6 * 7
42
> 84 / 2
42
Dado que usamos el REPL, no necesitamos una sentencia de impresión explícita en estos
ejemplos, debido a que el REPL automáticamente imprime el resultado de las sentencias.
En un programa real, necesitarías una sentencia de impresión para mostrar el resultado,
1 Perl también tiene una función print, pero la función integrada say es usada aquí porque agrega un carácter
say 40 + 2; # -> 42
> 6**2 + 6
42
Esto puede parecer anecdótico, pero por razones más allá del ámbito de este libro, esto per-
mite que Perl 6 pueda llevar a cabo operaciones aritméticas sobre números racionales con
un nivel de precisión mucho mayor que en los lenguajes de programación más comunes.
Por ejemplo, si intentas llevar a cabo la siguiente operación aritmética 0.3 - 0.2 - 0.1,
con los lenguajes de programación más generales (y dependiendo en la arquitectura de
tu máquina), podrías obtener un resultado tal como -2.77555756156289e-17 (en Perl 5), -
2.775558e-17 (en C con gcc), o -2.7755575615628914e-17 (Java, Python 3, Ruby, TCL). No te
preocupes acerca de estos valores si no los entiendes, digamos que son extremadamente
pequeños, pero no son 0, mientras que, obviamente, el resultado debería ser realmente
cero. En Perl 6, el resultado es 0 (hasta el quinceavo dígito decimal):
No hagas tal comparación en los lenguajes de programación más comunes; es posible que
obtengas un resultado erróneo.
¿Qué acerca de valores como "2" y "42.0"? Ellos parecen números, pero se encuentran
dentro de comillas dobles como las cadenas de texto.
> say '2'.perl; # perl devuelve una representación de Perl del invocante
"2"
> say "2".WHAT;
(Str)
> say '42'.WHAT;
(Str)
1.6. Lenguajes Formales y Naturales 11
Estos valores son cadenas de texto porque están definidos dentro de comillas dobles.
Aunque Perl usualmente realizará las conversiones necesarias para ti, es generalmente
buena práctica no usar comillas dobles si tu valor está supuesto a ser un número.
Cuando escribes un número entero largo, podrías estar tentado a usar comas entre grupos
de dígitos, como en 1,234,567. Esto no es un entero legal en Perl 6, pero sí es una expresión
legal:
> 1,234,567
(1 234 567)
>
¡Eso es actualmente una lista de tres números enteros diferentes, y no lo que esperábamos!
Perl 6 interpreta 1,234,567 como una secuencia de tres números enteros separados por
coma. Como veremos más adelante, la coma es un separador usado para construir listas.
No obstante, puedes separar grupos de dígitos con una barra baja “_” para mejorar la
legibilidad y para obtener un entero propio:
> 1_234_567
1234567
> say 1_234_567.WHAT
(Int)
>
Los lenguajes de programación son lenguajes formales que han sido diseña-
dos para expresar computaciones.
Los lenguajes formales tienen usualmente reglas sintácticas estrictas que gobiernan la es-
tructura de las sentencias. Por ejemplo, en las matemáticas la sentencia 3 + 3 = 6 es sintác-
ticamente correcta pero 3+ = 3$6 no lo es. En química H2 O es una fórmula sintácticamente
correcta, pero 2 Zz no lo es.
Las reglas sintácticas vienen en dos formas, pertenecientes a los tókenes y la estructura.
Los tókenes son los elementos básicos del lenguaje, tales como las palabras, números, y
12 Capítulo 1. La Forma del Programa
elementos químicos. Uno de los problemas con 3+ = 3$6 es que $ no es un token legal
en matemáticas ( al menos eso es lo que sé). Similarmente, 2 Zz no es legal porque no hay
elemento químico con la abreviación Zz.
La estructura es el segundo tipo de la regla sintáctica, la cual se concierne con la forma en
que los tókenes son combinados. La ecuación 3+ = 3 es ilegal en matemáticas porque,
aunque + y = son tókenes legales, no puedes tenerlos uno detrás del otro. De manera
similar, en una fórmula química, el subíndice que representa el número de átomos en un
compuesto químico viene después del nombre del elemento, no antes.
Esto es un@ oración en español bien e$tructurada con t*kenes inválidos. En esta oración
todos tókenes los válidos son, pero la inválida estructura es.
Cuando lees una oración en español o una sentencia en un lenguaje formal, debes descifrar
la estructura (aunque en en un lenguaje natural lo haces inconcientemente). Este proceso
es conocido como análisis sintáctico (parsing en inglés).
Aunque los lenguajes formales y los lenguajes naturales comparten muchas características
en común —tókenes, estructura, y sintaxis—también hay algunas diferencias:
Ambigüedad Los lenguajes naturales están llenos de ambigüedades, y las personas lidian
con ellas a través del uso de contexto y cualquier información adicional. Los lengua-
jes formales son diseñados para no tener ambigüedades, de tal manera que cualquier
sentencia tiene exactamente un solo significado.
Redundancia Para compensar por las ambigüedades y reducir los malentendidos, los
lenguajes naturales hacen uso de muchas redundancias. Como resultado, ellos tien-
den a ser verbosos. En cambio, los lenguajes formales son menos redundantes y más
concisos.
Literalidad Los lenguajes naturales están llenos de modismos y metáforas. Si decimos,
“Hablar por los codos,“ es probable que nadie está hablando por los codos literal-
mente (este modismo significa que una persona habla demasiado). Los lenguajes
formales hacen referencias exactas a lo que quieren decir.
Debido a que todos crecemos hablando lenguajes naturales, es un poco difícil ajustarse a
los lenguajes formales. La diferencia entre un lenguaje formal y un lenguaje natural es
como la diferencia entre la poesía y la prosa, pero más definida:
Poesía La palabras son usadas tanto por sus sonidos como sus significados, y el poema
completo crea un efecto o respuesta emocional. En este caso, la ambigüedad no es
solo común sino usualmente deliberada.
Prosa El significado literal de las palabras es más importante, y la estructura contribuye
más al significado. La prosa es más amena para el análisis que la poesía aunque
tiende a ser ambigüa en muchas ocasiones.
Programa El significado de un programa de computadora es literal y no tiene am-
bigüedades. Dicho programa puede ser comprendido enteramente a través del análi-
sis de los tókenes y la estructura.
Los lenguajes formales son mucho más densos que los lenguajes naturales, y por lo tanto
se tarda más en leerlos. De igual manera, la estructura es importante y algunas veces, no
siempre es eficiente leerlos de arriba hacia abajo o de izquierda a derecha. Por lo contrario,
1.7. Depuración de Programas 13
Existe evidencia que sugiere que las personas responden naturalmente a las computadoras
como si fueran personas. Cuando funcionan adecuadamente, las consideramos nuestras
compañeras, y cuando son obstinadas y groseras, nosotros nos dirigimos hacia ellas de la
misma manera que lo haríamos con personas obstinadas y groseras2 .
Prepararse de antemanos para lidiar con estas reacciones puede ser beneficioso. Una es-
trategia es tratar de pensar acerca de la computadora como si fuera un empleado con cier-
tas fortalezas, como la velocidad y la precisión, y con debilidades particulares tales como
la falta de empatía y la inhabilidad para comprender la perspectiva general.
Tu trabajo es ser un buen gerente: debes encontrar maneras de tomar ventaja de las fort-
alezas y mitigar las debilidades. Y encontrar maneras de usar tus emociones para familiar-
izarte y envolverte con el problema sin dejar que tus reacciones interfieran con tu habilidad
de trabajar efectivamente.
Aprender a depurar puede ser frustrante, pero es una habilidad valiosa que será muy útil
para muchas actividades más allá de la programación. Al final de cada capítulo hay una
sección, como esta, con sugerencias para la depuración. ¡Espero que sean útiles!
1.8 Glosario
Resolución de problemas El proceso de formular un problema, encontrar una solución y
expresarla adecuadamente.
Abstracción Una manera de proveer una perspectiva de alto nivel de una tarea y ocultar
los detalles técnicos con el propósito de hacer la tarea más simple.
and Places, (Center for the Study of Language and Information, 2003).)
14 Capítulo 1. La Forma del Programa
Prompt El interpretador muestra caracteres en la pantalla para indicar que está listo para
recibir entrada de texto por el usuario.
Sentencia de impresión Una instrucción que causa que el interpretador de Perl 6 muestre
un valor en la pantalla.
Operador Un símbolo especial que representa una simple computación como la adición,
la multiplicación, o la concatenación de una cadena de texto.
Valor Una de las unidades básicas de dato, como un número o una cadena de texto, que
un programa manipula.
Tipo Una categoría de valores. Los tipos que hemos visto hasta ahora son números enteros
(tipo Int), números racionales (tipo Rat), y cadenas de texto (tipo Str).
Racional (Rat) Un tipo que representa números con partes fraccionales. Internamente,
Perl almacena un racional como dos números enteros que representan respectiva-
mente el numerador y el denominador de una fracción.
Lenguaje natural Cualquiera de los lenguajes que las personas hablan y que han evolu-
cionado naturalmente.
Lenguaje formal Cualquiera de los lenguajes que las personas han diseñado para propósi-
tos específicos tales como la representación de ideas matemáticas o programas de
computadoras; todos los lenguajes de programación son lenguajes formales.
1.9 Ejercicios
Ejercicio 1.1. Es una buena idea que lea este libro al frente de una computadora para que puedas
intentar los ejemplos según avanzas.
Cuando experimentes con una característica nueva, trata de cometer errores. Por ejemplo, en el
programa “Hello, world!”, qué pasa si no incluye una de las comillas dobles? ¿Qué pasa si no
deletreas say correctamente?
Este tipo de experimento te ayuda a recordar lo que lees; además te ayuda cuando está programando
porque te acostumbras y aprendes lo que significan los mensajes de error. Es mejor cometer errores
ahora a propósito que cometerlos después por accidente.
1.9. Ejercicios 15
Ten pendiente que la mayoría de los ejercicios en este libro tienen soluciones que se encuentran en
el apéndice. No obstante, la intención de los ejercicios en este capítulo y en el siguiente no es para
resolver un problema actual. Están diseñados para que experimentes con el interpretador de Perl; no
hay una solución correcta, solo intenta lo que se propone en los ejercicios para obtener un sentido de
cómo funciona.
1. Si intentas imprimir una cadena de texto, qué pasa si no incluye una de las comillas dobles, o
ambas?
2. Puedes usar el signo de menos para hacer números negativos como -2. ¿Qué pasa si pones un
signo de adición frente al número? ¿Qué acerca de 2++2?
3. En notación matemática, los ceros al principio son aceptables, como en 02. ¿Qué pasa si
intentas esto en Perl?
4. ¿Qué pasa si tienes dos valores sin ningún operador entre ellos, como en say 2 2;?
Ejercicio 1.2. Comienza con el interpretador REPL de Perl 6 y úsalo como una calculadora.
2. ¿Cuántas millas hay en 10 kilómetros? Pista: Hay 1.61 kilómetros en una millas.
Este ejemplo lleva a cabo cuatro asignaciones. La primera asigna una cadena de texto a
una nueva variable llamada $mensaje, la segunda asigna el número entero 42 a $número, la
tercera reasigna el entero 17 a $número, y la cuarta asigna el valor aproximado del número
áureo a $phi.
Primero, en Perl, los nombres de las variables comienzan con un símbolo conocido como
un sigilo, i.e., un carácter no alfanumérico como $, @, %, &, y otros más. Este carácter especial
nos informa a nosotros y al compilador de Perl (el programa que lee el código de nuestro
programa y lo transfoma en instrucciones de computadoras) el tipo de variable que es. Por
ejemplo, el carácter $ indica que las variables más arriba son todas variables escalares, lo
cual significa que ellas pueden almacenar un solo valor en cualquier momento. Veremos
más adelante otros tipos de variables las cuales pueden contener más de un solo valor.
Segundo, nota como las tres variables más arriba son introducidas con la palabra clave my,
que es una manera de declarar una variable nueva. Cuando tu creas una variable nueva
en Perl, necesitas declararla, i.e., le deja saber a Perl que vas a usar una variable nueva; esto
se hace comúnmente con la palabra clave my, la cual declara una variable lexical. Explicare-
mos luego lo que es una variable lexical pero por ahora, digamos que te permite crear una
variable local en una parte limitada de tu código. Una de las consecuencias buenas del
requerimiento de declarar variables antes de que las uses es que, si tu cometes un error al
escribir el nombre de una variable, el compilador usualmente será capaz de decirte que es-
tás usando una variable que no ha sido declarada y como resultado te ayudará a encontrar
tu error. Esto tiene grandes complicaciones las cuales examinaremos más tarde.
Cuando escribimos al inicio de esta sección que una variable debe ser declarada antes de ser
usada (o solo cuando se usa), esto significa que la declaración tiene que ser antes (o al punto
de) del primer uso de la variable en el archivo de texto que contiene el programa. Veremos
más adelante que los programas no son ejecutados necesariamente de arriba hacia abajo en
el orden en el que las líneas o código aparecen en el programa; aún así, la declaración de la
variable debe ocurrir antes de su uso en el archivo de texto que contiene el programa.
Si olvidas declarar una variable, obtienes un error sintáctico:
> $número = 5;
===SORRY!=== Error while compiling <unknown file>
Variable '$número' is not declared
at <unknown file>:1
------> <BOL><HERE>$número = 5;
>
Ten presente que podrías obtener mensajes diferentes de error dependiendo de la versión
de Rakudo que ejecutes. El mensaje más arriba se obtuvo en Febrero 2016; con una versión
más nueva (Octubre 2016), el mismo error se muestra en una forma algo más organizada:
>
> $número = 5;
===SORRY!=== Error while compiling:
Variable '$número' is not declared
at line 2
------> <BOL><HERE>$número = 5;
>
Una manera común de representar variables en papel es escribir el nombre con una flecha
apuntando a su valor. Este tipo de dibujo se le conoce como un diagrama de estado porque
muestra en qué estado cada una de las variables se encuentra (imagínalo como el estado
mental de la variable). La figura2.1 muestra el resultado del ejemplo anterior.
2.2. Nombres de las Variables 19
El carácter de barra baja, _, puede aparecer en cualquier parte del nombre de una vari-
able. Usualmente se usa en nombres con múltiples palabras, tal como $tu_nombre o
$airspeed_of_unladen_swallow.
Hasta puedes usar guiones para crear lo que se conoce como “kebab case”1 y nombrar esas
variables $tu-nombre o $airspeed-of-unladen-swallow, y esto las puede hacer más legi-
bles: un guion - es válido en una variable siempre y cuando sea inmediatamente seguido
1 Llamado así porque las variables aparecen estar atravesadas como las piezas de comida preparadas para una
barbacoa.
20 Capítulo 2. Variables, Expresiones y Sentencias
por un carácter alfanumérico. Por ejemplo, $doble-clic or $la-niña son nombres legí-
timos de variables. Análogamente, puedes usar un apóstrofo ' (también conocido como
comilla simple) entre las letras, así que $isn't o $o'brien's-age son identificadores váli-
dos.
Si les da un nombre ilegal a una variable, obtienes un error sintáctico:
> my $76trombones = 'big parade'
===SORRY!=== Error while compiling <unknown file>
Cannot declare a numeric variable
at <unknown file>:1
------> my $76<HERE>trombones = "big parade";
>
> my $more§ = 100000;
===SORRY!=== Error while compiling <unknown file>
Bogus postfix
at <unknown file>:1
------> my $more<HERE>§ = 100000;
(...)
$76trombones es ilegal porque comienza con un número. $more§ es ilegal porque contiene
un carácter ilegal, §.
Si alguna vez has usado otro lenguaje de programación y te has tropezado con un mensaje
terso tal como "SyntaxError: invalid syntax", notarás que los diseñadores de Perl 6
han hecho un gran esfuerzo para proveerte con mensajes de error que sean detallados,
útiles y significativos.
Muchos lenguajes de programación tienen palabras claves o palabras reservadas que son parte
de la sintaxis, tales como if, while, o for, y por lo tanto no pueden ser usadas para iden-
tificar variables porque eso crearía ambigüedad. Dicho problema no existe en Perl: dado
que los nombres de las variables comienzan con un sigilo, el compilador es siempre capaz
de diferenciar entre una palabra clave y una variable. Nombres tales como $if o $while
son sintácticamente identificadores válidos de variables en Perl (si estos nombres hacen
sentido es un asunto diferente).
> my $n = 17;
17
> say $n;
17
La primera línea es una sentencia de asignación que asigna un valor a $n. La segunda línea
es una sentencia de impresión que muestra el valor de $n.
Cuando escribes una sentencia y después presiona Enter, el interpretador la ejecuta, lo
que significa que hace lo que la sentencia dicta.
Una sentencia puede ser combinada con expresiones usando los operadores aritméticos.
Por ejemplo, podrías escribir:
Aquí, $respuesta es primero declarada con un valor de 17. La siguiente sentencia asigna
a $respuesta el valor actual de $respuesta (i.e., 17) + 25. Esta operación es tan común en
Perl, como en otro lenguajes de programación, que tiene un atajo:
> my $n = 17;
17
> ++$n;
18
> say $n;
18
perl6 mi_script.pl6
Dado que Perl provee ambos modos, puedes ejecutar piezas de código en el modo interac-
tivo antes que las pongas en un script. Pero existen diferencias entre el modo interactivo y
el modo script que pueden confundir.
Por ejemplo, si estás usando el interpretador de Perl 6 como una calculadora, podrías es-
cribir:
La primera línea asigna un valor a $millas y muestra ese valor. La segunda línea es una ex-
presión, así que el interpretador la evalúa y muestra el resultado. Resulta que un maratón
es alrededor de 42 kilómetros.
Pero si escribes el mismo código en un script y lo ejecutas, no obtienes ningún resultado. En
el modo script, una expresión por sí misma no tiene ningún efecto visible. Perl actualmente
evalúa la expresión, pero no muestra el valor a menos que se lo indiques:
my $millas = 26.2;
say $millas * 1.61;
say 1;
my $x = 2;
say $x;
1
2
5;
my $x = 5;
$x + 1;
El one-liner más arriba puede no parecer útil, pero los one-liners desechables pueden ser
muy prácticos para realizar operaciones simples, tal como modificar un archivo que no está
formateado apropiadamente, sin tener que guardar un script en un archivo separado antes
de ejecutarlo.
No daremos ningunos detalles adicionales sobre el modo de una sola línea, pero dare-
mos más ejemplos útiles más adelante en este libro; por ejemplo, Subsección 8.4.1, Sub-
sección A.5.9 (resolviendo el ejercicio “rot-13”), o Subsección A.6.1 (resolviendo el ejercicio
sobre letras dobles consecutivas).
• Paréntesis tienen la mayor precedencia y pueden ser usados para forzar una ex-
presión a evaluar en el orden que deseas. Dado que las expresiones en paréntesis
son evaluadas primero, 2 * (3-1) es 4, y (1+1)**(5-2) es 8. También puedes usar
paréntesis para hacer una expresión más legible, como en ($minuto * 100) / 60,
aunque no cambia el resultado.
• Exponente (potenciación) es la siguiente en el nivel de precedencia, así que 1 + 2**3
es 9 (1 + 8), no 27, y 2 * 3**2 es 18, no 36.
• Multiplicación y División tiene mayor precedencia que Adición y Sustracción. Por
lo tanto, 2*3-1 es 5, no 4, y 6+4/2 es 8, no 5.
2 Los estudiantes estadounidenses se les enseña a usar el mnemónico "Please Excuse My Dear Aunt Sally" para
> '2'-'1a'
Cannot convert string to number: trailing characters after number
in '1?a' (indicated by ?)
in block <unit> at <unknown file>:1
Pero las siguientes expresiones son válidas porque estas cadenas de texto pueden ser coac-
cionadas en números sin ninguna ambigüedad:
> '2'-'1'
1
> '3'/'4'
0.75
El operador ~ realiza la concatenación de cadenas de texto, lo cual quiere decir que une las
cadenas de texto al enlazarlas de extremo a extremo. Por ejemplo:
El operador x también funciona con cadenas de texto; básicamente realiza repeticiones. Por
ejemplo:
> 'ab' x 3;
ababab
> 42 x 3
424242
> 3 x 42
333333333333333333333333333333333333333333
2.8 Comentarios
A medida que los programas crecen y se vuelven más complicados, se vuelven menos
legibles. Los lenguajes formales son densos, y es usualmente difícil mirar a un fragmento
de código y descifrar lo que está haciendo, o por qué lo está haciendo.
Por esta razón, es muy buena idea agregar notas a tus programas que expliquen en un
lenguaje natural lo que el programa hace. Estas notas son conocidas como comentarios, y
comienzan con el símbolo #:
En este caso, el comentario aparece en una línea por sí mismo. También puedes colocar
comentarios al final de una línea:
Todo desde el inicio de # al final de la línea es ignorado—lo que significa que no tiene un
efecto en la ejecución del programa.
Los comentarios son más útiles cuando documentan características del código que no son
obvias. Es razonable asumir que el lector puede descifrar lo que hace el código; es más útil
explicar el por qué.
Este comentario es redundante e inservible debido a que no brinda ninguna información
que no pueda deducirse del mismo código:
Por el contrario, este comentario contiene información útil que no está presente en el
código:
Los nombres de variables que son buenos y adecuados pueden reducir la necesidad de
comentarios, pero nombres muy largos pueden hacer que las expresiones sean difíciles de
leer, por lo tanto debe existir un balance.
2.9. Depuración de Programas 27
Error semántico El tercer tipo de error es semántico, lo que significa que está relacionado
con el significado. Si en tu programa hay un error semántico, el programa se ejecutará
sin generar mensajes de error. Específicamente, hará lo que le dijiste que hiciera, pero
no lo que habías previsto.
La identificación de errores semánticos puede ser complicada porque requiere que
trabajes de atrás hacia adelante mediante la observación de la salida del programa y
tratando de descifrar lo que el programa está haciendo.
2.10 Glosario
Variable Informalmente, un nombre que hace referencia a un valor. En términos precisos,
una variable es un contenedor que tiene un nombre y almacena un valor.
Palabra clave Una palabra reservada que es usada para parsear un programa; en muchos
lenguajes, no puedes usar palabras claves tales como if, for, y while como nombres
de variables. Este problema usualmente no ocurre en Perl porque los nombres de
variables comienzan con un sigilo.
3 Aquí estamos usando “error sintáctico” como un cuasi-sinónimo para “error al tiempo de compilación“; ellos
no son exactamente la misma cosa (en teoría, puedes tener errores sintácticos que no son errores al tiempo de
compilación y viceversa), pero se pueden considerar aquí lo mismo por razones prácticas. En Perl 6, errores al
tiempo de compilación tienen la cuerda de texto “===SORRY!===” al inicio del mensaje de error.
28 Capítulo 2. Variables, Expresiones y Sentencias
Evaluar Simplificar una expresión al realizar las operaciones para obtener un valor único.
Sentencia Una sección de código que representa un comando o una acción. Hasta ahora,
las sentencias que hemos visto son las asignaciones y las sentencias de impresión.
Las sentencias usualmente terminan con un punto y coma.
Modo script Una manera de usar el interpretador de Perl para leer código desde un script
y ejecutarlo.
Modo de una sola línea (one-liner) Una manera de usar el interpretador de Perl para leer
código pasado al prompt del sistema operativo y ejecutarlo.
Order de operaciones Reglas que dictan el orden en el que las expresiones que contienen
varios operadores y operandos son evaluadas. También se conoce como precedencia
de los operadores.
Error semántico Un error en un programa que causa que haga algo diferente a lo que
programador pretendía hacer.
2.11 Ejercicios
Ejercicio 2.1. Repetimos nuestro consejo mencionado en el capítulo anterior, cuando aprendas algo
nuevo, deberías intentarlo en el modo interactivo (de REPL) y cometer errores a propósito para ver
que es lo que no funciona.
• ¿Qué acerca de $x = $y = 1? (Pista: nota que tendrás que declarar ambas variables, por
ejemplo con una sentencia tal como my $x; my $y; o posiblemente my ($x, $y);, antes
de que ejecutes lo anterior.)
• En algunos lenguajes, las sentencias no tienen que terminar con un punto y coma, ;. ¿Qué
pasa en el modo script si omites un punto y coma al final de una sentencia de Perl?
• En notación matemática puedes multiplicar m y n así: mn. ¿Qué pasa si tratas de hacerlo en
Perl?
Ejercicio 2.2. Practica usando el interpretador de Perl como una calculadora:
1. El volumen de una esfera con radio r es 34 πr3 . ¿Cuál es el volumen de una esfera con radio 5?
2. Supón que el precio inicial de un libro es $24.95, pero la librería obtiene un 40% de descuento.
El envío cuesta $3 por la primera copia y 75 centavos por cada copia adicional. ¿Cuál es el
precio total de 60 copias?
3. Si dejo la casa a la 6:52 a.m. y corro 1 milla a paso suave (8 min. 15 seg. por milla), después
3 millas a paso rápido (7 min. 12 seg por milla) y 1 milla a paso suave otra vez. ¿Qué hora es
cuando termino con mi ejercicio?
30 Capítulo 2. Variables, Expresiones y Sentencias
3
Funciones
y el paréntesis izquierdo. Por ejemplo, la función round usualmente toma dos argumen-
tos: el valor a ser redondeado y la unidad o escala. Puedes invocarla en cualquiera de las
siguientes formas:
Los programadores de Perl con experiencia prefieren omitir los paréntesis cuando pueden
hacerlo. Esto hace posible encadenar varias funciones con una sintaxis más clara. Consid-
era por ejemplo la diferencia entre estas dos llamadas:
Es muy común decir que una función “toma“ uno o varios argumentos y “devuelve“ un
resultado. El resultado es también llamado valor de retorno.
Perl provee varias funciones que convierten valores de un tipo a otro. Cuando se invocan
con un solo argumento, la función round toma cualquier valor y lo convierte en un número
entero, o por el contrario se queja:
Nota que, en Perl 6, muchas funciones integradas pueden también usar la sintaxis de la lla-
mada de método con la tal llamada de notación de punto. Las siguientes sentencias muestran
el mismo resultado:
La función round puede redondear valores racionales y valores de coma flotante. Hay
un método Int que también convierte valores numéricos no enteros en enteros, pero no
redondea sino que descarta la parte fraccional:
Nota que estas funciones de conversión de tipo no necesitan ser invocadas explícitamente,
dado que Perl intentará hacer lo correcto en muchos de los casos. Por ejemplo, si tienes
una cadena de texto que se parece a un número entero, Perl coaccionará la cadena de texto
en un entero si tratas de aplicarle una operación aritmética:
> say 4 ~ 2;
42
> say (4 ~ 2).WHAT;
(Str)
34 Capítulo 3. Funciones
La coacción puede ocurrir hasta dos veces dentro de la misma expresión si es necesario:
> say (4 ~ 1) + 1;
42
> say ((4 ~ 1) + 1).WHAT;
(Int)
Depende de ti cuál de las dos formas prefieres usar, pero aquí usaremos ambas formas, aún
solo sea para acostumbrarnos a ambas.
use Math::Trig;
Esta sentencia importará un número de funciones que serás capaz de usar como si tu las
hubieses definido en el archivo de código principal, por ejemplo grad2rad convierte un
valor angular desde grados a radianes o rad2grad que hace la conversión opuesta.
Para la mayoría de funciones matemáticas comunes, sin embargo, no necesitas tener
ningún módulo matemático, dado que dichas funciones ya están incluidas en el núcleo
del lenguaje:
El primer ejemplo usa log10 (logaritmo común) para computar la razón entre la señal
y el ruido en decibelios (asumiendo que nivel-señal y nivel-ruido sean definidos en
las unidades correctas). Perl también provee una función log, la cual, cuando recibe un
argumento, calcula el logaritmo en base e del argumento y, cuando recibe dos argumentos,
computa el logaritmo del primer argumento en base del segundo argumento:
Este ejemplo encuentra el seno de $radianes. El nombre de la variable es una pista a que el
sin y otras funciones trigonométricas (cos, tan, etc.) toman argumentos en radianes. Para
convertir de grados a radianes, puedes usar la función deg2rad del módulo Math::Trig, o
simplemente divide por 180 y multiplica por π:
3.4 Composición
Hasta ahora, hemos discutido los elementos de un programa—variables, expresiones, y
sentencias—aisladamente, sin discutir sobre cómo combinarlos.
Una de las características más útiles de los lenguajes de programación es su habilidad de
tomar pequeños componentes y unir estos componentes, i.e., combinarlos de tal manera
que el resultado de uno es la entrada de otro. Por ejemplo, el argumento de una función
puede ser cualquier tipo de expresión, incluyendo operadores aritméticos:
Aquí, hemos usados paréntesis para el argumento de la función sin para clarificar que to-
das las operaciones aritméticas sean completadas antes que la función sin sea actualmente
llamada, para que así use el resultado de estas operaciones como su argumento.
Puedes componer llamadas de funciones:
> my $x = 10;
10
> $x = exp log($x+1)
11
Casi en cualquier lado que puedes poner un valor, puedes poner una expresión arbitraria,
con una excepción: el lado izquierdo de la sentencia de asignación tiene que ser el nombre
de una variable, posiblemente con su declaración. Casi cualquier otra expresión en el lado
izquierdo es un error sintáctico 1 :
> my $horas = 1;
1
> my $minutos = 0;
0
> $minutos = $horas * 60; # correcto
60
> $horas * 60 = $minutos; # incorrecto!!
Cannot modify an immutable Int
in block <unit> at <unknown file> line 1
1 Veremos una rara excepción a esta regla más tarde
3.5. Agregar Nuevas Funciones (o Subrutinas) 37
sub imprimir-discurso() {
say "Let freedom ring from the prodigious hilltops of New Hampshire.";
say "Let freedom ring from the mighty mountains of New York.";
}
sub es la palabra clave que indica que esto es una definición de subrutina. El nombre de la
función es imprimir-discurso. Las reglas para los nombres de subrutinas son las mismas
reglas para los nombres de variables: letras, números, y guiones bajos son legales, al igual
que un guion o un apóstrofo entre letras, pero el primer carácter debe ser una letra o un
guion bajo. No deberías utilizar una palabra clave usada por el lenguaje (tal como if o
while) como el nombre de una función (en algunos casos, podría actualmente funcionar
pero sería algo confuso, por lo menos para el lector humano).
Los paréntesis vacíos después del nombre indican que esta función no toma ningún ar-
gumento. En tal caso los argumentos son opcionales, pero son requeridos cuando los
parámetros necesitan ser definidos durante la definición de la subrutina.
La primera línea de la definición de una subrutina es algunas veces llamada la cabecera;
el resto es conocido como el cuerpo de la subrutina. El cuerpo tiene que ser un bloque
de código colocado entre llaves y puede contener una cantidad indefinida de sentencias.
Aunque no existe una regla que lo requiera, es buena práctica (y muy recomendado) inden-
tar las sentencias en el cuerpo con varios espacios delanteros, debido a que facilita descifrar
visualmente donde el cuerpo de la función inicia y termina.
Ten presente que no puedes utilizar la sintaxis de invocación de método con las subrutinas
(tal como imprimir-discurso) que escribas: debes llamarlas con la sintaxis de llamada de
función.
Las cadenas de texto en las sentencias de impresión están rodeadas por comillas dobles.
En este caso específico, las comillas simples se podrían haber usado para obtener el mismo
resultado, pero hay otros casos donde los dos tipos de comillas no harían lo mismo, así que
tendrás que elegir una de las dos dependiendo de las circunstancias.
Muchas personas usan las comillas dobles en aquellos casos donde una comilla simple (la
cual también es un apóstrofo) aparece en una cadena de texto:
De igual manera, podrías utilizar comillas simples cuando las comillas dobles aparecen en
una cadena de texto:
38 Capítulo 3. Funciones
Sin embargo, existe una diferencia más importante entre las comillas simples y las comillas
dobles: las comillas dobles permiten la interpolación de variables, lo cual no es posible con
el uso de las comillas simples. La interpolación de variables se refiere a que si el nombre
de una variable aparece dentro de una cadena de texto rodeada por comillas dobles, dicho
nombre será reemplazado por el valor de la variable; dentro una cadena de texto rodeada
por comillas simples, el nombre de la variable aparecerá literalmente. Por ejemplo:
my $var = 12;
say "La edad del niño es $var."; # -> La edad del niño es 12.
say 'La edad de la niña es $var.'; # -> La edad de la niña es $var.
> imprimir-discurso();
Let freedom ring from the prodigious hilltops of New Hampshire.
Let freedom ring from the mighty mountains of New York.
sub repetir-discurso() {
imprimir-discurso();
imprimir-discurso();
}
> repetir-discurso();
Let freedom ring from the prodigious hilltops of New Hampshire.
Let freedom ring from the mighty mountains of New York.
Let freedom ring from the prodigious hilltops of New Hampshire.
Let freedom ring from the mighty mountains of New York.
sub imprimir-discurso() {
say "let freedom ring from the prodigious hilltops of New Hampshire.";
say "Let freedom ring from the mighty mountains of New York.";
}
sub repetir-discurso() {
imprimir-discurso();
imprimir-discurso();
}
repetir-discurso();
repetir-discurso;
sub repetir-discurso() {
imprimir-discurso;
imprimir-discurso;
}
sub imprimir-discurso() {
# ...
}
función. ¡Después, mientra se encuentra en dicha función, el programa podría aún ejecutar
otra función!
Afortunadamente, Perl es lo suficientemente bueno para rastrear dónde se encuentra, así
que cada vez una función se completa, el programa continúa donde se quedó en el pro-
grama que lo llamó. Cuando llega al final del programa, entonces termina.
En resumen, cuando leas un programa, no siempre quieras leerlo de arriba hacia abajo. En
algunas ocasiones hace más sentido si sigues el flujo de ejecución.
Esta subrutina asigna el argumento al parámetro llamado $valor. Otra manera común de
interpretarlo es decir que la subrutina ata el parámetro definido en su cabecera al argu-
mento con el cual es llamada. Cuando la subrutina anterior es llamada, la misma imprime
el contenido del parámetro (cualquier cosa que sea) dos veces.
Esta función funciona con cualquier argumento que pueda ser imprimido:
Las mismas reglas de composición que aplican a la funciones integradas también aplican
a las funciones definidas por el programador, así que podemos llamar a doble-impresión
con cualquier tipo de expresión como un argumento:
El argumento es evaluado antes que la función sea llamada, así que en los ejemplos las
expresiones 'Let freedom ring! ' x 2 y cos pi son evaluadas una sola vez.
Esta función toma dos argumentos, los concatena, e imprime el resultado dos veces. Este
es un ejemplo que la usa:
Los parámetros también están en un ámbito lexical en la subrutina. Por ejemplo, fuera de
doble-impresión, no existe tal variable como $valor.
42 Capítulo 3. Funciones
Cada función se representa gráficamente por un marco. Un marco es una cuadro con el
nombre de la función al lado y los parámetros y las variables de la función dentro. El
diagrama de pila para el ejemplo anterior se muestra en la Figura 3.1.
Los marcos están organizados en la pila para indicar cual función llama a cual. En este
ejemplo, doble-impresión fue llamada por doble_concat, y doble_concat fue llamada
por main, el cual es un nombre especial para el marco superior. Cuando creas una variable
fuera de cualquier función, dicha variable le pertenece a main.
Cada parámetro hace referencia al mismo valor que su argumento correspondiente. Así
que, $parte1 tiene el mismo valor que $inicio, $parte2 tiene el mismo valor que $final
y $valor tiene el mismo valor que $concatenación.
En algunos lenguajes de programación, tales como Pascal o Ada, existe una gran distinción
entre una función (la cual devuelve un valor) y un procedimiento (el cual no devuelve ningún
3.11. Funciones Fructuosas y Funciones Voids 43
valor); ellas son definidas con diferentes palabras claves. Dicha distinción no aplica en Perl
ni en la mayoría de los lenguajes de programación modernos.
De hecho, desde un punto de vista sintáctico, las funciones de Perl siempre devuelven un
valor. Así que la distinción entre una función “fructuosa“ y “void“ no existe sintáctica,
sino semánticamente, i.e., desde el punto de vista del significado del programa: tal vez
necesitamos usar el valor devuelto, o tal vez no.
Otra distinción que se hace comúnmente entre funciones y mutadores es que las funciones
no cambian el estado inicial de los argumentos que se les pasa mientras que los mutadores
modifican los argumentos. No usaremos esta distinción aquí, pero es útil mantenerla pre-
sente.
Cuando llamas una función fructuosa, es muy común que quieras hacer algo con el resul-
tado; por ejemplo, podrías asignarlo a una variable o usarlo como parte de una expresión:
Cuando llamas una función en el modo interactivo (en el REPL), Perl 6 usualmente muestra
el resultado inmediatamente:
> sqrt 5;
2.23606797749979
¡Pero en un script, si llamas una función fructuosa por sí misma, el valor de retorno se
pierde para siempre! En algunos casos, el compilador será capaz de advertirte, pero no
siempre. Por ejemplo, considera el siguiente programa:
my $cinco = 5;
sqrt $cinco;
say $cinco;
El script calcula la raíz cuadrada de 5, pero dado que no almacena o muestra el resultado,
no es muy útil.
Las funciones void muestran algo en la pantalla, guardan algún dato en un archivo, modi-
fican una variable o un objeto, o tienen algún otro efecto, pero generalmente no tienen un
valor de retorno, o por lo menos algo no muy útil. Si asignas el resultado a una variable,
puedes conseguir el valor de retorno de la subrutina, el valor de la última expresión que
fue evaluada en la función, o un valor especial tal como Any, el cual esencialmente se refiere
a algo que no ha sido definido, o Nil.
Las subrutinas que hemos escrito hasta ahora esencialmente muestran cosas en la pantalla.
En ese caso, ellas devuelven True, por lo menos cuando la impresión fue exitosa. Aunque
ellas devuelven un valor verdadero, lo que ellas devuelven no es muy útil y podemos
considerarlas funciones void para nuestro propósito.
El siguiente es un ejemplo de una subrutina fructuosa bien simple:
44 Capítulo 3. Funciones
Si está función es llamada con una cadena de texto, conseguimos el siguiente error:
El tipo Int incluido en la signatura de la función es una restricción de tipo que puede
ayudar a prevenir errores sutiles. En algunos casos, también pueden ser una molestia.
Considera el fragmento de código:
3.13. Parámetros Inmutables y Mutables 45
Dado que el argumento de la subrutina mitad es "84", i.e., una cadena de texto, este código
fallará con un error de tipo. Si no hubiésemos incluidos el tipo Int en la signatura, el script
habría convertido (o coaccionado) la cadena de texto "84" en un número, dividido por 2, y
mostrado el resultado esperado:
En algunos casos, quieres que esta conversión ocurra, en otros, no quieres esto. Es tu deber
decidir si quieres tipado estricto o no, dependiendo en la situación específica y necesidad.
Es probablemente útil usar tipos en los parámetros en muchos casos, pero puede volverse
un obstáculo en algunas situaciones. Perl 6 te deja decidir qué tan estricto deseas ser sobre
estas cosas.
Nuestra subrutina original mitad tiene otra limitación: solo funciona con números enteros.
Pero una función que devuelve la mitad de su argumento debería supuestamente ser útil
para números racionales o hasta otros números. Puedes usar los tipos Real o Numeric para
hacer la función más general (la diferencia entre los tipos es que el tipo Numeric aceptará
no solo números Reales pero también números complejos (Complex) 2 ). Como sucede con
números complejos, el elegir un tipo Numeric abre más posibilidades:
La siguiente tabla resume e ilustra algunos de los varios tipos que hemos visto hasta ahora.
Tipo Ejemplo
String "Una cadena de texto (string)", 'Hola', "42"
Integer -3, -2, 0, 2, 42
Rational 1/2, 0.5, 3,14159, 22/7, 42.0
√
Real π, pi, 2, e, log 42, sin 0.7
Complex 5.4 + 3i
a y b son un números reales y i es un número imaginario de tal modo que i2 es igual a −1.
46 Capítulo 3. Funciones
En algunos otros lenguajes, este comportamiento es conocido como una “llamada por
valor“: en términos generales, la subrutina recibe (por defecto) un valor y no una vari-
able, y el parámetro por lo tanto no puede ser modificado.
Si quieres cambiar el valor del parámetro dentro de la subrutina (pero sin cambiar el argu-
mento), puedes agregar el rasgo is copy a la signatura:
Un rasgo (trait en inglés) es una propiedad del parámetro definida al tiempo de compi-
lación. Aquí, el parámetro $número es modificado dentro de la subrutina y el valor incre-
mentado es devuelto e imprimido como 8, pero, dentro de la función que hace la llamada,
la variable usada como un argumento para la función, $valor, no es modificada (es todavía
5).
Aunque esto puede ser algunas veces peligroso, puedes también querer escribir una sub-
rutina que modifica su argumento en el lugar donde la función es llamada. Para esto,
puedes usar el rasgo is rw en la signatura:
Con el rasgo is rw, el parámetro $número está ahora vinculado al argumento $valor, así
que cualquier cambio hecho al usar $número dentro de la subrutina será inmediatamente
aplicado a $valor en lugar donde la función es llamada, porque $número y $valor son
solo nombres diferentes para la misma cosa (ambos hacen referencia a la misma dirección
de memoria). El argumento es ahora totalmente mutable.
En algunos otros lenguajes de programación, esto se conoce como “llamada por referen-
cia“, porque, en aquellos lenguajes, si tu pasas una referencia (o un puntero) de una vari-
able a una función, entonces es posible que la función pueda modificar la variable referida
por la referencia.
Aquí, el parámetro $código se refiere a una función o cualquier otro objeto que se pueda
llamar. Esto es un ejemplo que usa do-twice para llamar una función saludar dos veces:
sub saludar {
say "¡Hola Mundo!";
}
do-twice &saludar;
Esto imprimirá:
¡Hola Mundo!
¡Hola Mundo!
El sigilo & se coloca antes del nombre de la subrutina en la lista de argumentos para dejarle
saber a Perl que estás pasando una subrutina o cualquier objeto de código que se puede
llamar (y no estás llamando la subrutina en el momento).
De hecho, sería más idiomático usar también el sigilo & en la definición de la subrutina
do-twice para especificar que el parámetro es un objeto de código que se puede llamar:
De igual manera:
La sintaxis con el sigilo & tiene el beneficio de proveer un mejor mensaje de error si cometes
un error y le pasas a do-twice algo que no se puede llamar.
Todas las funciones que hemos visto hasta ahora han tenido nombres, pero no es necesario
que una función tenga un nombre y por lo tanto, puede ser una función anónima. Por
ejemplo, se puede almacenar directamente en una variable escalar:
my $saludar = sub {
say "¡Hola Mundo!";
};
$saludar(); # imprime "¡Hola Mundo!"
do-twice $saludar; # imprime "¡Hola Mundo!" dos veces
Para casos simples donde no hay necesidad de pasar un argumento o devolver un valor,
puedes omitir la palabra clave sub y pasar un bloque de código directamente a la función:
Como puedes observar, do-twice es una subrutina genérica cuyo trabajo es ejecutar dos
veces cualquier función o bloque de código que se le pasa, sin ningún conocimiento sobre
lo que la función o el bloque de código hace. Este es un concepto poderoso usado en
algunas técnicas de programación avanzadas que discutiremos más adelante.
Las subrutinas se pueden también pasar como valores de retorno desde otras subrutinas:
Aquí, la función crear-función devuelve una subrutina que saluda a alguien. Se llama
dos veces con dos argumentos diferentes para crear dos funciones diferentes al tiempo
de ejecución, $saludar_mundo y $saludar_amigo. Una función como crear-función es
algunas veces conocida como una factoría de funciones porque puedes crear un número de-
terminado de funciones con solo llamar a crear-función. Este ejemplo puede parecer una
manera complicada de hacer algo tan simple. Lo que pasa es que en este momento es
muy prematuro dar ejemplos útiles, no obstante esta es una técnica de programación muy
poderosa.
Regresaremos a estas técnicas a lo largo de este libro y hasta dedicaremos un capítulo en-
tero (capítulo 14) para este asunto y otros temas relacionados.
3.17 Glosario
Función Una secuencia de sentencias que realiza una trabajo útil. Las funciones pueden
tener cero o más argumentos y pueden producir o no producir un resultado. Perl
viene con muchas funciones integradas, y tú puedes crear tus propias funciones. En
Perl, las funciones definidas por el usuario son usualmente conocidas como subruti-
nas.
Definición de una función Una sentencia que crea una función nueva, especifica su nom-
bre, sus parámetros, y las sentencias que contiene.
Parámetro Un nombre que se usa dentro de la subrutina para referirse al valor que se pasa
como un argumento.
Llamada de función Una sentencia que ejecuta la función. Consiste del nombre de la fun-
ción seguido por una lista de argumentos, la cual puede o no puede estar encerrada
por paréntesis.
Argumento Una valor que se provee a la función cuando ésta se llama. Este valor es
asignado al parámetro correspondiente en la función.
Variable lexical Una variable definida dentro de una subrutina o un bloque de código.
Una variable lexical definida dentro de una función se puede usar solo dentro de la
función.
Valor de retorno El resultado de una función. Si una llamada de función se usa como una
expresión, el valor de retorno es el valor de la expresión.
Nil También un valor especial devuelto algunas veces por las subrutinas “void“.
Módulo Una archivo que contiene una colección de funciones relacionadas y otras defini-
ciones.
3.18. Ejercicios 51
Sentencia use Una sentencia que lee un módulo y usualmente exporta algunas funciones.
Composición Usar una expresión como parte de una expresión más compleja o una sen-
tencia como parte de una sentencia más larga.
Diagrama de pila Una representación gráfica de una pila de subrutinas, sus variables, y
los valores a los cuales hacen referencia.
Marco Una caja en un diagrama de pila que representa una llamada de subrutina. Con-
tiene las variables locales y los parámetros de la subrutina.
Parámetros inmutables Una parámetro de una función o subrutina que no se puede cam-
biar dentro del cuerpo de la función. Por defecto, los parámetros de una subrutinas
son inmutables.
Objeto de primera clase Las subrutinas de Perl se dice que son objetos de orden superior
o objetos de primera clase porque ellas se pueden pasar como argumentos a otras
subrutinas o como valores de retorno, como cualquier otro objeto.
Factoría de funciones Una función de produce otras funciones como valores de retorno.
3.18 Ejercicios
Ejercicio 3.1. Escribe una subrutina llamada alinear-a-derecha que toma una cadena de texto
llamada $cadena-entrada como parámetro e imprime la cadena de texto con suficiente espacio
blanco de tal modo que la última letra de la cadena de texto esté en la columna 70 de la pantalla.
Pista: Usa la concatenación de cadena de texto y repetición. De igual manera, Perl provee una
función integrada llamada chars que devuelve la longitud de una cadena, así que el valor de
chars 'Larry Wall' or 'Larry Wall'.chars es 10. Solución: A.1.1.
Ejercicio 3.2. Hemos visto que las funciones y otros objetos de código se pueden pasar como val-
ores, tal como cualquier objeto. Se dice que las funciones objetos de primera clase. Por ejemplo,
do-twice es una función que toma una función como un argumento y la llama dos veces:
52 Capítulo 3. Funciones
Solución: A.1.2.
Ejercicio 3.3. Nota: Debes resolver este ejercicio con solo el uso de las sentencias y otras caracterís-
ticas que hemos discutido hasta ahora.
2. Escribe una subrutina que dibuje una cuadrícula similar con cuatro filas y cuatro columnas.
Solución: A.1.3.
Crédito: Este ejercicio está basado en un ejercicio en Oualline, Practical C Programming, Third
Edition, O’Reilly Media, 1997.
54 Capítulo 3. Funciones
4
Bucles, Condicionales y Recursión
El tema principal de este capítulo es la sentencia if, la cual ejecuta código diferente depen-
diendo del estado del programa. Pero primero quiero introducir dos operadores nuevos:
división de enteros y módulo.
Sin embargo, normalmente nosotros no escribimos las horas con puntos decimales. La
división de enteros devuelve el número entero de horas, descartando la parte fraccional:
Una alternativa es usar el operador de módulo, %, el cual divide dos números y devuelve
el residuo:
Para ser sincero, Perl 6 también tiene un operador específico para la divisibilidad, %%. La
expresión $dividendo %% $divisor devuelve verdadero si $dividendo %% $divisor es
igual a 0, es decir si $dividendo es divisible por $divisor (de lo contrario, devuelve falso):
> 42 %% 2;
True
De igual manera, puedes extraer el dígito (o dígitos) más a la derecha de un número con el
operador de módulo. Por ejemplo, $x % 10 extrae el dígito más a la derecha de $x (en base
10). Similarmente, $x % 100 extrae los últimos dos dígitos:
> 5 == 5;
True
> 5 == 6;
False
True y False son valores especiales que pertenecen al tipo Bool; ellos no son cadenas de
texto:
$x != $y # $x no es numéricamente igual a $y
$x > $y # $x es numéricamente mayor que $y
$x < $y # $x es numéricamente menor que $y
$x >= $y # $x es numéricamente mayor o igual a $y
$x <= $y # $x es numéricamente menor o igual a $y
$x === $y # $x y $y son verdaderamente idénticos
Aunque estas operaciones probablemente son familiares, los símbolos en Perl son difer-
entes a los símbolos matemáticos. Un error común es usar un solo signo de igualdad (=) en
vez de dos signos de igualdad (==). Recuerda que = es el operador de asignación y == es un
operador de relación. No existe tal cosa como =<, aunque existe el operador =>, el cual no
es un operador de relación sino algo completamente distinto (como veremos más adelante,
dicho operador es el constructor de pares).
La diferencia entre == y === es que el primer operador chequea si los valores de los operan-
dos son iguales y el último chequea si los operandos son verdaderamente idénticos. Como
un ejemplo, considera esto:
Estos operadores de relación pueden solo comparar valores numéricos (números o vari-
ables que contienen números) o valores que se pueden coaccionar en valores numéricos,
tal como, por ejemplo, la cadena de texto "42" la cual, si se usa con estos operadores (ex-
cepto ===), serán coaccionada en el número 42.
Para la comparación de cadenas de texto (en un tipo de comparación lexicográfica o
“pseudo-alfabética“), necesitas usar los operadores de relación de cadena de texto:
Por ejemplo, puedes comparar (alfabéticamente) los nombres de dos presidentes de los
Estados Unidos:
Es necesario aclarar que los operadores de relación numérica y los operadores de relación
de cadena de texto no funcionan de la misma manera (y esta es una buena razón para
tener operadores diferentes), porque ellos no poseen la misma idea de lo que es mayor que
o menor que.
Al comparar dos enteros positivos, un número con cuatro dígitos es siempre mayor que un
número con solo dos o tres dígitos. Por ejemplo, 1110 es mayor que 886.
También existen los operadores de relación de “tres sentidos“, cmp, <=> y leg, pero regre-
saremos a ellos cuando estudiemos cómo ordenar los elementos de una lista. Similarmente,
necesitamos aprender otras cosas sobre Perl antes que podamos justificar el increíblemente
poderoso y expresivo operador de coincidencia inteligente, ~~.
Un punto final acerca de la comparación de cadenas de texto es que las letras mayúscu-
las son siempre menores que las letras minúsculas. Así que "A", "B", "BB" y "C" son todas
menores que "a", "b", "bb", y "c". No discutiremos los detalles aquí pero esto se vuelve com-
plicado (y algo confuso) cuando las cadenas de texto a ser comparadas contienen caracteres
no alfabéticos (o letras Unicode que no son ASCII).
• lógico o : “or” y ||
• lógico no : “not” y !
Finalmente, el operador not niega una expresión Booleana, así que not (x > y) es ver-
dadero si x > y es falso, es decir, si x es menor o igual a y.
Los operadores &&, ||, y ! tienen el mismo significado, respectivamente, como and, or, y
not, pero una precedencia más estricta, lo que significa que cuando están en una expresión
con otros operadores, ellos tienen una mayor precedencia de ejecución. Volveremos con
la precedencia más adelante, pero digamos por el tiempo presente que, en los casos más
comunes, los operadores and, or, y not usualmente serán suficientes para hacer lo que
deseas.
En sentido estricto, los operandos de los operadores lógicos deberían ser expresiones
Booleanas, pero Perl, como muchos otros lenguajes derivados parcialmente de C, no es
estricto con respecto a eso. Los números 0 y 0.0 son falsos (False); y cualquier número que
no sea cero o una cadena de cuerda que no esté vacía es interpretado como True:
Aunque esta flexibilidad puede ser muy útil, existen sutilezas que pueden ser confusas. A
menos que sepas lo que estás haciendo, esto es algo que deberías evitar.
La función integrada so devuelve una evaluación de sus argumentos:
Aquí la expresión (0 and True) es falsa porque 0 es falso y la expresión sería verdadera
solo si ambos argumentos del operador and son verdaderos.
Cuando varias condiciones Booleanas son enlazadas con algunos operadores lógicos, Perl
solo realizará la comparaciones que son estrictamente necesarias para descifrar el resultado
final, comenzando con aquellas en la izquierda. Por ejemplo, si escribes:
no hay necesidad de evaluar la segunda expresión Booleana para saber que la expresión
completa será falsa. En este caso, Perl no trata de chequear si el número es positivo o hasta
si está definido. Se dice que estos operadores realizan una evaluación de “corto-circuito“
con las condiciones innecesarias.
De la misma manera, en el siguiente código, la subrutina calcular-pensión no será lla-
mada si la edad de la persona es menos de 65:
Lo mismo ocurre con el operador or, pero de manera completamente distinta: si la primera
expresión booleana de una sentencia or es verdadera, entonces la siguiente expresión no
será evaluada. El siguiente código es equivalente al anterior:
la cual aborta el programa si haz-algo devuelve un valor falso, lo cual quiere decir que fue
incapaz de hacer algo tan esencial que no vale la pena continuar con su ejecución.
Más adelante examinaremos formas más claras y comunes de ejecutar código con condi-
cionales.
if $número > 0 {
say '$número es positivo';
}
Nota que en la sentencia de impresión anterior, el punto y coma final se omitió. Cuando
una sentencia es la última línea de código de un bloque, inmediatamente antes de la llave
derecha } que cierra el bloque, el punto y coma final es opcional y puede ser omitido,
aunque sería recomendable incluirlo.
En teoría, el fragmento de código anterior es por sí mismo una sentencia y debería también
terminar con un punto y coma después de la llave derecha. Pero una llave derecha seguida
por un carácter de nueva línea implica un separador de sentencia, así que no necesitas un
punto y coma ahí y por lo tanto, es generalmente omitido.
if $número % 2 == 0 {
say 'La variable $número es par'
} else {
say 'La variable $número es impar'
}
Si el residuo del $número dividido por 2 es 0, entonces sabemos que el $número es par, y el
programa muestra el mensaje apropiado. Si la condición es falsa, el segundo conjunto de
sentencias se ejecuta. Dado que la condición debe ser verdadera o falsa, exactamente una
de las alternativas será ejecutada. Las alternativas son conocidas como ramas, porque son
ramas en el flujo de ejecución.
Nota que si $número divide a dos exactamente, se imprimirá este código:
El valor de la variable $número no es interpolado, porque hemos usado las comillas simples
con el propósito de imprimir el nombre de la variable y no su valor. Habríamos usado las
comillas dobles si hubiésemos querido imprimir el valor de la variable y no el nombre.
if $x < $y {
say 'La variable $x es menor que la variable $y'
} elsif $x > $y {
say 'La variable $x es mayor que la variable $y'
} else {
say 'Las variables $x y $y son iguales'
}
La palabra clave elsif es una abreviación de “else if“ que tiene la ventaja de prevenir los
bloques anidados. Otra vez, exactamente una de las ramas será ejecutada. No hay límite
con el número de sentencias elsif.
Si existe una claúsula else, tiene que ir al final. Sin embargo, no tiene que haber una:
if $selección eq 'a' {
draw_a()
} elsif $selección eq 'b' {
draw_b()
} elsif $selección eq 'c' {
draw_c()
}
if $x == $y {
say 'Las variables $x y $y son iguales'
} else {
if $x < $y {
say 'La variable $x es menor que la variable $y'
} else {
say 'La variable $x es mayor que la variable $y'
}
}
La condicional exterior contiene dos ramas. La primera rama contiene una sentencia sim-
ple. La segunda rama contiene otra sentencia if, la cual contiene dos ramas. Estas dos
ramas son sentencias simples, aunque pudieran haber sido sentencias condicionales. Se
dice que la condición if $x < $y está anidada dentro de la rama else de la condicional
exterior.
Tales condicionales anidadas muestran lo crítico que es para tu propia comprensión in-
dentar apropiadamente las sentencias condicionales, debido a que sería difícil entender la
estructura sin la ayuda visual proveída por la correcta indentación.
Aunque la indentación de las sentencias ayuda a entender la estructura, las la lectura de
las condicionales anidadas se complica rápidamente. Es una buena idea evitarlas cuando
sea posible. Los operadores lógicos usualmente proveen una manera de simplificar las
condicionales anidadas. Por ejemplo, considera el siguiente código (el cual asume que $x
es un entero):
my Int $x;
# ... $x = ...;
if 0 < $x {
if $x < 10 {
say 'El valor de $x es un número positivo de un solo dígito.'
}
}
La sentencia say se ejecuta solo si pasamos ambas condicionales, así que podemos obtener
el mismo efecto con el operador Booleano and, y el código puede escribirse usando una
sola condicional:
Para este tipo de condición, Perl 6 provee una opción más concisa usando los operadores
de relación encadenados discutidos anteriormente:
if 0 < $x < 10 {
say '$x es un número positivo de un solo dígito.'
}
4.8. Condicionales if como Modificadores de Sentencias 63
Esto es equivalente a:
if $número < 0 {
say '$número es negativo.'
}
Esta forma sintáctica es más concisa debido a que toma una sola línea de código en vez de
tres. La ventaja de esto es que puedes ver más de tu código en una sola pantalla, sin tener
que desplazarte hacia arriba o hacia abajo. Sin embargo, esta sintaxis es clara y limpia solo
cuando la condición y la sentencia son cortas y simples. Por lo tanto, es mejor usarlas en
estos casos.
Esta palabra clave unless tiene el mismo significado que la palabra en inglés: mostrará la
sentencia “$número es negativo.“ a menos que (unless) el número sea mayor o igual a 0.
No puedes usar las sentencias else o elsif con unless, porque eso se volvería confuso.
my $producto = 1 * 2 * 3 * 4 * 5;
say $producto; # imprime 120
El problema es que esta construcción sintáctica no escala muy bien y se vuelve tediosa para
el producto de los primeros diez enteros (o factorial de 10). Y se vuelve una pesadilla para
el factorial de 100. La computación del factorial de un número es algo común en matemáti-
cas (especialmente en los campos de la combinatoria y probabilidad) y la ciencia de la
computación. Necesitamos automatizarlo, y el uso del bucle for es una de las maneras
más obvia de hacerlo:
my $producto = 1;
for 1..5 {
$producto *= $_
}
say $producto; # imprime 120
for 1..5 { .say }; # imprime los números del 1 al 5, cada uno en su línea
4.11. Recursión 65
Aquí .say es un atajo de sintaxis equivalente a $_.say. Y debido a que, como vimos, $_
toma cada valor sucesivo del rango introducido por la palabra clave for, este atajo imprime
cada número entre 1 y 5, cada uno de ellos en una línea distinta. Esto es un ejemplo típico
del uso de la variable tópica $_ sin ser explícitamente mencionada. Más adelante veremos
otros usos de la variable especial $_.
Algunas veces, no necesitas la variable $_ dentro de un bucle, por ejemplo si quieres hacer
algo cinco veces pero no te importa a cuál iteración del bucle has llegado. Una subrutina
que imprime un mensaje un número n de veces podría lucir de la siguiente manera:
El bucle for tiene una forma de modificador de sentencia ( o forma sufijo), usada aquí para
calcular otra vez el factorial de 5:
my $producto = 1;
$producto *= $_ for 1..5;
say $producto; # imprime 120
Existe otra forma de sintaxis del bucle for, usando una variable de bucle explícita:
El bucle for en esta subrutina usa lo que se llama una sintaxis de “bloque puntiagudo“.
Es esencialmente la misma idea del bucle for anterior, excepto que, en vez de usar la
variable tópica $_, ahora declaramos una variable de bucle explícita $x con la sintaxis
1..$num -> $x para iterar sobre el rango de valores. El uso de una variable de bucle ex-
plícita puede hacer tu código más claro cuando las cosas se complican, por ejemplo cuando
necesitas anidar varios bucles for. Más adelante veremos más ejemplos.
También veremos otras formas de calcular el factorial de un número en este libro.
4.11 Recursión
Es legal que una función o subrutina llame a otra; también es legal que una subrutina se
llame a sí misma. Podría no ser obvio porque esto es algo bueno, sin embargo resulta que
es una de las cosas más brillante y mágica que un programa puede hacer. Por ejemplo,
observa la siguiente subrutina:
say '¡Despegue!';
} else {
say $tiempo-restante;
cuenta-regresiva($tiempo-restante - 1);
}
}
cuenta-regresiva(3);
3
2
1
¡Despegue!
main
cuenta-regresiva $tiempo-restante 0
cuenta-regresiva $tiempo-restante 1
cuenta-regresiva $tiempo-restante 2
cuenta-regresiva $tiempo-restante 3
Para ejemplos simples como este, puede parecer fácil usar un bucle for. Pero más adelante
veremos ejemplos que son difíciles de escribir con un bucle for pero son fáciles de escribir
con recursión, por lo tanto es bueno empezar temprano.
Cada vez que una subrutina es llamada, Perl crea un marco para contener las variables
locales de la subrutina y los parámetros. Para una subrutina recursiva, podría haber más
de un marco en la pila al mismo tiempo.
Está vacío porque no creamos ninguna variable dentro del marco. Tampoco pasamos
ningún argumento al marco.
68 Capítulo 4. Bucles, Condicionales y Recursión
Esta es probablemente una de las maneras más común de obtener entrada interactiva de
texto desde el usuario, porque es una buena idea dejarle saber lo que se espera.
Otra posibilidad es usar el método get (el cual lee una sola línea) en la entrada estándar:
o la función get, la cual lee una línea de la entrada estándar por defecto:
La manera más fácil de extraer argumentos que han sido pasados al programa es con el uso
de la subrutina especial MAIN. Un programa que tiene una subrutina MAIN definida llamará
automáticamente a dicha subrutina, y los argumentos de la línea de comando suministra-
dos al programa serán pasados como argumentos a MAIN. La signatura MAIN te permitará
extraer los argumentos suministrados en la línea de comando y posiblemente chequear la
validez de los argumentos.
Puedes llamar este programa dos veces con diferentes argumentos en la línea de comando
de tal forma:
Es muy fácil cambiar el argumento, debido a que todo lo que necesitas hacer en la línea
de comando del sistema operativo es usar la flecha y editar el final de la línea de comando
anterior.
$ perl6 saludar.pl6
Usage:
saludar.pl6 <nombre>
• ¿Dónde ocurrió?
70 Capítulo 4. Bucles, Condicionales y Recursión
Los errores sintácticos son usualmente fáciles de encontrar, sin embargos existen algunos
trucos. En general, los mensajes de error indican donde se descubrió el problema, aunque
el actual error podría encontrarse un poco antes, algunas veces en la línea previa o hasta
varias líneas antes.
Por ejemplo, el objetivo del siguiente código era mostrar la tabla de multiplicación:
# ADVERTENCIA: código con fallas
sub tabla-multiplicación {
for 1..10 -> $x {
for 1..10 -> $y {
say "$x x $y\t= ", $x * $y;
say "";
}
}
tabla-multiplicación();
Pero falló al tiempo de compilación con el siguiente error:
$ perl6 tabla_mult.pl6
===SORRY!=== Error while compiling /home/Laurent/tabla_mult.pl6
Missing block (taken by some undeclared routine?)
at /home/Laurent/tabla_mult.pl6:9
------> tabla-multiplicación();<HERE><EOL>
El mensaje de error reporta un error en la línea 9 del programa (la última línea del código),
al final de la línea, pero el error actual es que falta una llave izquierda después de la línea 4
y antes de la línea 5. La razón de esto es que, mientras el programador cometió el error en
la línea 4, el interpretador de Perl no pudo detectar este error antes de alcanzar el final del
programa. El programa correcto para mostrar la tabla de multiplicación podría ser:
sub tabla-multiplicación {
for 1..10 -> $x {
for 1..10 -> $y {
say "$x x $y\t= ", $x * $y;
}
say "";
}
}
tabla-multiplicación();
Cuando un error es reportado en la última línea de un programa, es comúnmente causado
por la falta de un paréntesis, un corchete, una llave, o una comilla varias líneas más arriba.
Un editor con coloración de sintaxis puede algunas veces ayudarte a resolver este error.
Lo mismo sucede con errores al tiempo de ejecución. Considera este programa cuyo obje-
tivo es calcular 360 grados divididos sucesivamente por los enteros entre 2 y 5:
# ADVERTENCIA: código con fallas
my ($a, $b, $c, $d) = 2, 3, 5;
my $valor = 360;
$valor /= $_ for $a, $b, $c, $d;
say $valor;
4.17. Glosario 71
Este programa compila correctamente pero muestra una advertencia y después una excep-
ción al tiempo de ejecución:
El mensaje de error indica una excepción “division by zero” en la línea 4, pero no hay
nada incorrecto con esa línea. La advertencia en la línea 3 podría darnos una pista sobre el
intento del script de usar un valor indefinido, pero el error real se encuentra en la primera
línea del script, donde uno de los cuatros enteros necesarios fue omitido por equivocación
en la asignación de la lista.
Deberías prestarle atención a los mensajes de error y leerlos cuidadosamente. Sin embargo,
no asumas que ellos apuntan a la causa de la excepción; usualmente ellos apuntan a prob-
lemas subsecuentes.
4.17 Glosario
División de enteros Una operación, denotada por div, que divide dos números y re-
dondea el resultado.
Operador de módulo Un operador, denotado por un signo de porcentaje (%), que funciona
con números enteros y devuelve el residuo al dividir un número por otro.
Operador de relación Uno de los operadores que comparan sus operandos. Los oper-
adores de relación numérica más comunes son ==, !=, >, <, >=, y <=. Los operadores
equivalentes de relación de cadenas de texto son eq, ne, gt, lt, ge, and le.
Operador lógico Uno de los operadores que combinan expresiones Booleanas: and, or, y
not. Los operadores equivalentes de precedencia superior son &&, ||, y !
Condición La expresión booleana en una sentencia condicional que determina cual de las
ramas se ejecuta.
Condicional encadenada Una sentencia condicional con una serie de ramas alternativas.
Condicional anidada Una sentencia condicional que aparece en una de las ramas de otra
sentencia condicional.
Modificador de sentencia Una expresión condicional sufija, i.e., una expresión condi-
cional (usando por ejemplo if, unless o for) que es colocada después de la sentencia
cuya ejecución controla. También puede referirse a una expresión de bucle sufijo.
Sentencia de retorno Una sentencia que forza a una función a terminar de inmediato y
devolver a la función que realiza la llamada.
72 Capítulo 4. Bucles, Condicionales y Recursión
4.18 Ejercicios
Ejercicio 4.1. Usando los operadores división de enteros y módulo:
1. Escribe una subrutina que calcule cuantos días, horas, minutos y segundos hay en el número
de segundos pasado como argumento a la subrutina.
2. Escribe un script que calcule cuantos días, horas, minutos y segundos hay en 240, 000 segun-
dos.
3. Cambia el script para calcular el número de días, horas, minutos y segundos que hay en un
número de segundos proveído por el usuario cuando se le pide proveer un número de segundos.
an + bn = cn
para todos los valores de n mayores que 2.
1. Escribe una función con el nombre chequear-fermat que toma cuatro parámetros—a, b, c,
y n—y chequea si se cumple el teorema de Fermat. Si n es mayor que 2 y
an + bn = cn
el programa debería imprimir, “¡Santo dios, Fermat estaba equivocado¡‘ Por lo contrario, el
programa debería imprimir “No, eso no funciona.“
2. Escribe una subrutina que incita al usuario a entrar valores para las variables a, b, c, y n,
los convierte a enteros, y usa la función chequear-fermat para chequear si ellos violan el
teorema de Fermat.
Solución: A.2.3
Ejercicio 4.3. Si se te provee con tres palillos, podrías o no podrías ser capaz de organizarlos en
un triángulo. Por ejemplo, si uno de los palillos tiene una longitud de 12 pulgadas y los restantes
tienen 1 pulgada, no podrás conseguir que los palillos pequeños se encuentren en el medio. Para tres
longitudes cualquiera, existe una simple prueba para ver si es posible crear un triángulo:
Si una de las tres longitudes es mayor que la suma de las otras dos, entonces no
puedes formar una triángulo. De lo contrario, puedes formar un triángulo. (Si la
suma de dos lados es igual al tercero, ellos forman lo que se conoce como un triángulo
"degenerado".)
4.18. Ejercicios 73
1. Escribe una función llamada es-triángulo que toma tres números positivos como argumen-
tos, e imprime “Sí“ o “No,“ dependiendo si puedes formar un triángulo con las longitudes
dadas.
2. Escribe una función que incite al usuario a entrar tres longitudes y use la función
es-triángulo para chequear si palillos con las longitudes dadas pueden formar un trián-
gulo.
Solución: A.2.4
Ejercicio 4.4. Los números Fibonacci fueron inventados por Leonardo Fibonacci (también conocido
como Leonardo de Pisa o simplemente, Fibonacci), un matemático italiano del siglo XIII.
Los números Fibonacci son una secuencia de números como tal:
2. Escribe un programa que incite al usuario a entrar un número n y, usando el bucle for,
calcule y muestre el número Fibonacci nth .
Solución: A.2.5
Ejercicio 4.5. ¿Cuál es la salida del siguiente programa? Dibuja un diagrama de pila que muestre
el estado del programa cuando imprime el resultado.
Solución: A.2.6
74 Capítulo 4. Bucles, Condicionales y Recursión
5
Subrutinas Fructuosas
La mayoría de funciones de Perl que hemos usado, tales como la funciones matemáticas,
producen valores de retorno. Aún así, la mayoría de funciones que hemos escrito hasta
ahora son funciones void: ellas tienen un efecto, como imprimir un valor, pero no tienen
un valor de retorno. En este capítulo, aprenderás a escribir funciones fructuosas.
my $pi = 4 * atan 1;
my $altura = $radio * sin $radianes;
Muchas de las subrutinas que hemos escrito hasta ahora son void. Hablando casualmente,
ellas no tienen un valor de retorno útil; más precisamente, su valor de retorno puede ser
Any, Nil, (), o True.
En este capítulo, finalmente escribiremos subrutinas fructuosas. El primer ejemplo es area,
la cual devuelve el área de un círculo con un radio en particular:
Hemos visto la sentencia return anteriormente, pero en una subrutina fructuosa la senten-
cia return incluye una expresión. Esta sentencia quiere decir: “ Devuelve inmediatamente
desde esta función y usa la siguiente expresión como un valor de retorno.“ La expresión
puede ser arbitrariamente complicada, así que podríamos escribir esta función en una man-
era más concisa:
76 Capítulo 5. Subrutinas Fructuosas
Por otro lado, las variables temporales como area_circular hace la depuración mucho
más fácil. Además ayudan a documentar lo que está sucediendo en la función.
Algunas veces es útil tener varias sentencias return, por ejemplo una en cada rama de una
condicional:
Dado que estas sentencias return están en una condicional alternativa, solo una se ejecuta.
Esto podría también escribirse de forma concisa usando la sintaxis del modificador de sen-
tencia:
Otra vez, solo una de las sentencias return se ejecuta: si el número es negativo, la primera
sentencia return es ejecutada y la ejecución de la subrutina termina ahí; en cambio, si el
número es positivo o cero, solo la segunda sentencia return es ejecutada.
Tan pronto una sentencia return es ejecutada, la función termina sin ejecutar las sentencias
subsecuentes. Cualquier código que aparece después de una sentencia return incondi-
cional, o cualquier otro lugar que el flujo de ejecución no puede alcanzar es conocido como
código muerto.
En una función fructuosa, es una buena idea asegurar que cada posible ruta a través del
programa se alcance con una sentencia return. Por ejemplo:
> valor_absoluto(0)
()
Casualmente, Perl provee una función integrada llamada abs que calcula valores absolu-
tos.
Como ejercicio, escribe una subrutina comparar que toma dos números, $x y $y, y devuelve
1 si $x > $y, 0 if $x == $y, y -1 if $x < $y.
Solución: A.3.1
q
distancia = ( x2 − x1 )2 + ( y2 − y1 )2
El primer paso es considerar cómo la función distancia debería lucir en Perl. En otras
palabra, cuáles son las entradas (parámetros) y cuál es la salida (valor de retorno)?
En este caso, las entradas son los puntos, los cuales puedes representar usando cuatro
números. El valor de retorno es la distancia representada por un valor numérico.
Inmediatamente puedes escribir un bosquejo de la función:
Obviamente, esta versión no calcula la distancia; siempre devuelve cero. Sin embargo, es
sintácticamente correcta, y puede ser ejecutada, lo que significa que la probaste antes de
hacerla más compleja.
Para probar la nueva función, llámala con algunos argumentos:
Elegí estos valores para que la distancia horizontal sea 3 y la distancia vertical sea 4; de esa
manera, el resultado es 5, la hipotenusa de un triángulo 3-4-5. Al probar una función, es
siempre útil saber la respuesta correcta con antelación.
En este momento, hemos confirmado que la función es sintácticamente correcta, y podemos
comenzar a añadir código al cuerpo de la función. El paso siguiente razonable es encontrar
las diferencias x2 − x1 and y2 − y1 . La siguiente versión almacena esos valores en variables
temporales y los imprime:
78 Capítulo 5. Subrutinas Fructuosas
Si la subrutina funciona como debe, debería mostrar $dx es 3 y $dy es 4 (y todavía de-
volver 0.0). Si esto sucede, sabemos que la función está recibiendo los argumentos correctos
y realizando la primera computación correctamente. Por lo contrario, si esto no sucede solo
tenemos que depurar algunas líneas.
El siguiente paso es computar la suma de los cuadrados de $dx y $dy:
Otra vez, ejecutarías el programa en esta etapa y verificarías la salida (que debería ser
25). Finalmente, puedes usar la función integrada sqrt para calcular la raíz cuadrada y
devolver el resultado:
2. Usa variables para almacenar los valores intermedios para que puedas mostrarlos y
chequearlos.
3. Una vez que el programa funciona, podrías remover algo del código de andamiaje o
consolidar sentencias múltiples en una expresión compuesta, pero solo si no dificulta
la lectura del programa.
Nota que, por lo menos para casos relativamente simples, puedes usar el REPL para probar
expresiones y hasta sentencias multilíneas o subrutinas en el modo interactivo antes de
incluirlas en tu código. Esto es usualmente rápido y puede ahorrarte algo de tiempo.
Como un ejercicio, usa el desarrollo incremental para escribir una función llamada
hipotenusa que devuelve la longitud de la hipotenusa de un triángulo rectángulo, dadas
las longitudes de los otros dos lados como argumentos. Registra cada etapa del desarrollo
a medida que avanzas.
Solución: A.3.2.
5.3 Composición
Como deberías saber por ahora, puedes llamar una función desde otra. Como un ejem-
plo, escribiremos una función que toma dos puntos, el centro del círculo y un punto en el
perímetro, y calcula el área del círculo.
Asume que el centro es almacenado en las variables $x-c y $y-c, y el perímetro está en
$x-p y $y-p. El primer paso es encontrar el radio del círculo, el cual es la distancia entre
los dos puntos. Anteriormente escribimos una función, distancia, que hace exactamente
eso:
El siguiente paso es encontrar el área de un círculo con ese radio; también ya escribimos
algo para hacer eso mismo:
my $resultado = area($radio);
Las variables temporales $radio y $resultado son útiles para el desarrollo y la depu-
ración, pero una vez que el programa ya funciona adecuadamente, podemos hacer todo
más conciso al componer las llamadas de las funciones en la siguiente manera:
La última línea del ejemplo anterior ahora funciona como una tubería (del inglés pipeline)
de datos desde la derecha hacia la izquierda; la función distancia toma los cuatros argu-
mentos y devuelve una distancia (el radio) el cual se pasa como un argumento a area; con
este argumento, area es ahora capaz de devolver el área, la cual es entonces devuelta por
area-círculo al código que hace la llamada. Regresaremos más tarde a este modelo de
tubería de datos.
Es común darle a las funciones Booleanas nombres que suenan como preguntas con re-
spuestas de sí/no; es-divisible, por ejemplo, devuelve True o False para indicar si x es
divisible por y.
Este es un ejemplo:
El resultado del operador == es un valor Booleano, así que podemos escribir la subrutina
de manera concisa al devolverlo directamente:
Si no hay una sentencia de retorno, una subrutina en Perl devuelve el valor de la expre-
sión en la última línea de código de la subrutina (provisto que la última expresión sea
una expresión que puede ser evaluada), así que la sentencia return no es requerida aquí.
Además, dado que 0 es una valor falso y cualquier otro entero es un valor verdadero, la
misma subrutina podría escribirse de otra forma:
Las declaraciones de tipo Int en las signaturas de la subrutina más arriba no son necesarias.
La subrutina funcionaría sin ellas, pero ellas pueden proveer algún tipo de protección en
contra del uso de esta subrutina con argumentos defectuosos.
Las funciones Booleanas se usan usualmente en los modificadores de sentencias:
5.5. Un Lenguaje de Programación Completo 81
> 9 %% 3
True
> 9 %% 4
False
La prueba de esa afirmación no es un ejercicio trivial. El primero en probarla fue Alan Tur-
ing, uno de los primeros científicos de la computación (algunas personas argumentan que
él fue un matemático, sin embargo muchos de los primeros científicos de la computación
comenzaron como matemáticos). Por esta razón, se le conoce como la prueba de Turing
(Turing Test). Para una discusión más completa (y precisa) de la prueba de Turing, re-
comiendo el libro Introduction to the Theory of Computation de Michael Sipser.
Si ves esa definición en el diccionario, podrías enojarte. Sin embargo, si busca la definición
de la función factorial, denotada con el símbolo !, podrías conseguir algo como esto:
0! = 1
n! = n(n − 1)!
Esta definición dice que el factorial de 0 es 1, y que el factorial de cualquier otro valor
(entero positivo), n, es n multiplicado por el factorial de n − 1.
Así que 3! es 3 por 2!, el cual es 2 por 1!, el cual es 1 por 0!. Si lo ponemos todo junto, 3! es
igual a 3 por 2 por 1 por 1, lo cual es 6.
Si puedes escribir una definición recursiva de algo, puedes escribir un programa de Perl
para evaluar dicha definición. El primer paso es decidir cuáles deberían ser los parámetros.
En este caso, es claro que el factorial toma un número1 :
sub factorial( $n ) {
}
sub factorial( $n ) {
if $n == 0 {
return 1;
}
}
Por el otro lado, y ésta es la parte interesante, tenemos que hacer una llamada recursiva
para encontrar el factorial de n − 1 y después multiplicarlo por n:
1 Debería ser realmente un entero, pero regresaremos a esto más tarde en este capítulo.
5.6. Más Recursión 83
sub factorial( $n ) {
if $n == 0 {
return 1;
} else {
my $recurse = factorial($n-1);
my $result = $n * $recurse;
return $result;
}
}
sub factorial( $n ) {
return 1 if $n == 0;
return $n * factorial $n-1;
}
Esto no es nada mejor que nuestra primera versión, y probablemente no se ejecutará sig-
nificativamente más rápido, pero es presumiblemente más clara, por lo menos cuando te
acostumbra a este tipo de sintaxis.
2 Luego veremos más formas idiomáticas de computar el factorial de un número.
84 Capítulo 5. Subrutinas Fructuosas
main
6
factorial n 3 recurse 2 result 6
2
factorial n 2 recurse 1 result 2
1
factorial n 1 recurse 1 result 1
1
factorial n 0
5.7 Acto de Fe
Seguir el flujo de ejecución es una manera de leer un programa, pero se vuelve agobiante
en poco tiempo. Una alternativa es lo que puede ser llamado el “acto de fe“. Cuando en-
cuentras una subrutina, en lugar de seguir el flujo de ejecución, tú asumes que la subrutina
funciona correctamente y devuelve el resultado correcto.
De hecho, ya estás practicando este acto de fe cuando usas las funciones integradas.
Cuando llamas las funciones matemáticas tales como cos o sqrt, no examinas los cuer-
pos de esas funciones. Asumes que funcionan adecuadamente porque las personas que
escribieron las funciones integradas eran probablemente buenos programadores (y porque
puedes asumir seguramente que dichas funciones han sido probadas extensivamente).
Lo mismo es cierto cuando llamas una de tus subrutinas. Por ejemplo, en la Sección 5.4,
escribimos una subrutina llamada es-divisible que determina si un número es divisible
por otro. Una vez que nos convencemos que esta subrutina es correcta—al examinar y
probar el código—podemos usar la subrutina sin examinar el cuerpo de la misma otra vez.
org/wiki/Sucesión_de_Fibonacci):
fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n − 1) + fibonacci(n − 2)
En lenguaje llano, una sucesión de Fibonacci es una secuencia de números tales como:
donde los dos primeros términos son iguales a 1 y cualquier otro término es la suma de los
dos números que lo preceden.
Discutimos la secuencia de Fibonacci brevemente en el Ejercicio 4.4 del capítulo anterior y
la implementamos con un bucle for.
Ahora traduzcamos la definición recursiva en Perl. Así es como luce:
sub fibonacci( $n ) {
return 1 if $n == 0 or $n == 1;
return fibonacci($n-1) + fibonacci($n-2)
}
Si tratas de seguir el flujo de ejecución aquí, aún para valores pequeños de $n, tu cabeza va
a explotar. Sin embargo, de acuerdo al acto de fe, si asumimos que las llamadas recursivas
funcionan correctamente, entonces es claro que obtendrás el resultado correcto al añadirlo
todo junto.
El tipo Int en la signatura chequea por números no enteros, lo cual ya lo sabemos. La parte
where $n >= 0 es una restricción de parámetro: si el parámetro es negativo, la subrutina
debería fallar. Técnicamente, la restricción se implementa aquí dentro de la signatura us-
ando una característica sintáctica llamada un rasgo (trait en inglés), que es una propiedad
impuesta sobre el parámetro al tiempo de compilación. Si el argumento que se pasa a la
función no es un entero o si es negativo, el programa imprime un mensaje de error para
indicar que algo malo pasó:
> say factorial 1.5
Type check failed in binding $n; expected Int but got Rat
in sub factorial at <unknown file> line 1
in block <unit> at <unknown file> line 1
my Par-entero $x = 2; # OK
my Par-entero $y = 3; # Error de discordancia de tipo
De igual manera, en el caso de la subrutina factorial, podemos crear un subconjunto de
enteros no negativos y usarlos para chequear el parámetro que se pasa a la subrutina:
subset Entero-no-neg of Int where { $_ >= 0 }
# ...
5.10 Sobrecargas
Es posible escribir versiones múltiples de una subrutina con el mismo nombre pero con sig-
naturas diferentes, por ejemplo una aridad diferente (una palabra sofisticada para referirse
al número de argumentos) o diferentes tipos del argumentos, usando la palabra clave
multi. En este caso, el interpretador elegirá la versión de la subrutina cuya signatura mejor
coincide con la lista de argumentos.
Por ejemplo, podríamos escribir de nuevo la función factorial como sigue:
Aquí, no entramos en una recursión infinita porque, cuando el parámetro que se pasa a
fact es 0, es la primera de la versión de la subrutina que se llama y devuelve un valor
entero (1), y esto termina la recursión.
Similarmente, la función Fibonacci puede escribirse de nuevo con sobrecargas:
multi fibonacci( 0 ) { 1 }
multi fibonacci( 1 ) { 1 }
multi fibonacci( Int $n where $n > 1 ) {
fibonacci($n - 2) + fibonacci($n - 1)
}
say fibonacci 10; # -> 89
Muchas de las funciones integradas y muchos de los operadores de Perl 6 son escritos como
sobrecargas.
• Hay algo incorrecto con los argumentos que la subrutina recibe; una precondición es
violada.
• Hay algo incorrecto con la subrutina; una poscondición es violada.
• Hay algo incorrecto con el valor de retorno o la forma en la cual es usado.
Para descartar la primera posibilidad, puedes agregar una sentencia de impresión al inicio
de la función y mostrar los valores de los parámetros (y tal vez sus tipos). O puedes escribir
código para chequear las precondiciones explícitamente.
Para el propósito de depuración, es usualmente útil imprimir el contenido de una vari-
able o de un parámetro dentro de una cadena de texto con caracteres alrededor, para así
88 Capítulo 5. Subrutinas Fructuosas
visualizar caracteres que de otra manera son invisibles, tales como espacios o caracteres
de nueva línea. Por ejemplo, piensas que la $var debería contener “two“ y ejecutar la
siguiente lo prueba:
if $var eq "two" {
do-something()
}
say "[$var]";
if $var eq "two" {
do-something()
}
[two ]
o:
[two
]
Ahora sabes que la prueba de igualdad falla porque $var contiene un carácter colgante
(espacio o nueva línea) que podría ser difícil de detectar.
Si los parámetros están bien, agrega una sentencia print antes de cada sentencia return y
muestra el valor de retorno. Si es posible, chequea el resultado manualmente. Considera
llamar la función con valores que facilitan chequear el resultado (como en la Sección 5.2).
Si la función aparenta funcionar, observa la llamada de la función para asegurarte que el
valor de retorno está siendo utilizado correctamente (o apenas siendo utilizado).
Agregar sentencias de impresión al comienzo y final de una función ayuda a hacer al flujo
de ejecución más visible. Por ejemplo, la siguiente es una versión de factorial con sen-
tencias de impresión:
factorial 4
factorial 3
factorial 2
factorial 1
factorial 0
devolviendo 1
devolviendo 1
devolviendo 2
devolviendo 6
devolviendo 24
Si estás confundido sobre el flujo de ejecución, este tipo de salida puede ser útil. Toma
tiempo desarrollar andamiaje efectivo, pero un poco de andamiaje puede ahorrar mucho
tiempo de depuración.
5.12 Glosario
Variable temporal Una variable usada para almacenar un valor intermedio en una op-
eración compleja.
Código muerto Parte de un programa que nunca se ejecuta, usualmente porque aparece
después de una sentencia return.
Desarrollo incremental Un plan de desarrollo de programa destinado a evitar la depu-
ración al añadir y probar solo piezas pequeñas de código a la vez.
Andamiaje Código usado durante el desarrollo de un programa pero que no es parte de
la versión final.
Guardián Un patrón de programación que usa una sentencia condicional para chequear y
manejar circunstancias que podrían causar un error.
5.13 Ejercicios
Ejercicio 5.1. Dibuja un diagrama de pila para el siguiente programa. ¿Qué imprime el programa?
Por favor trata de contestar estas preguntas antes de ejecutar el programa.
sub b( Int $z ) {
my $prod = a($z, $z);
say $z, " ", $prod;
return $prod;
}
sub a( Int $x is copy, Int $y ) {
$x++;
return $x * $y;
}
sub c( Int $x, Int $y, Int $z ) {
90 Capítulo 5. Subrutinas Fructuosas
my $total = $x + $y + $z;
my $square = b($total) ** 2;
return $square;
}
my $x = 1;
my $y = $x + 1;
say c($x, $y + 3, $x + $y);
Ejercicio 5.2. La función de Ackermann, A(m, n), se define como sigue:
n + 1
if m = 0
A(m, n) = A(m − 1, 1) if m > 0 and n = 0
A(m − 1, A(m, n − 1)) if m > 0 and n > 0.
Ver https: // es. wikipedia. org/ wiki/ Funci% C3% B3n_ de_ Ackermann . Escribe una sub-
rutina llamada ack que evalúa la función de Ackermann. Usa tu subrutina para evaluar ack(3,
4), que debería ser 125. ¿Qué pasa con valores más grandes de m y n?
Solución: A.3.4.
Ejercicio 5.3. Un palíndromo es una palabra que que se lee igual adelante que atrás, como “Ana“
y “radar“. Recursivamente, una palabra es un palíndromo si las letras inicial y final son las mismas
y el medio es un palíndromo.
La siguientes son subrutinas que toman un argumento de cadena de texto y devuelve las letras
primera, última y del centro:
Por el momento, no es necesario que sepas cómo funcionan; veremos esto en el Capítulo 7 sobre
cadenas de texto. Por ahora:
Solución: A.3.5.
Ejercicio 5.4. Un número entero a, es una potencia de b si es divisible por b y a/b es una potencia
de b. Escribe una función llamada es-potencia-de que toma los parámetros a y b y devuelve
True si a es una potencia de b. Nota: tendrás que pensar sobre el caso base.
Solución: A.3.6
Ejercicio 5.5. El máximo común divisor (MCD) de a y b es el mayor número entero que los divide
sin dejar un residuo.
Una manera de encontrar el MCD de dos números está basada en la observación de que si r es el
residuo cuando a es dividido por b, entonces mcd( a, b) = mcd(b, r ). Como un caso base, podemos
usar mcd( a, 0) = a.
Escribe una función llamada mcd que toma los parámetros a y b y devuelve su máximo común
divisor.
Crédito: este ejercicio está basado en un ejemplo de Structure and Interpretation of Computer
Programs de Abelson y Sussman.
Solución: A.3.7
92 Capítulo 5. Subrutinas Fructuosas
6
Iteración
> my $a = 5;
5
> my $b = $a; # $a y $b son iguales ahora
5
> $a = 3; # $a y $b ya no son iguales
3
> say $b;
5
94 Capítulo 6. Iteración
5
$x
7
Fig. 6.1: Diagrama de estado.
La tercera línea cambia el valor de $a pero no cambia el valor de $b, así que ya no son
iguales.
En resumen, recuerda que = es un operador de asignación y no un operador de igualdad;
los operadores para probar igualdad entre dos términos son == para números y eq para
cadenas de texto.
6.2 Reasignación
Como tal vez descubriste, es legal hacer más de una asignación a la misma variable. Una
asignación nueva hace que una variable existente haga referencia a un nuevo valor (y deje
de referirse al valor antiguo):
> my $x = 5;
5
> say $x;
5
> $x = 7;
7
> say $x
7
> $x = $x + 1;
Esto significa “obtén el valor actual de $x, agrégale uno, y después actualiza a $x con el
valor nuevo.“
6.4. La Sentencia while 95
Si intentas actualizar una variable a la cual no se le ha dado un valor, obtienes una adver-
tencia, porque Perl evalúa el lado derecho de la sentencia de asignación antes de asignar
un valor a $x:
> my $x;
> $x = $x + 1;
Use of uninitialized value of type Any in numeric context
in block <unit> at <unknown file> line 1
Antes de actualizar una variable, tienes que declararla e inicializarla, usualmente con una
sentencia de asignación:
> my $x = 0;
> $x = $x + 1;
Como se mencionó anteriormente en la Sección 2.3, Perl tiene algunos atajos para el incre-
mento y el decremento:
$x += 1; # equivalente a $x = $x + 1
$x++; # también equivalente
$x -= 1; # equivalente a $x = $x - 1
$x--; # también equivalente
Otra es la sentencia while. Esta es una versión de cuenta-regresiva que usa la sentencia
while:
Puedes casi leer la sentencia while como si fuera inglés. Significa, “Mientras (while) $n sea
mayor que 0, muestra el valor de $n y después reduce $n. Cuando llegues a 0, muestra la
palabra ¡Despegue!“
Este tipo de flujo se llama un bucle (loop en inglés) porque el tercer paso regresa nueva-
mente al principio.
El cuerpo del bucle debería cambiar el valor de una o más variables para que la condición
se vuelva falsa eventualmente y el bucle termine. De lo contrario, el bucle se repetirá por
siempre, lo cual se conoce como un bucle infinito. Una fuente de entretenimiento para
los científicos de la computación es la observación de que las instrucciones en la botella de
champú, “Enjabona, enjuage, repite“, son un bucle infinito.
Con otros bucles, no es tan fácil decir si el bucle termina. Por ejemplo:
Cada vez a través del bucle, el programa muestra el valor de $n y después chequea si es par
o impar. Si es par, $n es dividido por 2. Si es impar, el valor de $n es reemplazado con $n*3
+ 1. Por ejemplo, si el argumento que se pasa a secuencia es 42, los valores resultantes de
n son 42, 21, 64, 32, 16, 8, 4, 2, 1.
Dado que $n algunas veces incrementa y otras disminuye, no hay prueba obvia que $n
llegará a 1, o que el programa termina. Para algunos valores particulares de n, podemos
probar que el programa termina. Por ejemplo, si el valor inicial es una potencia de dos,
n será un par a través del bucle hasta que llega al 1. El ejemplo anterior termina con tal
secuencia de potencias de dos, comenzando con 64.
6.5. Variables Locales y Ámbito de una Variable 97
¡La pregunta difícil es si podemos probar que este programa termina para todos los valores
positivos de n. Hasta ahora, nadie ha sido capaz de probarlo o refutarlo! (Ver https:
//es.wikipedia.org/wiki/Conjetura_de_Collatz.)
Como un ejercicio, podrías escribir de nuevo la función imprimir-n-veces de la Sec-
ción 4.11 usando iteración en lugar de recursión.
La sentencia while puede también usarse como un modificador de sentencia (o sintaxis
sufija):
my $val = 5;
print "$val " while $val-- > 0; # imprime 4 3 2 1 0
print "\n";
La sentencia while del bucle ejecuta el bloque mientra la condición sea verdadera. También
existe un bucle until, el cual ejecuta el bloque mientra su condición se falsa:
my $val = 1;
until $val > 5 {
print $val++; # imprime 12345
}
print "\n";
if $condición eq True {
my $foo = "bar";
say $foo; # imprime "bar"
}
say $foo; # ERROR: "Variable '$foo' is not declared ..."
# ERROR: "La variable '$foo' no está declarada ..."
98 Capítulo 6. Iteración
my $foo;
if $condición eq True {
$foo = "bar";
say $foo; # imprime "bar"
} else {
$foo = "baz";
}
say $foo; # imprime "bar" o "baz" dependiendo de $condición
Si una variable lexical no se declara dentro dentro de un bloque, su ámbito se extiende hasta
al final del archivo (esto se llama algunas veces una variable estática o global, aunque estos
términos no son del todo precisos). Por ejemplo, en el último fragmento de código, el ám-
bito de la variable $foo se extenderá hasta al final del archivo, lo cual puede o no puede ser
algo bueno, dependiendo de como pretendas usarla. Es usualmente mejor reducir el ám-
bito de las variables tanto como sea posible porque esto ayuda a reducir las dependencias
entre las varias partes del código y limita el riesgo de errores sutiles. En el código ante-
rior, si queremos limitar el ámbito de $foo, podemos agregar llaves para crear un bloque
delimitador con el único propósito de limitar el ámbito de dicha variable:
{
my $foo;
if $condición eq True {
$foo = "bar";
say $foo; # imprime "bar"
} else {
$foo = "baz";
}
say $foo; # imprime "bar" o "baz" dependiendo de $condición
}
Ahora las llaves exteriores crean un bloque delimitador que delimita el ámbito de $foo
solamente donde lo necesitamos. Esto puede parecer un ejemplo un poco forzado, pero es
común agregar llaves solo con el propósito de definir el ámbito de algo.
El ámbito lexical también significa que las variables con el mismo nombre pueden ser tem-
poralmente redefinidas en un nuevo ámbito:
my $posición = "fuera";
sub exterior {
say $posición;
}
sub interior {
my $posición = "dentro";
say $posición;
}
6.5. Variables Locales y Ámbito de una Variable 99
Tenemos en efecto dos variables con el mismo nombre, $posición, pero en diferentes ám-
bitos. Una es válida solo dentro de la subrutina interior donde ha sido redefinida, y la
otra en otro lugar.
Si agregamos una subrutina nueva:
sub en-ningún-lugar {
my $posición = "en ningún lugar";
exterior();
}
en-ningún-lugar(); # -> fuera
está aún imprimirá “fuera“ porque la subrutina exterior sabe sobre la versión “fuera“
de la variable $posición, la cual existía cuando exterior fue definida. En otras palabras,
el código exterior que aludió a la variable exterior (“fuera“) sabe sobre la variable que
existía cuando fue creado, pero no sobre la variable existente donde dicho código fue lla-
mado. Así es como las variables lexicales funcionan. Este comportamiento es la base
para construir clausuras, una forma de subrutina con algunas propiedades especiales que
estudiaremos más tarde en este libro, pero que en efecto está presente en todas partes en
Perl 6.
Mientras que tener variables diferentes con el mismo nombre puede darte mucho poder
expresivo, te aconsejamos que evite crear variables distintas con el mismo nombre y difer-
entes ámbitos, por lo menos hasta que realmente entiendas estos conceptos lo suficiente
para saber lo que estás haciendo, debido a que esto puede ser un poco complejo.
Mayormente, muchas de las variables usadas en Perl son variables lexicales, que se
declaran con el declarador my. Aunque no son declarados con my, los parámetros declara-
dos en la signatura de una subrutina y los parámetros de un bloque puntiagudo también
tienen un ámbito lexical limitado al cuerpo de la subrutina o el bloque de código.
Existen otros declaradores, tales como our, el cual crea una variable con ámbito de paquete,
y state, que crea una variable con ámbito lexical pero con un valor persistente. Raramente
son usados.
Un punto final: aunque usualmente no se declaran con un declarador my, las subrutinas
tienen también un ámbito lexical por defecto. Si se definen dentro de un bloque, serán
visibles solo dentro de ese bloque. Un ejemplo de esto se mostró al final de la solución al
ejercicio MCD del capítulo anterior (ver Subsección A.3.7). Con eso dicho, puedes declarar
una subrutina con un declarador my si así lo deseas:
my sub frobnicate {
# ...
}
Por ejemplo, supón que quieres tomar entrada del usuario hasta que entre terminar. Po-
drías escribir lo siguiente:
while True {
my $línea = prompt "Entra algo ('terminar' para salir)\n";
last if $línea eq "terminar";
say $línea;
}
say '¡Terminado!';
La condición de bucle es True, lo cual siempre es verdadero, así que el bucle se ejecuta
hasta que alcanza la sentencia last.
Cada vez, el bucle incita al usuario a teclear algo. Si el usuario escribe terminar, la sen-
tencia last terminar el bucle. Por lo contrario, el programa refleja cualquier cosa que el
usuario escribe y regresa a la parte superior del bucle. Esta es una muestra:
$ perl6 terminar.pl6
Entra algo ('terminar' para salir)
Hola
Hola
Entra algo ('terminar' para salir)
Estoy aprendiendo Perl6
Estoy aprendiendo Perl6
Entra algo ('terminar' para salir)
terminar
¡Terminado!
Esta manera de escribir bucles while es común porque puedes chequear la condición en
cualquier parte en el bucle (no solo en la parte superior) y puedes expresar la condición
para terminar afirmativamente (“termina cuando esto suceda“) en lugar de negativamente
(“mantente haciendo esto hasta qu e esto pase“).
Usar un bucle while con una condición que es siempre verdadera es una manera natural
de escribir un bucle infinito, i.e., un bucle que se ejecutará por siempre hasta que algo en el
código (tal como la sentencia last usada anteriormente) force al programa a salir del bucle.
Esto es comúnmente usado en lenguajes de programación, y funciona muy bien en Perl.
No obstante, existe otra forma común y más idiomática de construir un bucle infinito en
Perl 6: al usar la sentencia loop, la cual estudiaremos en la Sección 9.7 (p. 160). Por ahora,
usaremos la sentencia while True, la cual es bastante legítima.
Algunas veces, en lugar de salir del bucle while con la sentencia de control last, por ejem-
plo, necesitas comenzar el cuerpo del bucle al principio. Por ejemplo, quieres chequear si la
entrada del usuario es correcta con alguna subrutina es-válido (no especificada) antes de
procesar los datos, e incitar al usuario a intentar nuevamente si la entrada no era correcta.
En este caso, la sentencia de control next te deja comenzar al principio del bucle otra vez:
6.7. Raíces Cuadradas 101
while True {
my $línea = prompt "Entra algo ('terminar' para salir)\n";
last if $línea eq "terminar";
next unless es-válido($línea);
# procesamiento posterior de $línea;
}
print('¡Terminado!')
Aquí, el bucle termina si el usuario escribe “terminar“. Si no, la entrada del usuario se
chequea con la subrutina es-válido; si la subrutina devuelve un valor verdadero, el proce-
samiento continúa; si devuelve un valor falso, entonces el flujo de control comienza nue-
vamente al principio del cuerpo del bucle, y por lo tanto se incita al usuario a escribir una
entrada válida.
La sentencias de control last y next también funcionan con los bucles for. Por ejemplo,
el siguiente bucle for itera en teoría sobre un rango de números enteros entre 1 y 20, pero
desecha los números impares por virtud de una sentencia next y sale del bucle con una
sentencia next tan pronto la variable del bucle es mayor que $max (i.e., 10 en este ejemplo):
my $max = 10;
for 1..20 -> $i {
next unless $i %% 2; # mantiene solos los valores pares
last if $i > $max; # termina el bucle si $i es mayor que $max
say $i; # imprime 2 4 6 8 10
}
Puedes tener tantas sentencia last y next como desees, al igual que puedes tener tantas
sentencias return como quieras en una subrutina. El uso de tales sentencias de flujo de
control no es considerado mala práctica. Durante la infancia de la programación estruc-
turada, algunas personas insistían que los bucles y las subrutinas tienen solo una entrada y
una sola salida. La noción de una sola entrada es todavía una buena idea, pero la noción de
una sola salida ha causado que muchas personas hagan lo imposible y escriban muchísimo
código no tan natural. La mayor parte de la programación consiste en recorrer árboles de
decisiones. Un árbol de decisiones naturalmente comienza con un solo trunco pero ter-
mina con muchas hojas. Escribe tu código con el número de controles de bucle (y salida de
subrutinas) que es natural para el problema que estás intentando resolver. Si declaras tus
variables con ámbitos razonables, todo se limpia automáticamente al momento apropiado,
sin importar como dejes el bloque.
Por ejemplo, una manera de calcular las raíces cuadradas es con el método de Newton
(también conocido como el método de Newton-Raphson). Supón que quieres saber la raíz
cuadrada de a. Si comienzas con casi cualquier estimado, x, puedes calcular un mejor
estimado con la siguiente fórmula:
102 Capítulo 6. Iteración
x + a/x
y=
2
Por ejemplo, si a es 4 y x es 3:
> my $a = 4;
4
> my $x = 3;
3
> my $y = ($x + $a/$x)/2;
2.166667
√
El resultado está mucho más cerca de 3 que a la respuesta correcta ( 4 = 2). Si repites el
proceso con el estimado nuevo, se acerca aún más:
> $x = $y;
2.166667
> $y = ($x + $a/$x)/2;
2.006410
Después de varias actualizaciones, el estimado es casi exacto:
> $x = $y;
2.006410
> $y = ($x + $a/$x)/2;
2.000010
> $x = $y;
2.000010
> $y = ($x + $a/$x)/2;
2.000000000026
En general no sabemos con antelación cuántos pasos toma para obtener la respuesta cor-
recta, pero sabemos cuando la obtenemos porque el estimado para de cambiar:
> $x = $y;
2.000000000026
> $y = ($x + $a/$x)/2;
2
> $x = $y;
2
> $y = ($x + $a/$x)/2;
2
Cuando $y == $x, podemos parar. Este es un bucle que comienza con un estimado inicial,
x, y lo mejora hasta que para de cambiar:
my ($a, $x) = (4, 3);
while True {
say "-- Value Intermedio: $x";
my $y = ($x + $a/$x) / 2;
last if $y == $x;
$x = $y;
}
say "Final result is $x";
6.8. Algoritmos 103
Esto imprimirá:
-- Valor Intermedio: 3
-- Valor Intermedio: 2.166667
-- Valor Intermedio: 2.006410
-- Valor Intermedio: 2.000010
-- Valor Intermedio: 2.000000000026
-- Valor Intermedio: 2
Resultado final es 2
Para muchos valores de $a esto funciona bien, pero tenemos que considerar varias cosas.
Primero, en la mayoría de lenguajes de programación, es peligroso chequear la igualdad
de coma flotante (float), porque los valores de coma flotante son solo aproximadamente
correctos. No tenemos este problema con Perl 6, porque, como hemos mencionado, Perl 6
usa una mejor representación de los números racionales que la mayoría de los lenguajes
de programación generalistas. (Deberías tener esto presente si estás usando algún otro
lenguaje). Aún si no tenemos este problema en Perl 6, también pueden existir otros prob-
lemas con algoritmos que no se comportan tan bien como el algoritmo de Newton. Por
ejemplo, algunos algoritmos podrían no convergir tan rápida y perfectamente como el al-
goritmo de Newton pero en lugar producir valores alternativos por encima y por debajo
del resultado preciso.
En lugar de chequear si $x and $y son exactamente iguales, es más seguro usar la función
integrada abs para calcular el valor absoluto, o magnitud, de la diferencia entre ellos:
donde epsilon tiene un valor pequeñísimo como 0.0000001 que determina qué tan cerca
es suficientemente cerca.
6.8 Algoritmos
El método Newton es un ejemplo de un algoritmo, que es un proceso mecánico para re-
solver una categoría de problemas (en este caso, calcular raíces cuadradas).
Para entender lo que es un algoritmo, podría ser útil comenzar con algo que no es aún un
algoritmo. Cuando aprendiste a multiplicar los números de un solo dígito, probablemente
memorizaste la tabla de multiplicación. En efecto, memorizaste 100 soluciones específicas.
Ese tipo de conocimiento no es algorítmico.
Pero si eres “perezoso“, posiblemente aprendiste algunos trucos. Por ejemplo, para encon-
trar el producto de n y 9, puedes escribir n − 1 como el primer dígito y 10 − n como el
segundo dígito. Por lo tanto para calcular 9 ∗ 7, n − 1 es 6 y 10 − n es 3, así que el producto
9 ∗ 7 es 63. Es truco es una solución general para multiplicar un número de un solo dígito
por 9. ¡Eso es un algoritmo!
Similarmente, las técnicas que aprendiste en la escuela para añadir (cargando), substraer
(tomando prestado), y la división larga son todos algoritmos. Una de las características
de los algoritmos es que no requieren ninguna inteligencia para llevarse a cabo. Ellos son
procesos mecánicos donde cada paso que sigue precede al último de acuerdo a un conjunto
simple de reglas.
104 Capítulo 6. Iteración
6.10 Glosario
Reasignación Asignar un valor nuevo a una variable que ya existe.
Actualización Una asignación donde el valor nuevo de la variable depende del viejo.
Inicialización Una asignación que provee una variable con valor inicial que puede ser
actualizada más tarde.
Incremento Una actualización que aumenta el valor de una variable (usualmente por
uno).
Decremento Una actualización que reduce el valor de una variable.
Iteración Ejecución repetida de un conjunto de sentencias usando una llamada de una
función recursiva o un bucle.
Bucle infinito Un bucle en el cual la condición para terminar nunca es satisfecha.
Algoritmo Un proceso general para resolver una categoría de problemas.
6.11. Ejercicios 105
6.11 Ejercicios
Ejercicio 6.1. Copia el bucle de la Sección 6.7 y encapsúlalo en una subrutina llamada mi-r-cuad
que toma a $a como un parámetro, elige un valor razonable de $x, y devuelve un estimado de la raíz
cuadrada de $a.
Para probarla, escribe una función llamada probar-raiz-cuadrada que imprime una tabla como
esta:
√
1 2 2 ∞ (4k)!(1103 + 26390k )
9801 k∑
=
π =0 (k!)4 3964k
Escribe una función llamada estimar-pi que use esta fórmula para computar y devolver un esti-
mado de π. Debería usar un bucle while para computar los términos de la sumatoria hasta que el
último término sea menor que 1e-15 (la cual es la notación de Perl para 10−15 ). Puedes chequear
el resultado al compararlo con la constante integrada pi.
Solución: A.4.2.
106 Capítulo 6. Iteración
7
Cadenas de Texto
Las cadenas de texto no son como los enteros, los racionales, y los booleanos. Una cadena
de texto es una secuencia de caracteres, lo cual significa que es una colección ordenada
de otros valores, y algunas veces necesitas acceder algunos de estos valores individuales.
En este capítulo verás cómo analizar, manejar, y modificar cadenas de texto, y aprenderás
sobre algunos de los métodos que las cadenas de texto proveen. También aprenderás so-
bre una herramienta muy poderosa para la manipulación de texto, expresiones regulares
(también conocidas como “regexes“).
El método comb en la segunda sentencia divide la cadena de texto en una lista de caracteres
que puedes acceder individualmente con los corchetes.
108 Capítulo 7. Cadenas de Texto
De nuevo, la subcadena “nana“ comienza en la tercera letra de 'banana', pero esta letra
está indexada 2, y la sexta letra tiene índice 5.
Aunque esto puede ser útil en ocasiones, esta no es la manera en la que manejarías cade-
nas de texto en Perl, el cual tiene herramientas superiores que son más poderosas y más
expresivas, así que raramente necesitas usar índices o subíndices para acceder caracteres
individuales.
También, si existe una necesidad real de acceder y manipular las letras individuales, haría
más sentido almacenarlas en un array, pero no hemos hablado sobre arrays todavía, así
que regresaremos a esto más tarde.
Nota que, con el advenimiento de Unicode, la noción sobre la longitud de una cadena de
texto se ha vuelto más complicada de lo que era con cadenas de texto ASCII. Hoy en día,
un carácter puede estar compuesto de uno, dos, o más bytes. La rutina chars devuelve
el número de caracteres (en el sentido de los grafemas de Unicode, los cuales son más o
menos lo que los humanos perciben como caracteres) dentro de la cadena, aún cuando
algunos caracteres requieren una codificación de más de 2, 3, o 4 bytes.
Una cadena de texto con una longitud cero (i.e., sin caracteres) se conoce como una cadena
de texto vacía.
7.2. Operadores Comunes de las Cadenas de Texto 109
Nuevamente, el índice es un desplazo del inicio de la cadena de texto, así que el índice de
la primera letra (“n“) es cero, y el índice de la segunda “a“ es 3, no 4.
También puedes invocar a index con una sintaxis de método:
La función index puede tomar un tercer argumento opcional, un entero que indica donde
comenzar la búsqueda (y por lo tanto ignorando en la búsqueda cualquier carácter posterior
a la posición de inicio):
Nota que aunque rindex inspecciona la cadena de texto de atrás hacia adelante, la función
devuelve una posición computada desde el inicio de la cadena de texto.
Nota que, al igual que la función chars, la longitud es expresada en caracteres (o grafemas
Unicode), no en bytes. De igual modo, como puedes observar,los espacios que separan
las palabras dentro de la cadena de texto obviamente cuentan como caracteres. El argu-
mento de la longitud es opcional; si no se provee,la función substr devuelve la subcadena
comenzando en la posición de inicio al final de la cadena de texto:
110 Capítulo 7. Cadenas de Texto
También puedes comenzar a contar de atrás hacia adelante con la siguiente sintaxis:
Esto puede no ser obvio todavía, pero prontamente veremos que la combinación de las
funciones de cadenas de texto que discutimos más arriba nos dan ya mucho poder para
manipular cadenas de texto más allá de lo que piensas posible hasta el momento.
Déjanos mencionar brevemente varias funciones adicionales que pueden ser útiles de vez
en cuando.
7.2.4.1 flip
7.2.4.2 split
La función (o método) split divide una cadena de texto en subcadenas de texto, basado
en los delimitadores en la cadena de texto:
El delimitador puede ser un solo carácter con comillas como en el ejemplo más arriba o una
cadena de varios caracteres, tales como una coma y un espacio en el ejemplo más abajo:
> .perl.say for split ', ', "Jan, Feb, Mar", :v;
"Jan"
", "
"Feb"
", "
"Mar"
Los otros adverbios que pueden usarse en este contexto son :k (claves), :kv (claves y val-
ores), y :p (pares). Su significados detallados pueden encontrarse en la documentación
para split (https://docs.perl6.org/routine/split). El adverbio skip-empty remueve
las partes vacías de la lista de resultados.
La función split pueden también usarse con un patrón de expresión regular como un de-
limitador, y esto puede hacerla mucho más poderosa. Estudiaremos expresiones regulares
más adelante en este capítulo.
7.2.4.5 join
La función join toma un argumento separador y una lista de cadenas de texto como ar-
gumentos; la función las intercala con el separador, concatena todo en una sola cadena de
texto, y devuelve la cadena de texto resultante.
Este ejemplo ilustra el uso de las funciones (o métodos) words y join:
say 'I have a dream'.words.join('|'); # -> I|have|a|dream
say join ";", words "I have a dream"; # -> I;have;a;dream
En ambos casos, words primero divide la cadena original en una lista de palabras, splits
sutura los artículos de esta lista y los devuelve en una nueva cadena de texto intercalados
con el separador.
my $indice = 0;
my $fruta = "banana";
while $indice < $fruta.chars {
my $letra = substr $fruta, $indice, 1;
say $letra;
$indice++;
}
b
a
n
a
n
a
Este bucle recorre la cadena de texto y muestra cada letra en un su propia línea. La condi-
ción del bucle es $indice < $fruta.chars, así que cuando $indice es igual a la longitud
de la cadena de texto, la condición es falsa, y el cuerpo del bucle no se ejecuta. En otras
palabras, el bucle termina cuando $indice es la longitud de la cadena de texto menos uno,
lo cual corresponde al último carácter de la cadena de texto.
Como un ejercicio, escribe una función que toma una cadena de texto como un argumento
y muestra las letras de atrás hacia adelante, una por línea. Hazlo por lo menos una vez sin
utilizar la función flip. Solución: A.5.1.
my $fruta = "banana";
for $fruta.comb -> $letra {
say $letra
}
Cada vez a través del bucle, el siguiente carácter en la cadena de texto es asignado a la
variable $letra. El bucle continúa hasta que no hayan más caracteres.
El siguiente ejemplo muestra cómo usar la concatenación y un bucle for para generar una
tipo de abecedario (es decir, en orden alfabético). En el libro Make Way for Ducklings de
McCloskey, los nombres de los patitos son Jack, Kack, Lack, Mack, Nack, Ouack, Pack, y
Quack. Este bucle podría mostrar estos nombres en orden:
my $sufijo = 'ack';
for 'J'..'Q' -> $letra {
say $letra ~ $sufijo;
}
La salida es:
Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack
Por supuesto, esto no es del todo correcto dado que “Ouack” y “Quack” están mal escritos.
Como un ejercicio, modifica el programa para arreglar este error. Solución: A.5.2.
my $palabra = 'banana';
my $conteo = 0;
for $palabra.comb -> $letra {
$conteo++ if $letra eq 'a';
}
say $conteo; # -> 3
manipulación de cadenas de texto. Pero supón que quieres extraer de la cadena “yellow
submarine“ cualquier letra después de la letra “l“ y antes de la letra “w“. Este tipo de
“búsqueda difusa“ puede hacerse con un bucle, pero no es práctico. Puedes hacerlo como
un ejercicio si deseas, pero debo advertirte: es algo delicado y difícil. Aún si no lo haces, la
solución puede interesarte: ver SubsecciónA.5.4.
Si agregas otra condición, por ejemplo que dicha letra debería extraerse o capturarse (i.e.
ser almacenada para uso futuro) solo si el resto de la cadena de texto contiene la subcadena
“rin“, esto comienza a volverse algo tedioso. También, cualquier cambio a los requerim-
ientos conduce a que tengamos que hacer una reescritura substancial o hasta refactorizar
el código completamente.
Para este tipo de trabajo, una expresión regular o un regex es una herramienta mucha
más poderosa y expresiva. La siguiente es una manera de extraer letras usando el criterio
descrito anteriormente:
arraigadas en el núcleo del lenguaje Perl, mientras que los otros lenguajes las han adoptado
como un add-on o plug-in, usualmente basado o derivado de una librería conocida como
Expresiones Regulares Compatible con Perl (PCRE por sus siglas en inglés).
Las expresiones regulares de Perl han extendido estas nociones tanto que tienen muy poco
que ver con el concepto original en la teoría de lenguaje, y por lo tanto se ha consider-
ado apropiado parar de llamarlas expresiones regulares y en cambio hablar de regexes, i.e.,
un tipo de sub-lenguaje de coincidencia de patrón que funciona de manera similar a las
expresiones regulares.
El proceso de coincidencia podría describirse de la siguiente forma (pero ten presente que
esto una simplificación): busca en la cadena de texto (desde izquierda a derecha) un carác-
ter que coincide con el primer átomo (i.e., el primer artículo de coincidencia) del patrón;
cuando se encuentra, busca si el segundo carácter puede coincidir el segundo átomo del
patrón, etc. Si se usa el patrón entero, entonces el regex fue exitoso. Si falla durante el
proceso, comienza nuevamente desde la posición inmediatamente después del punto de
coincidencia inicial. Esto se conoce como vuelta atrás o retroceso (backtracking en inglés).
Y se repite hasta que uno de los siguientes casos ocurra:
• Hay una coincidencia exitosa. En este caso, el proceso finaliza y se reporta éxito.
En este ejemplo, el motor de regex comienza a coincidir “bca“ con bc., pero el atento coin-
cidencia inicial falla, porque la siguiente letra en la cadena de texto, “b“, no coincide con la
7.7. Cómo Construir tus Patrones Regex 117
“e“‘del patrón. El motor de regex da vuelta atrás e inicia la búsqueda nuevamente desde
la tercera letra (“c“) de la cadena. Comienza una nueva coincidencia en la quinta letra de
la cadena (la segunda “b“), logra coincidir exitosamente con “bcde“ y sale con un estado
exitoso (sin buscar ninguna otra coincidencia).
Si la cadena de texto a analizar está contenida en la variable tópica $_, entonces el operador
de coincidencia inteligente es implícito y la sintaxis es aún más simple:
En todos los casos que hemos visto hasta ahora, usamos directamente un patrón dentro
de un par de delimitadores de barra oblicua /. Podemos utilizar otros delimitadores si
precedemos nuestro patrón con la letra “m“:
o:
El operador "m" no altera la manera en que un regex funciona; solo hace posible el uso de
otros delimitadores (además de las barras oblicuas). Dicho diferentemente, el prefijo "m"
es la manera estándar de introducir un patrón, pero es implícito y puede omitirse cuando
el patrón es delimitado por barras oblicuas. Es probablemente mejor usar barras oblicuas,
porque es lo que la gente comúnmente utiliza y reconoce inmediatamente, y usar otros
delimitadores solo cuando el patrón de regex contiene barras oblicuas.
Un patrón puede también almacenarse en una variable (o, más precisamente, en un objeto
regex), usando el operador rx//:
my $regex = rx/c..f/;
say "Coincidió" if 'abcdef' ~~ $regex; # -> Coincidió
my $cadena = "superlative";
say "$cadena contiene 'perl'." if $cadena ~~ /perl/;
# -> superlative contiene 'perl'.
Sin embargo nota que, para tales coincidencias literales, la función index discutida ante-
riormente será probablemente más eficiente que un regex con cadenas de texto largas. El
método contains, el cual devuelve verdadero si su argumento es una subcadena de su
invocante, es también probablemente más rápido.
Los caracteres alfanuméricos y la raya baja _ son coincidencias literales. Todos los otros
caracteres deben escaparse con una barra invertida (por ejemplo \? para coincidir con un
signo de interrogación derecho) o incluirse dentro de comillas:
my $cadena = "superlative";
say "$cadena contiene 'pe.l'." if $cadena ~~ / pe . l /;
# -> superlative contiene 'pe.l'.
El ejemplo más arriba ilustra otra característica de los regexes: los espacios en blanco usual-
mente no son significativos dentro de los patrones de regex (a menos que se especifiquen
con el adverbio : s o : sigspace, como veremos más adelante).
Hay categorías de caracteres predefinidas de la forma \w. Su negación se escribe con una
letra en mayúscula, \W. El carácter de palabra \w coincide con un solo carácter alfanumérico
(i.e., entre los caracteres alfanuméricos, dígitos, y el carácter _). Por su parte, \W coincidirá
cualquier otra carácter que no sea un carácter de palabra. Nota, sin embargo, que Perl 6
es compatible con Unicode y que, por ejemplo, letras del alfabeto griego o cirílico o dígitos
tailandenses coincidirán con \w:
En este ejemplo, las cadenas de texto coincidieron porque, de acuerdo al estándar Unicode,
δ ("G REEK SMALL LETTER DELTA") es una letra y por lo tanto pertenece a la categoría de
clase \w.
Otras categorías de caracteres incluyen:
7.7. Cómo Construir tus Patrones Regex 119
Aquí, coincidimos con ”nd 007”, porque encontramos un carácter de palabra (”n”), seguido
por un un carácter que no es un dígito (“d“), seguido por un espacio en blanco, seguido
por tres dígitos.
También puedes especificar tus propias categorías de caracteres al insertar entre <[ ]>
cualquier número de caracteres individuales y rangos de caracteres (expresado con dos
puntos entre los puntos extremos), con o sin espacio en blanco. Por ejemplo, una categoría
de caracteres para dígitos hexadecimales podría lucir así:
Por favor nota que generalmente no tienes que escapar caracteres que no son alfanuméricos
en tus categorías de caracteres:
En este ejemplo, usamos el cuantificador ”+” que discutiremos en la siguiente sección, pero
el punto aquí es que no necesitas escapar el punto y la barra oblicua dentro de la definición
de categoría de caracteres.
7.7.3 Cuantificadores
Un cuantificador hace que un átomo1 precedente coincida no solamente una vez, sino un
específico o variable número de veces. Por ejemplo, a+ coincide una o más caracteres ”a”.
En el siguiente código, \d+ coincide con uno o más dígitos (tres dígitos en este caso):
Los cuantificadores + and ∗ se dice que son voraces (o greedy en inglés), lo cual significa
que ellos coinciden con tantos caracteres como sea posible. Por ejemplo:
Aquí, .+ coincide con tantos caracteres de la cadena de texto como sea posible, mientras
que es capaz de coincidir con la final ”b”. Usualmente esto es lo que deseas, pero no
siempre. Quizás tu intención era coincidir con todas las letras hasta la primera “b“. En
tales casos, usarías las versiones frugales (no voraces) de estos cuantificadores, las cuales
se obtienen al precederlos con un signo interrogación derecho: +? y ∗?. Un cuantificador
frugal coincidirá con tantos caracteres como sea posible para que el regex sea exitoso pero
no más de lo necesario. Para coincidir con todas las letras hasta la primera b, podrías usar:
También podrías especificar un rango (min..max) para indicar el número de veces que un
átomo debe coincidir. Por ejemplo, para coincidir con un entero menor que 1,000:
Esto coincide con uno a tres dígitos. Los caracteres ^ y $ son anclas que representan el
inicio y el final de la cadena de texto y los discutiremos en la siguiente sección.
Para coincidir con un número exacto de veces, solo reemplaza el rango con un número
individual:
7.7.4.1 Anclas
Las anclas más usadas comúnmente son ^ (inicio de la cadena de texto) y $ (final de la
cadena de texto):
my $cadena = "superlative";
say "$cadena comienza con 'perl'" if $cadena ~~ /^perl/; # (No salida)
say "$cadena termina con 'perl'" if $cadena ~~ /perl$/; # (No salida)
say "$cadena es igual a 'perl'" if $cadena ~~ /^perl$/; # (No salida)
7.7. Cómo Construir tus Patrones Regex 121
Los tres regexes más arriba fallan porque, aunque la $cadena contiene la subcadena “perl“,
la subcadena no se encuentra al inicio, ni al final de la cadena de texto.
En el evento de que tengas que manejar cadenas de texto con líneas múltiples, podrías usar
las anclas ^^ (inicio de línea) y $$ (final de línea).
Existen otras anclas útiles, como las anclas << (inicio de palabra o borde izquierdo de pal-
abra) y >> (final de palabra o borde derecho de palabra).
Las aserciones look-around (o "mirar alrededor") hacen posible especificar reglas más com-
plejas: por ejemplo, coincidir “foo“ pero solamente si es precedida (o seguida) por “bar“ (o
ni precedida ni seguida por “bar“):
También puedes incluir una aserción de código <?{...}>, la cual coincidirá si el bloque de
código devuelve un valor verdadero:
> say ~$/ if /\d\d <?{$/ == 42}>/ for <A12 B34 C42 D50>;
42
Una aserción de código negativa <!{...}> coincidirá siempre y cuando el bloque no de-
vuelve un valor verdadero:
> say ~$/ if /\d\d <!{$/ == 42}>/ for <A12 B34 C42 D50>
12
34
50
Las aserciones de código son útiles para especificar condiciones que no pueden expresarse
fácilmente como los regexes.
De igual manera, pueden usarse para mostrar algo, por ejemplo para el propósito de depu-
rar un regex al imprimir información sobre las coincidencias parciales:
122 Capítulo 7. Cadenas de Texto
> say "Coincidió $/" if "A12B34D50" ~~ /(\D) <?{ say ~$0 }> \d\d$/;
A
B
D
Coincidió D50
La salida muestra los atentos múltiples de coincidencia que fallaron (“A” y “B”) antes que
el proceso de vuelta atrás condujera al éxito (“D50” al final de la cadena).
No obstante, las aserciones de código son raramente necesarias para casos simples, porque
siempre puedes añadir un bloque simple de código con el mismo propósito:
Este código produce la misma salida, sin preocuparnos si el bloque devuelve un valor
verdadero.
7.7.5 Alternancia
Las alternancias se usan para coincidir con una de varias alternativas.
Por ejemplo, para chequear si una cadena de texto representa uno de los tres colores básicos
de un una imagen (en JPEG y algunos otros formatos), podrías usar:
En este ejemplo, el patrón coincide con “ab“ sin tener que hacer coincidencia más adelante,
aunque habría una coincidencia presumiblemente “mejor“ (i.e., más larga) con la otra alter-
nativa. Cuando se usa este tipo de alternación, tienes que pensar cuidadosamente acerca
del orden en el cual colocas las múltiples alternativas, dependiendo en lo que necesitas
hacer.
Sin embargo, ten presente que esto funcionará como se explicó más arriba solo si las coin-
cidencias alternativas comienzan en la misma posición dentro de la cadena de texto:
Aquí, la coincidencia con la posición más a la izquierda triunfa (esta es una regla general
con los regexes).
7.7. Cómo Construir tus Patrones Regex 123
La diferencia entre los paréntesis y los corchetes es que los paréntesis no solo agrupan las
cosas, sino que capturan datos: ellos permiten que la cadena de texto que coincide dentro
de los paréntesis esté disponible como una variable especial (y también como un elemento
del objeto de coincidencia resultante):
En este ejemplo, el patrón coincidió con la cadena de texto $cad y la parte del patrón dentro
de los paréntesis fue capturada en la variable especial $0. Si hay varios grupos con parén-
tesis, las capturas se encuentran en las variables $0, $1, $2, etc. (contando los paréntesis
abiertos de la izquierda a derecha):
Las variables $0, $1, etc. son actualmente atajos para $/[0], $/[1], el primer y se-
gundo elemento del objeto de coincidencia en un contexto de lista, así que imprimir
"El número es $/[0]" tendría el mismo efecto.
Como se indicó, los paréntesis tienen dos roles en los regexes: ellos agrupan elementos de
regex y capturan lo que coincide con el subregex dentro de los paréntesis. Si solo deseas el
comportamiento de agrupamiento, usa los corchetes [ ... ]:
Por ejemplo, el adverbio :ignorecase (or :i) le dice al compilador que ignore la distinción
entre letras mayúsculas y letras minúsculas:
124 Capítulo 7. Cadenas de Texto
La función integrada so usada aquí coacciona su argumento (i.e., el valor devuelto por la
expresión de coincidencia de regex) a un valor Booleano.
Si se coloca en frente del patrón, el adverbio aplica al patrón entero:
El adverbio también puede colocarse después en el patrón y afectar en este caso solo la
parte del regex posterior al mismo:
Anteriormente dije que los espacios en blanco usualmente no son significativos en los pa-
trones de regex. El adverbio :sigspace o :s hace que los espacios en blanco sean significa-
tivos en un regex:
En lugar de buscar por solo una coincidencia y devolver un objeto de coincidencia, el ad-
verbio :global o :g le dice al compilador que busque cada coincidencia que no esté super-
puesta y las devuelva en una lista:
> say "Conteo de palabras = ", $/.elems if "I have a dream" ~~ m:g/ \w+/;
Conteo de palabras = 4
> say ~$/[3];
dream
Estos son los adverbios más usados comúnmente. Otro adverbio, :ratchet o :r, le dice
al motor de regex que no vuelva atrás y es muy importante para usos específicos, pero
regresaremos a esto en otro capítulo (ver sección 13.4).
• Un número octal válido (números en el sistema octal solo usan los dígitos del 0 al 7);
• La primera palabra al inicio de una cadena de texto (para el propósito de estos pe-
queños ejercicios, el separador de palabra puede considerarse un espacio, pero po-
drías hacerlo sin esta suposición);
• La primera palabra de una cadena de texto que comienza con una “a”;
• La primera palabra de una cadena de texto que comienza con una letra minúscula;
• Un número telefónico francés (en Francia, los números telefónicos tienen 10 dígitos
y comienzan con “06“ o “07“); puedes asumir que los dígitos son consecutivos (sin
espacios);
• La primera palabra de una cadena de texto que comienza con una vocal en mayúscula
o minúscula;
• La primera ocurrencia de una letra doble (la misma letra dos veces);
Solución: A.5.5
Como mencionamos anteriormente, uno de los lemas en Perl es “Hay más de una forma
para hacer algo“ (TIMTOWTDI). Los ejemplos múltiples más abajo deberían ilustrar este
principio bastante bien al mostrar varias maneras diferentes para extraer la fecha en la
cadena:
• Uso de una categoría de caracteres y un cuantificador para evitar coincidir con al-
gunos números pequeños en otra parte de la cadena si existen:
say ~$0 if $cadena ~~ /(<[\d-]> ** 10)/; # -> 2016-12-25
• El mismo regex, pero usando un agrupamiento adicional para evitar la repetición del
subpatrón \- \d\d:
say ~$/[0] if $cadena ~~ /(\d ** 4 [\- \d\d] ** 2 )/;
Nota que el uso de la tilde como un prefijo más arriba causa que $año, $mes, y $día
sean pobladas con cadenas de texto. Asumiendo que desees que estas variables con-
tengan números enteros, podrías coaccionarlas en valores numéricos usando el oper-
ador prefijo +:
my $a = rx/\d ** 4/;
my $m = rx/\d ** 2/;
my $d = rx/\d ** 2/;
$cadena ~~ /(<$y>) \- (<$m>) \- (<$d>)/;
my ($año, $mes, $día) = ~$0, ~$1, ~$2;
• Podríamos mejorar el subpatrón $m (mes) para que coincida solo con “01“ al “12“ y
así verificar que solo coincida con un número de mes válido:
my $m = rx { 1 <[0..2]> # 10 a 12
|| 0 <[1..9]> # 01 a 09
};
Como puedes ver, el uso de comentarios y espacios en blanco facilita que la intención
del regex sea más clara.
Otra forma de lograr la misma meta es usar una aserción de código para chequear
que el valor se encuentre numéricamente entre 1 y 12:
my $m = rx /\d ** 2 <?{ 1 <= $/ <= 12 }> /;
Como un ejercicio, podrías intentar validar que el subpatrón $d (día) cubra los
números en el rango 01 y 31. Intenta usar ambas técnicas de validación que fueron
discutidas anteriormente.
El objeto de coincidencia $/ tiene los métodos prematch y postmatch para extraer lo que
viene antes y después de la parte que coincidió en la cadena de texto:
Como un ejercicio, intenta adaptar los regexes anteriores para varios formatos de fecha
(tales como DD/MM/YYYY o YYYY MM, DD) y pruébalos. Si intentas con el formato
YYYY MM, DD, por favor recuerda que los espacios no son usualmente significativos en un
patrón de regex, así que tendrías que especificar espacios en blanco explícitos (con el uso de
la categoría de caracteres \s) o utilizar el adverbio :s para hacer los espacios significativos.
• Uso de una categoría de caracteres y un cuantificador (nota que cada octeto puede
tener de uno a tres dígitos, así que el total de números de caracteres puede variar de
7 a 15):
say ~$0 if $cadena ~~ /(<[\d.]> ** 7..15)/;
Con esta definición del subpatrón $octeto, el regex podría coincidir con cualquier
número de uno o dos dígitos, o un número de tres dígitos comenzando con los dígitos
del 1 al 2.
7.9 Sustituciones
El reemplazo de una parte de una cadena de texto con otra subcadena es un requerimiento
muy frecuente en el manejo de cadenas de texto. Esto podría ser necesario para la correc-
ción de errores ortográficos, formateo de datos, remoción de información personal de los
datos, etc.
my $cadena = "abcdefg";
$cadena = $cadena.subst("cd", "DC"); # -> abDCefg
El primer argumento de este método es la parte de búsqueda, y puede ser una cadena de
texto literal, como en el ejemplo más arriba, o un regex:
my $cadena = "abcdefg";
$cadena = $cadena.subst(/c \w+ f/, "SUBST"); # -> abSUBSTg
my $cadena = "salviavodas";
$cadena ~~ s/ v \w+ o /VAVI/; # -> salVAVIdas
$_ = "salviavodas";
s/ v \w+ o /VAVI/; # -> salVAVIdas
Los delimitadores no tienen que ser barras oblicuas (y esto puede ser bastante útil si la
búsqueda o el reemplazo contiene barras oblicuas):
7.9.4 Adverbios
Los adverbios discutidos más arriba (Sección 7.7.7) pueden usarse con el operador de susti-
tución.
Los modificadores más usados comúnmente en sustituciones son los adverbios
:ignorecase (o :i) y :global (o :g). Ellos funcionan como lo describimos en la Sub-
sección 7.7.7 de la sección sobre regexes y coincidencia.
El punto que quiero hacer aquí es que las sustituciones se hacen usualmente solo una vez.
Pero con el adverbio :global (o :g), se realizarán en toda la cadena de texto:
my $i = 0;
my $j = $palabra2.chars;
while $j > 0 {
say '$i = ', $i, ' $j = ', $j;
return False if substr($palabra1, $i, 1) ne substr($palabra2, $j, 1);
$i++; $j--;
}
return True;
}
say es-inversa "pots", "stop";
La primera sentencia sufijo if chequea si las palabras tienen la misma longitud. Si no,
podemos devolver False inmediatamente. De lo contrario, por el resto de la subrutina,
podemos asumir que las dos palabras tienen la misma longitud. Este es un ejemplo del
patrón guardián descrito en la Sección 5.9 (p. 85).
Las variables $i y $j son los índices: $i recorre a $palabra1 hacia adelante mientras $j
recorre a $palabra2 hacia atrás. Si encontramos dos letras que no coinciden, podemos
devolver False inmediatamente. Si logramos pasar a través del bucle y todas las letras
coinciden, devolvemos True.
Si probamos esta función con las palabras ”stop” y ”pots”, esperamos el valor de retorno
True, pero obtenemos False en lugar de eso. Así que, cuál es el problema?
Con este tipo de código, el sospechoso usual es un posible error en el manejo de los índices
(quizás un error de un solo número). Para depurar este tipo de error, el primer paso podría
ser la impresión de los valores de los índices inmediatamente antes de las líneas donde se
usan:
my $i = 0;
my $j = $palabra2.chars;
while $j > 0 {
say '$i = ', $i, ' $j = ', $j;
return False if substr($palabra1, $i, 1) ne substr($palabra2, $j, 1);
$i++; $j--;
}
7.10. Depuración de Programas 131
$i 0 $j 3
return True;
}
$i = 0 $j = 4
False
La primera vez a través del bucle, el valor de $j es 4, el cual está fuera de rango para la
cadena de texto 'pots'. El índice del último carácter es 3, así que el valor inicial para $j
debería ser $palabra2.chars - 1.
Nota que en el evento de no haber sido suficiente para rastrear el error de fuera de rango,
podríamos haber ido un paso más allá e imprimir las letras. Al hacer esto, hubiésemos
visto que no obteníamos la última letra de la segunda palabra.
Si arreglamos el error y ejecutamos el programa nuevamente, obtenemos:
$i = 0 $j = 3
$i = 1 $j = 2
$i = 2 $j = 1
True
Esta vez obtenemos la repuesta correcta, pero parece ser que el bucle solo se ejecutó tres
veces, lo cual es sospechoso: parece que el programa no comparó la última letra de la
primera palabra (indexada $i = 3) con la última letra de la segunda palabra (indexada $j
= 0).
Podemos confirmar esto al ejecutar la subrutina con los siguientes argumentos: “stop” y
“lots”, lo cual muestra:
$i = 0 $j = 3
$i = 1 $j = 2
$i = 2 $j = 1
True
Comienza con este diagrama, ejecuta el programa en papel, cambia los valores de $i y $j
durante cada iteración. Encuentra y arregla el segundo error en esta función.
Solución: A.5.6.
7.11 Glosario
Objecto Algo que una variable puede almacenar. Por el momento, puedes usar “objeto“ y
“valor“ indistintamente.
Secuencia Una colección ordenada de valores donde cada valor es identificado por un
índice entero.
Índice Un valor entero que se usa para seleccionar un artículo en una secuencia, tal como
un carácter en un cadena de texto. En Perl, los índices comienzan desde el 0.
Rebanada Una parte de una cadena de texto especificada por un rango de índices.
Cadena de texto vacía Una cadena de texto sin caracteres y con longitud 0, representada
por dos comillas.
Recorrer Iterar a través de los artículos de una secuencia, realizando una operación similar
en cada uno de ellos.
Búsqueda Una patrón de recorrido que cesa cuando encuentra lo que está buscando.
Contador Una variable que se usa para contar algo, usualmente inicializada a cero y pos-
teriormente incrementada.
Patrón Una secuencia de caracteres con sintaxis especial que describen de izquierda a
derecha el contenido que se intenta coincidir dentro de una cadena de texto en par-
ticular.
Retroceso El proceso por el cual, dado un intento de coincidencia fallido en una cadena
de texto, el motor de regex abandona parte del intento de coincidencia actual, vuelve
atrás en la cadena de texto, e intenta nuevamente para ver si puede encontrar una
nueva ruta a una coincidencia exitosa. El proceso de retroceso eventualmente ter-
mina tan pronto una coincidencia exitosa ocurre, o finalmente cuando todas las coin-
cidencias posibles fallan. Este proceso se conoce como backtracking en inglés.
7.12. Ejercicios 133
7.12 Ejercicios
Ejercicio 7.1. Escribe una subrutina que use la función index en un bucle para contar el número
de caracteres “a“ en `banana` como hicimos en la Sección 7.4. Modifícala para contar cualquier
letra en cualquier palabra pasadas como argumentos a la subrutina.
Escribe otra subrutina para contar una letra dada en una palabra dada usando la función substr.
Solución: A.5.7.
Ejercicio 7.2. La categoría de caracteres <[a..z]> coinciden con cualquier carácter en minúscula
(solo caracteres ASCII en minúscula, no caracteres Unicode). La siguiente subrutina:
debería devolver True si su argumento es una letra ASCII en minúscula y de lo contrario, False.
Asegúrate de que funcione como se espera (y arréglala si es necesario). La función so coacciona el
resultado de la coincidencia de regex en un valor Booleano.
Las siguientes subrutinas usan la subrutina es-minúscula y su trabajo es chequear si una cadena
de texto contiene letras en minúsculas, pero algunas de ellas no funcionan adecuadamente. Analiza
cada subrutina manualmente, determina si funciona bien, y describe lo que actualmente hace (puedes
asumir que el parámetro es una cadena de texto). Después, pruébalas todas con varias cadenas de
texto para chequear si tu análisis estaba en lo cierto.
}
return True;
}
}
Solución: A.5.8.
Ejercicio 7.3. Un cifrado César es una forma débil de cifrado que involucra la “rotación“ de cada
letra por un número fijo de posiciones. La rotación de una letra se refiere a su desplazamiento a través
del alfabeto, y puede envolverse alrededor si es necesario, así que “A“ rotada por 3 corresponde a “D“
y “Z“ rotada por 1 corresponde a “A“.
Para rotar una palabra, rota cada letra por el mismo número de posiciones. For ejemplo, “educar“
rotada por 7 es “lkbjhy“ y “carta“ rotada por -10 es “sqhjq“. (Para el cifrado, usamos solo caracteres
del alfabeto básico latino ISO.) En la película 2001: A Space Odyssey, la computadora de la nave
se llama HAL, que es una rotación de IBM por -1.
Escribe una función llamada rotar-palabra que toma una cadena de texto y un número entero
como parámetros, y devuelve una nueva cadena que contiene las letras de la cadena original rotadas
por el monto dado.
Podrías desear utilizar las funciones integradas ord, la cual convierte un carácter a su código
numérico (Unicode code point), y char, la cual convierte tales código numérico devuelta a los car-
acteres:
Las letras del alfabeto están codificadas en orden alfabético, así que por ejemplo:
dado que 'c' es la segunda letra después de 'a' en el alfabeto. Pero ten pendiente: los códigos
numéricos para las letras mayúsculas son diferentes.
Bromas en la Internet que tienen el potencial de ser ofensivas son algunas veces codificadas en
ROT13, el cual es un cifrado César con rotación 13. Dado que 13 es la mitad del número de letras en
nuestro alfabeto, la aplicación de una rotación por 13 devuelve la palabra original, así que el mismo
procedimiento puede usarse para cifrar y descifrar en rotación 13. Si no te ofendes fácilmente,
encuentra y descifra algunas de estas bromas. (ROT13 se usa también para otros propósitos, tales
como ocultar débilmente la solución de un acertijo.)
Solución: A.5.9.
136 Capítulo 7. Cadenas de Texto
8
Caso Práctico: Juego de Palabras
slurp-rest el cual devuelve el contenido completo del archivo desde la posición actual
hasta el final (y el contenido completo del archivo si lo hemos abierto por primera vez).
Esta es la manera tradicional de abrir y leer archivos en la mayoría de los lenguajes.
No obstante, el rol IO de Perl (en términos simples, un rol es una colección de métodos
relacionados) ofrece métodos más simples los cuales pueden abrir y leerlo en una sola
instrucción (i.e., sin tener que primero abrir un descriptor de archivo y después cerrarlo):
La función (o método) slurp toma la responsabilidad de abrir y cerrar el archivo por ti.
Podemos también leer el archivo línea por línea, lo cual es muy práctico si cada línea con-
tiene una entidad lógica tal como un registro, y es especialmente útil para archivos muy
grandes que podrían no caber en la memoria:
Por defecto, el método .lines removerá los caracteres de nueva línea colgantes de cada
línea, para que no tengas que preocuparte sobre ellos.
No hemos estudiados los arrays aún, pero puedes también leer todas las líneas de un
archivo en un array, con cada línea del archivo volviéndose un artículo del array. Por
ejemplo, puedes cargar el archivo mi_archivo.txt en el array @líneas:
my @líneas = "mi_archivo.txt".IO.lines;
El acceso a cada línea puede entonces hacerse con el operador bracket y un índice. Por
ejemplo, para imprimir la primera y séptima línea:
say @líneas[0];
say @líneas[6];
Para escribir datos en un archivo, es posible abrirlo al igual que cuando queremos leer
desde el mismo, excepto que la opción :w (write) (escritura) debe usarse:
Sé cuidadoso con esto. Si el archivo ya existía, cualquier contenido será sobrescrito. Así
que sé cauteloso al abrir un archivo en el modo de escritura, debido a que podrías borrar
todo el contenido de un archivo si no tienes cuidado.
También es posible abrir el archivo en modo de concatenación, usando la opción :a (ap-
pend). Con esta opción, los datos nuevos serán entonces agregados al contenido existente.
La escritura a un archivo puede simplificarse con el uso de la función spurt, la cual abre el
archivo, escribe los datos en el mismo, y consecuentemente lo cierra:
8.2. Lectura de Listas de Palabras 139
spurt "ruta/a/mi_archivo.txt",
"escribir esta línea en el archivo\n ",
:append;
Nota del traductor: En lugar de utilizar la lista de palabras en spanish.txt del proyecto
Moby, he decidido utilizar la lista en lemario.txt (https://gitlab.com/uzluisf/
piensaperl6/tree/master/suplementos/lemario.txt)que contiene aproximada-
mente 88,000 palabras1 . La decisión se debe a que la lista de palabras en español del
proyecto Moby contiene muchas palabras que no están formateadas apropiadamente
y esto dificultaría el procesamiento adecuado de las palabras.
Algunos ejercicios y ejemplos más adelante usan la lista de palabras
words.txt usada en Think Perl 6. Dicha lista puede encontrarse aquí
(https://raw.githubusercontent.com/LaurentRosenfeld/thinkperl6/master/
Supplementary/words.txt).2
Este archivo contiene texto simple (con cada palabra de la lista en su propia línea), así que
puedes abrirlo con un editor de texto, pero también puedes leerlo con Perl. Hagamos esto
en el modo interactivo (con el REPL):
proyectos/lemarios/index.html)
2 O alternativamente aquí: https://gitlab.com/uzluisf/piensaperl6/raw/master/suplementos/words.
txt
140 Capítulo 8. Caso Práctico: Juego de Palabras
Imprimir la variable $línea entre los paréntesis angulares dentro de una cadena de texto
nos muestra que la función get removió explícitamente los caracteres de nueva línea col-
gantes, en este caso una combinación de los caracteres \r\n (un retorno de carro y una
nueva línea), dado que este archivo aparentemente fue preparado en Windows.
Esto es grandioso si queremos explorar las primeras líneas del archivo lemario.txt, pero
no vamos a explorar las casi 88,000 líneas del archivo de esta forma.
Necesitamos que un bucle lo haga por nosotros. Podríamos insertar la nueva instrucción
get en un bucle while, pero hemos visto una manera más fácil y más eficiente de hacerlo
con un bucle for y el método IO.lines, sin la inconveniencia de abrir o cerrar el archivo:
Este código lee el archivo línea por línea, e imprime cada línea en la pantalla. Pronto
haremos cosas más interesantes que solo mostrar las líneas en la pantalla.
8.3 Ejercicios
Este caso práctico consiste principalmente de ejercicios y soluciones dentro del cuerpo del
capítulo porque solucionar los ejercicios es la manera principal de enseñar el material de
este capítulo. Por lo tanto, las soluciones a estos ejercicios se encuentran en las siguientes
secciones de este capítulo, no en el apéndice. Por lo menos deberías tratar de solucionar
cada uno de ellos antes de leer las soluciones.
Ejercicio 8.1. Escribe un programa que lee el archivo lemario.txt e imprime solo las palabras
con más de 20 caracteres.
Ejercicio 8.2. En 1939, Ernest Vincent Wright publicó una novela con aproximadamente 50,000
palabras llamada Gadsby que no contiene la letra “e“. Dado que “e“ es la letra más común en inglés,
eso no es fácil de hacer. De hecho, es difícil construir un solo pensamiento sin usar la letra más
común. Tales escritos donde se omite alguna letra son algunas veces conocidos como un lipogramas.
Escribe una subrutina llamada sin-e que devuelve True si la palabra dada no contiene la letra “e“.
Modifica tu programa del ejercicio previo para imprimir solamente palabras que no tienen “e“ y
calcular el porcentaje de palabras en la lista que no tienen “e“.
(El diccionario de palabras que estamos usando, lemario.txt, tiene todas las letras en minúsculas,
así que no tienes que preocuparte acerca de la “E“ mayúscula.)
8.4. Búsqueda 141
Ejercicio 8.3. Escribe una subrutina llamada evitar que toma una palabra y una cadena de texto
de letras prohibidas, y devuelve True si la palabra no contiene ninguna de las letras prohibidas.
Después, modifica tu programa para encitar el usuario a entrar una cadena de texto de letras pro-
hibidas y después imprimir el número de palabras que no contienen ningunas de ellas. ¿Puedes
encontrar una combinación de cinco letras prohibidas que excluya el menor número de palabras?
Ejercicio 8.4. Escribe una subrutina llamada usa-solamente que toma una palabra y una cadena
de letras, y devuelve True si la palabra contiene solo las letras en la lista. ¿Puedes construir una
oración usando solo las letras acefhlo?
Ejercicio 8.5. Escribe una subrutina llamada usa-todas que toma una palabra y una cadena de
letras requeridas, y devuelve True si la palabra usa todas las letras requeridas por lo menos una vez.
¿Cuántas palabras se encuentran ahí que usan todas las vocales aeiou? ¿Y cuántas usan aeiouy?
Ejercicio 8.6. Escribe una función llamada es_abecedaria que devuelve True si las letras en
una palabra aparecen en orden alfabético (letras dobles son aceptables). ¿Cuántas palabras son
abecedarias?
8.4 Búsqueda
La mayoría de los ejercicios en la sección previan tiene algo en común; pueden solucionarse
con el patrón de búsqueda (y la función index que vimos en la Sección 7.2.2 (p. 109). Mu-
chos de ellos también pueden solucionarse con regexes.
Dado que el código es tan simple, esto es un ejemplo típico de un one-liner útil (como
se describió en la Sección 2.5). Asumiendo que quieras saber las palabras con más de 20
caracteres, no necesitas ni siquiera escribir un script, guardarlo y ejecutarlo. Simplemente
puedes teclear esto en la prompt de tu sistema operativo:
La opción “-e“ le dice a Perl que el script a ser ejecutado viene en la línea de comando entre
comillas. La “-n“ le indica a Perl que lea el archivo línea por línea después de la línea de
comando, almacene cada línea en la variable tópico $_, y aplique el contenido del script a
cada línea. Y el script en modo one-liner solo imprime el contenido de $_ si su longitud es
mayor que 20.
Para simplificarlo un poco, las dos opciones -n y -e pueden agruparse como perl6 -ne. En
adición, la variable tópico $_ puede omitirse en las llamadas de método (en otras palabras,
si un método no tiene invocante, el invocante será $_ por defecto). Finalmente, el punto y
coma colgante puede igualmente removerse. El script en modo one-liner más arriba puede
entonces simplificarse aún más:
142 Capítulo 8. Caso Práctico: Juego de Palabras
Recuerda que, si está intentando esto en Windows, necesita reemplazar las comillas sim-
ples con las comillas dobles (y viceversa si el script contiene comillas dobles):
Podríamos también haber usado un regex para comprobar la presencia de una “e“ en la
segunda línea de esta subrutina:
Esta sintaxis bastante concisa es atractiva, pero cuando se busca por una coincidencia literal
exacta, la función index es probablemente más eficiente (más rápida) que un regex.
Buscar y contar las palabras sin “e“ en nuestra lista de palabras no es muy difícil:
my $cuenta-total = 0;
my $cuenta-sin-e = 0;
for 'lemario.txt'.IO.lines -> $línea {
$cuenta-total++;
if sin-e $línea {
$cuenta-sin-e++;
8.4. Búsqueda 143
say $línea;
}
}
say "=" x 24;
say "Cuenta total de palabras: $cuenta-total";
say "Palabras sin 'e': $cuenta-sin-e";
printf "Porcentaje de palabras sin 'e': %.2f %%\n",
100 * $cuenta-sin-e / $cuenta-total;
========================
Cuenta total de palabras: 87899
Palabras sin 'e': 36671
Porcentaje de palabras sin 'e': 41.72 %
Así que más de un tercio de las palabras en nuestra lista no tienen “e“.
Podemos devolver False tan pronto encontremos una letra prohibida; si llegamos al final
del bucle, devolvemos True. Debido a que una subrutina devuelve la última expresión
evaluada, no necesitamos usar la sentencia return True explícitamente en la última línea
de código más arriba. He usado esta característica como un ejemplo; podrías encontrarlo
más claro devolver los valores explícitamente, excepto quizás con subrutinas muy simples
de una línea.
Nota que tenemos dos bucles anidados implícitamente. Podríamos invertir los bucles ex-
terno e interno:
El código principal que llama la subrutina más arriba es similar al código que llama a sin-e
y podría lucir así:
144 Capítulo 8. Caso Práctico: Juego de Palabras
my $cuenta-total = 0;
my $cuenta-no-prohibidas = 0;
En lugar de una lista de letras prohibidas, tenemos una lista de letras disponibles. Si en-
contramos una letra en $palabra que no está en $disponible, podemos devolver False.
Y devolvemos True si llegamos al final del bucle.
En lugar de recorrer las letras en $palabra, el bucle recorre las letras requeridas. Si
cualquiera de las letras requeridas no aparece en la palabra, podemos devolver False.
Si estabas realmente pensando como un científico de la computación, habrías reconocido
que usa-todas era una instancia de un problema previamente resuelto en inverso: si la
palabra A usas todas las letras de la palabra B, entonces la palabra B usa solo las letras de
la palabra A. Así que podemos llamar la subrutina usa-solamente y escribir:
8.4. Búsqueda 145
while $i < $j {
return False if un-car($palabra, $i) ne
un-car($palabra, $j);
$i++;
$j--;
}
return True;
}
usando es-inversa de la Sección 7.10 ( pero deberías probablemente elegir la versión cor-
regida de la subrutina es-inversa dada en el apéndice: ver Subsección A.5.6).
ser incluidas, pero están incluidas) y dejar pasar otros errores (palabras que deberían ser
incluidas pero no están incluidas).
En general, las pruebas pueden ayudar a encontrar errores, pero no es fácil generar un buen
conjunto de pruebas de casos, y aún si lo haces, no puedes estar seguro que tu programa
es correcto.
8.6 Glosario
Objeto de archivo Un valor que representa un archivo abierto.
Caso especial Un caso de prueba que es atípico o que no es evidente (y con pocas posibil-
idades de ser manejado correctamente). Los términos edge case and corner case expre-
san más o menos la misma idea.
8.7 Ejercicios
Ejercicio 8.7. Este problema está basada en un Puzzler (acertijo) que fue transmitido en el pro-
grama radial Car Talk (http: // www. cartalk. com/ content/ puzzlers ):
Dame una palabra con tres letras dobles consecutivas. Te daré unas cuantas palabras
que casi califican, pero no completamente.Por ejemplo, la palabra committee, c-o-m-m-
i-t-t-e-e. Sería un gran ejemplo excepto por la “i“ que se escabulle ahí. O Mississippi:
M-i-s-s-i-s-s-i-p-p-i. Si pudieras sacar esas i, sería otro gran ejemplo. No obstante,
existe una palabra que tiene tres pares de letras consecutivas y a mi saber, esta puede
ser la única palabra que satisface dicha condición. ¡Solo bromeo! Existen probable-
mente alrededor de 500 de estas palabras pero solo puede pensar sobre una. ¿Cuál es la
palabra?
Solución: A.6.1.
Ejercicio 8.8. Aquí presentamos otro Puzzler deCar Talk (http: // www. cartalk. com/
content/ puzzlers ):
3 "Program testing can be used to show the presence of bugs, but never to show their absence!" — Edsger W.
Dijkstra
148 Capítulo 8. Caso Práctico: Juego de Palabras
“Estaba conduciendo en la autopista el otro día y sucede que noto mi odómetro. Como
la mayoría de odómetros, este muestra seis dígitos, en millas en números enteros sola-
mente. Así que, si mi carro tenía 300,000 millas, por ejemplo, yo vería 3-0-0-0-0-0.
Ahora bien, lo que ví ese día fue algo muy interesante. Noté que los últimos cuatro
dígitos eran palíndromos; es decir, se pueden leer igual de atrás hacia adelante. Por
ejemplo, 5-4-4-5 es un palíndromo, así que mi odómetro podría leerse 3-1-5-4-4-5.
Una milla más tarde, los últimos 5 números eran palíndromos. Por ejemplo, podría
leerse 3-6-5-4-5-6. Una milla después de esa, los 4 números del medio eran palíndromos.
¿Y estás listo para esto? ¡Otra milla más tarde, todos los 6 números eran palíndromos¡‘
“La pregunta es, qué había en el odómetro la primera vez que lo miré?”
Escribe un programa que chequee todos los números de seis dígitos e imprime cualquier número que
satisface estos requerimientos.
Solución: A.6.2.
Ejercicio 8.9. Otro Puzzler de Car Talk que puedes solucionar con una búsqueda (http: // www.
cartalk. com/ content/ puzzlers ):
Recientemente visité a mi madre y me dí cuenta que los dos dígitos que componen mi
edad al ser invertidos resultan en su edad. Por ejemplo, si ella tiene 73 años, entonces
yo tengo 37. Nos preguntamos que tan a menudo esto había ocurrido a través de los
años pero nos distraímos con otros temas y jamás obtuvimos una respuesta.
Cuando llegué a la casa fui capaz de deducir que los dígitos de nuestras edades habían
sido reversibles seis veces hasta ahora. También deducí que si tenemos suerte pasaría
nuevamente dentro de algunos años, y si somos bien suertudos ocurriría una vez más
después de eso. En otras palabras, pasaría 8 veces en total. Así que la pregunta es, cuál
es mi edad ahora?
Escribe un programa de Perl que busca las soluciones para este Puzzler. Pista: podrías encontrar el
método para el formateo de cadenas de texto sprintf muy útil.
Solución: A.6.3.
9
Arrays y Listas
Este capítulo presenta algunos de los tipos integrados de Perl más útiles, los arrays y las
listas.
> 3, 4, 5
(3 4 5)
> say (3, 4, 5).WHAT;
150 Capítulo 9. Arrays y Listas
(List)
say $_ for 1, 2, 3;
1
2
3
No necesitas usar paréntesis para crear una lista, pero son usualmente útiles para delimitar
los valores, i.e., para estipular dónde comienza y dónde termina, y en algunos casos, anular
la precedencia.
estamos básicamente creando y usando una lista de números enteros (desde el punto de
vista de la jerarquía de tipos en Perl; esta observación no es totalmente correcta para el
segundo ejemplo, dado que 1..10 es del tipo Range y es transformado en el tipo Seq. Sin
embargo, esta aproximación es suficientemente buena para nuestros propósitos aquí).
Los arrays son variables cuyos nombres comienzan con el sigilo @. Los arrays con nombres
necesitan ser declarados antes de ser usados, como cualquier otra variable que hemos visto
hasta ahora (excepto la variable tópico, $_). Una de las formas más fácil de crear un array
es asignar una lista a una variable:
> my @dígitos_impares = 1, 3, 5, 7, 9;
[1 3 5 7 9]
> say @dígitos_impares.WHAT;
(Array)
> my @números_solo_dígito = 0..9;
[0 1 2 3 4 5 6 7 8 9]
En el REPL de Perl, un array se muestra entre corchetes ([ y ]), mientras que las listas se
muestran entre paréntesis.
Si los elementos no contienen caracteres de espacio, es bien útil construir una lista (y asig-
narla a cualquier array si es necesario) con el operador quote-word <...>:
La ventaja de este método es que no hay necesidad de separar los elementos con comas y
ni tampoco hay necesidad de insertar comillas cuando los elementos son cadenas de texto.
Básicamente, el operador quote-word descompone su contenido por espacios en blanco y
devuelve una lista de palabras, la cual puede ser entonces usada en un bucle o asignada a
un array como en el ejemplo anterior.
9.2. Los Arrays Son Mutables 151
La mayoría de este capítulo será dedicado a los arrays. No obstante, ten presente que
muchas de las funciones y operadores de los arrays que estudiaremos aquí también fun-
cionan con listas (por lo menos todas aquellas que no violan la propiedad de inmutabilidad
de las listas).
Los elementos de un array (o una lista) no necesitan ser del mismo tipo:
> my @array-heterogeneo = 1, 2.3, pi, "str", (1, 2, 4);
[1 2.3 3.14159265358979 str (1 2 4)]
Aquí, el array está compuesto por un entero, un racional, un número con coma flotante
(tipo Num), una cadena de texto, y una lista de tres enteros. Aunque esto podría no ser
recomendado por la sanidad mental del programador quien tendría que usar un array con
elementos tan heterogéneos, Perl no se queja: al final, depende de ti hacer sentido de tus
datos.
El array anterior hasta contiene una lista de artículos. Si iteras sobre los elementos de este
array, por ejemplo con el bucle for, esta lista terminará como un elemento distinto; este
elemento no será “aplanado“ como tres elementos del array. Similarmente, elems es un
método que cuenta el número de artículos de un array (o de una lista). Al usarlo en el
array anterior produce el siguiente resultado:
> say @array-heterogeneo.elems;
5
Como puedes observar, la lista (1, 2, 4) cuenta como un solo elemento en el array.
Una lista dentro de otra es una lista anidada.
Un array que carece de elementos se conoce como un array vacío; puedes crear un array
vacío con paréntesis vacíos, ():
> my @vacío = ();
[]
Este código está realmente asignando una lista vacía al array. Pero esta sintaxis no es nor-
malmente necesaria para crear un nuevo array vacío, dado que solo con declarar un array
sin definirlo tiene el mismo efecto:
> my @vacío;
[]
Así que el uso de paréntesis vacíos (i.e., en la asignación de una lista vacía) sería necesario
para restaurar un array existente como un array vacío.
También puedes usar rangos o listas de índices para acceder partes (o rebanadas) de un
array o una lista:
> my @dígitos-pares = 0, 2, 4, 6, 8;
[0 2 4 6 8]
> my @pequeños-dígitos_pares = @dígitos-pares[0..2];
[0 2 4]
> my @min-max-dígitos-pares = @dígitos-pares[0, 4]
[0 8]
Si necesitas rebanadas en el orden opuesto, puedes usar la función reverse para revertir el
rango:
A diferencia de las listas, los arrays son mutables. Cuando el operador de corchetes aparece
después de un array en el lado izquierdo de una asignación, el operador identifica el ele-
mento del array que será asignado:
> my @impares = 1, 3, 5;
[1 3 5]
> @impares[3] = 7;
7
> say @impares;
[1 3 5 7]
El método end devuelve el resultado del método elems menos uno porque, dado que los
índices comienzan en la posición 0, el índice del último elemento es igual al número de
elementos menos uno.
La función o método unique devuelve una secuencia de elementos únicos en la lista de
entrada o el array ( i.e., el método devuelve la lista original si no hay valores duplicados):
Si sabes que la entrada está ordenada (y que, por lo tanto, los duplicados son adyacentes),
en vez de usar la función unique, usa la función squish debido a que termina siendo más
eficiente. La función squish remueve elementos duplicados y adyacentes.
Para saber si dos arrays son idénticos (estructuralmente los mismos, con el mismo tipo y los
mismos valores), usa el operador de equivalencia eqv. Para saber si los arrays contienen los
mismos elementos, usa el operador de coincidencia inteligente ~~. Entre dos arrays o listas,
el operador de equidad numérica == devolverá True si los arrays tienen el mismo número
de elementos y de lo contrario, False. Esto se debe a que == coacciona sus argumentos a
tipo numérico, así que compara el número de elementos:
> my @pares1 = 0, 2, 4, 6, 8;
[0 2 4 6 8]
> my @pares2 = reverse 8, 6, 4, 2, 0;
[0 2 4 6 8]
> say @pares1 eqv @pares2; # mismos elementos, misma estrutura
True
> say <1 2 3 4 5> eqv 1..5; # mismos elementos, estructura diferente
False
> say <1 2 3 4 5> ~~ 1..5; # mismos elementos, True
True
> my @array = 1..5;
[1 2 3 4 5]
> say <1 2 3 4 5> ~~ @array; # mismos elementos, True
True
> say <1 2 3 4 6> ~~ @array; # no los mismos elementos
False
> say <1 2 3 4 5> == <5 6 7 8 9>; # compara el número de elementos
True
La sentencia <1 2 3 4 5> eqv 1..5 devuelve False, debido a que aunque contienen los
mismos elementos, los argumentos son estructuralmente entidades diferentes (uno es una
lista y la otra es un rango).
Como podrías esperar por ahora, estas subrutinas también tienen una sintaxis de invo-
cación de método. Por ejemplo:
No obstante, debes tener presente que si usas las funciones push o unshift con un array
como argumento, obtendrás algo diferente a lo que podrías esperar:
Como puedes observar, cuando @agregar-array se agrega como una entidad al array
@números, @agregar-array se convierte en un elemento nuevo del array original. Si lo
que quieres es agregar los elementos de @agregar-array al array original, puedes usar el
método append en vez de push:
O puedes usar el operador prefijo “|“, el cual aplana el array añadido en una lista de
argumentos:
9.4. Pilas y Colas 155
También existe el método prepend que reemplaza a unshift para agregar elementos indi-
viduales de un array al principio de otro array existente ( en vez de añadir el array como
una sola identidad).
Una pila es una estructura de datos de tipo LIFO (del inglés Last In / First Out, “último en
entrar, primero en salir“). Puedes pensar sobre una pila como un grupo de platos apilados.
Cuando pones un plato limpio en la pila, usualmente lo pones en la parte superior; de igual
manera cuando tomas un plato, lo toma de la parte superior. Así que el primer plato que
tomas fue el último en ser añadido. Una pila en la ciencia de la computación implementa la
misma idea: usas una pila cuando la primera pieza de dato que necesitas de una estructura
de datos fue la última en ser añadida.
Por el contrario, una cola (o fila) es una estructura de datos de tipo FIFO ( del inglés First
In / First Out, “primero en entrar, primero en salir“). Esta es la idea de personas esperando
en línea para pagar en el supermercado. La primera persona en ser atendida es la primera
persona que entra en la fila.
Una pila puede ser implementada con un array y las funciones push y pop, las cuales agre-
gan un elemento (o varios) al final de un array y remueven un elemento del final del array
respectivamente. Esta es una implementación bien simple de una pila:
[1 2 3 4 5 6]
6
5
4
156 Capítulo 9. Arrays y Listas
Esta pila es simplista porque, por lo menos, una implementación más robusta debería hacer
algo sensible cuando tratas de tomar-desde-pila cuando la pila está vacía. También sería
sabio agregar signaturas a las subrutinas. En adición, podrías querer poner-en-pila más
de un elemento al mismo tiempo. Échale un vistazo a la solución sobre el ejercicio sobre
colas más abajo (Subsección A.7.1) para descifrar cómo esta pila podría ser mejorada.
Podrías obtener las mismas funciones usando las subrutinas unshift y shift en vez de
push y pop. Los elementos serán añadidos al principio del array y tomados desde el inicio,
pero todavía tendrás el comportamiento LIFO.
Como un ejercicio, trata de implementar una cola de tipo FIFO en el mismo modelo. Pista:
probablemente quieres usar un array y las funciones unshift y pop (o las funciones push y
shift). Solución: A.7.1.
Aquí, la sentencia splice removió tres elementos (6, 5, 4) y los reemplazó con los reempla-
zos en los argumentos (4, 5, 6). También devolvió los artículos removidos, los cuales fueron
almacenados en @dígitos_removidos. El número de reemplazos no necesita ser igual al
número de elementos removidos. En tal caso, el array crecerá o se reducirá. Por ejemplo,
si no se proveen reemplazos, entonces splice removerá y devolverá el número requerido
de elementos y el array se reducirá por el mismo número:
my @dígitos = 1..9;
my @dígitos_removidos = splice @dígitos, 3, 2;
say @dígitos_removidos; # -> [4 5]
say @dígitos; # -> [1 2 3 6 7 8 9]
Asumiendo que la función shift no existiera en Perl, tú podrías escribir una subrutina
mi-shift para simularla:
Podrías levantar una excepción si el array que se pasa a la función mi-shift está vacío.
Esto podría hacerse al modificar la subrutina de la siguiente manera:
La expresión @array > 0 evalúa a True si el número de elementos del array es mayor que 0,
i.e., básicamente, si no es un array vacío. Es equivalente a la expresión @array.elems > 0.
Como un ejercicio, escribe subrutinas usando splice para simular las funciones integradas
pop, unshift, push, y delete. Solución: A.7.2.
Esto funciona bien si solo necesitas leer los elementos de la lista. Pero si quieres escribir o
actualizar los elementos de un array, tú necesitas un bloque de doble punta. Por ejemplo,
podrías usar la función tc (“title case“ en inglés) para capitalizar la primera letra de cada
palabra de un array:
También puedes usar la sintaxis de un bucle for con la variable tópico $_. Por ejemplo, esto
usa la función uc (“upper case“ en inglés) para capitalizar cada palabra del array anterior:
for @colores-inglés {
$_ = $_.uc
}
say @colores-inglés; # -> [RED ORANGE YELLOW GREEN BLUE INDIGO VIOLET]
Algunas veces quieres recorrer un array y necesitas saber el índice de los elementos que
estás visitando. Una forma común de hacer esto es con el operador de rango .. para iterar
sobre los índices. Por ejemplo, para imprimir el índice y el valor de cada elemento de un
array:
Esto es útil, por ejemplo, para recorrer dos (o más) arrays en paralelo:
my @letras = 'a'..'e';
my @números = 1..5;
for [email protected] -> $idx {
say "@letras[$idx] -> @números[$idx]";
}
Esto imprimirá:
a -> 1
b -> 2
c -> 3
d -> 4
e -> 5
No necesitas especificar el rango de índices tú mismo, dado que la función keys devolverá
una lista de índices para un array o una lista:
Otra forma de iterar sobre los índices y valores de un array es con el uso de la función
o método kv (“keys values“-“llaves valores“) que devuelve el índice y el valor de cada
elemento del array:
my @letras = 'a'..'e';
say @letras.kv; # -> (0 a 1 b 2 c 3 d 4 e)
El bloque de doble punta con las variables de iteración hace posible procesar un índice y un
valor a cada paso del bucle. Por supuesto, puedes tener más de dos variables de iteración
si es necesario.
my @letras = 'a'..'e';
for @letras {
say $^a-letra;
}
my @letras = 'a'..'e';
for @letras.kv {
say "$^a -> $^b";
}
Esto imprimirá:
0 -> a
1 -> b
2 -> c
3 -> d
4 -> e
Estos marcadores no se usan muy a menudo para recorrer los arrays, pero veremos más
adelante lo útiles que son en casos donde no sería muy práctico tener que declarar los
parámetros.
La segunda construcción de repetición que quiero introducir aquí usa la palabra clave loop
y es similar al bucle for de stilo C (i.e., el bucle del lenguaje de programación C). En este
tipo de bucle, tú declaras entre una pareja de paréntesis tres expresiones separadas por
puntos y comas: el valor inicial de la variable de iteración, la condición por la cual el bucle
debería terminar, y el cambio que se hace a la variable de iteración en cada iteración:
Para los bucles más comunes, el bucle for visto anteriormente es más fácil de escribir y
usualmente más eficiente que esta construcción. Esta construcción especial loop debería
ser probablemente usada solo cuando la salida de una condición o el cambio que se hace
a la variable de iteración es bien inusual y sería algo difícil de expresar en un bucle for
regular. Como una excepción, la construcción loop sin la especificación de tres partes es
bien común y hasta idiomática para hacer un bucle infinito:
loop {
# haz algo
# termina si ...
}
La variable $total es inicializada a 0. Cada vez que el bucle itera, la variable $x consigue
uno de los elementos de la lista y es agregada a $total. Mientra el bucle se ejecuta, $total
acumula la suma de los elementos; una variable que es usada de esta forma se conoce como
un acumulador.
Una operación que combina una secuencia de elementos en un solo valor es usualmente
llamada una operación de reducción porque su efecto es reducir todos los artículos a un
solo elemento (esto es conocido como “pliegues“ (del inglés folding) en otros lenguajes de
programación). Estas ideas son derivadas de lenguajes de programación funcionales tal
como LISP (cuyo nombre deriva de “LISt Processing“ (procesamiento de listas)).
Perl 6 tiene una función reduce, la cual genera un solo valor "combinado" de la lista de
valores. Esto se lleva a cabo al aplicar de forma iterativa a cada artículo una función que
sabe cómo combinar dos valores. El uso de la función reduce para calcular la suma de los
primeros diez números luce así:
¿Recuerdas la función factorial de la Sección 4.10? Esta función usaba un bucle for para
calcular el producto de los primeros enteros hasta un cierto límite. Podría ser escrita de
nuevo usando la función reduce:
sub factorial( Int $num ) {
return reduce { $^a * $^b }, 1..$num;
}
say factorial 10; # -> 3628800
De hecho, el código para calcular el factorial es tan corto con la función reduce que se
puede argumentar se ha vuelto innecesario escribir una subrutina para eso. Podrías solo
poner el código “en línea“:
my $fact10 = reduce { $^a * $^b }, 1..10; # -> 3628800
Podemos hacer muchísimas cosas poderosas con esto, pero regresaremos a esto más tarde,
debido a que requiere algunas características sintácticas que no hemos visto todavía.
La variable @resultado es declarado como un array vacío; cada vez que el bucle itera,
agregamos el siguiente el siguiente elemento. Así que @resultado es otro tipo de acumu-
lador.
Una operación como mayúsculas es algunas veces conocida como un map porque “mapea“
(asocia) una función (en este caso el método uc) a cada elemento en una secuencia.
Perl tiene una función map que hace posible todo eso en solo una sentencia:
Aquí la función map aplica el método uc a cada elemento del array @palabras_minus y los
devuelve en el array @palabras_mayus. Más precisamente, la función map asigna de forma
iterativa cada elemento del array @palabras_minus a la variable tópico $_, aplica el bloque
de código después de la palabra clave map a $_ para crear nuevos valores, y devuelve una
lista de estos valores.
Para generar una lista de números pares entre 1 y 10, podríamos usar el operador de rango
para generar números entre 1 y 5 y usar map para multiplicarlos por dos:
En vez de usar la variable tópico $_, podríamos también usar la sintaxis del bloque punti-
agudo con una variable de iteración explícita:
En vez de un bloque de código, el primer argumento para map puede ser una referencia de
código (una referencia de una subrutina):
El nombre de la subrutina necesita ser precedido con el sigilo & para clarificar que es un
parámetro para map y no una llamada directa de la subrutina.
Si el nombre del array a la izquierda y a la derecha de la asignación es el mismo, entonces
la modificación es hecha “en lugar,“ i.e., lo cual aparece ser como si el array original es
modificado en el proceso.
Esta es una función inmensamente poderosa y expresiva; regresaremos a ella más tarde.
El nombre de la función integrada grep usada para filtrar algunas entradas proviene del
mundo de Unix, donde es una utilidad que filtra las líneas de un archivo de texto que
coinciden con un patrón dado.
En el código de arriba, todas las cadenas de texto de @entrada serán comparadas con-
tra el bloque grep, y aquellas que coinciden con el regex serán almacenadas en el array
@filtradas. Al igual que map, la función grep asigna, de forma iterativa, cada elemento
del array @entrada a la variable tópico $_, aplica el bloque de código después de la palabra
clave grep a $_, y devuelve una lista de los valores para los cuales el bloque de código
evalúa como verdadero. Aquí, el bloque de código es un simple regex que se aplica a la
variable $_.
Al igual que map, podríamos haber usado una referencia de función como el primer argu-
mento para grep:
9.8. Asociaciones, Filtros y Reducciones 165
Para generar una lista de números pares entre 1 y 10, podríamos usar el operador de rango
para generar números entre 1 y 10 y usar la función grep para filtrar los números impares:
Como un ejercicio, escribe un programa usando la función map para producir una array
que contenga el cuadrado de los números de la lista de entrada y un programa que use
grep para mantener solo los números de la lista de entrada que son cuadrados perfectos.
Solución: A.7.3.
Muchas de las operaciones de listas pueden ser expresadas como una combinación de map,
grep, y reduce.
Las funciones reduce, map, y grep son usualmente conocidas como funciones de orden
superior porque ellas no solo manipulan datos, sino que también manipulan otras fun-
ciones. Se puede pensar de estas funciones como funciones genéricas abstractas—ellas
realizan una operación puramente técnica: procesan los elementos de una lista y aplican a
cada de ellos un comportamiento definido en el bloque de código o la función del primer
parámetro.
Esto es inmensamente útil y puede darte un increíble poder expresivo para resolver ciertos
tipos de problemas. Pero otros tipos de problemas podrían resolverse mejor con el modelo
tradicional procedimental o imperativo, mientras que otros problemas se pueden benefi-
ciar de una perspectiva orientada a objetos. Perl 6 te deja elegir el modelo de programación
que quieras usar, y hasta hace posible la combinación de varios paradigmas en el mismo
programa.
Del mismo modo, puedes declara el tamaño de un array Los arrays con un tamaño fijo son
conocidos como arrays moldeados (del inglés “shaped arrays“). Hay doce meses en un año,
así que podrías decirle a Perl que tu array @meses nunca tendrá más de doce elementos:
Aquí, Perl destinó 12 “espacios“ para el array, aunque los último cinco están actualmente
indefinidos. Perl no necesita destinar memoria cuando definas el décimo artículo del ar-
ray. Y Perl te dejará saber tu equivocación cuando accidentalmente trates de acceder un
elemento que está fuera de rango.
La definición del tipo de elementos y el tamaño máximo del array puede conducir a un
mayor rendimiento en términos de la velocidad de ejecución (al menos para algunas op-
eraciones) y reducir significativamente el consumo de memoria por el programa, especial-
mente cuando se manipulan los arrays grandes.
Por ejemplo, podrías usar un array de dos dimensiones para almacenar una lista de em-
pleados con sus respectivos salarios:
> my @empleados;
[]
> @empleados[0;0] = "Liz";
Liz
> @empleados[0;1] = 3000;
3000
> @empleados[1] = ["Bob"; 2500];
[Bob 2500]
> @empleados[2] = ["Jack"; 2000];
[Jack 2000]
> @empleados[3] = ["Betty"; 1800];
[Betty 1800]
> say @empleados[1;1];
2500
> say @empleados[2];
[Jack 2000]
> say @empleados;
[[Liz 3000] [Bob 2500] [Jack 2000] [Betty 1800]]
Es posible tener más de dos dimensiones. Por ejemplo, podríamos tener un array con tres
dimensiones para almacenar las temperaturas de un reactor químico, medidas en varios
lugares identificados por sus coordinadas x, y y z:
my @temp;
@temp[1;3;4] = 80;
Sin embargo, para este tipo de datos, es mejor usar la estructura de datos que cubriremos
en el siguiente capítulo, los hashes.
Los arrays multidimensionales pueden también tener un tamaño fijo. Por ejemplo, esta
puede ser la declaración de un array de dos dimensiones donde la primera dimensión es
el mes del año y la segunda es el número de días del mes:
my @fecha[12, 31];
Existen varios tipos de ordenamiento. Los más comunes son los ordenamientos numérico
y lexicográfico (o alfabético). Ellos difieren en la manera en que compara los elementos
individuales a ser ordenados.
168 Capítulo 9. Arrays y Listas
El ordenamiento numérico es muy diferente: el valor de interés es el valor total del número.
Por ejemplo, si estamos ordenando números enteros, 11 es es mayor que 9 porque tiene
más dígitos. Sin embargo, el ordenamiento alfabético de 9 y 11 considera a 11 menor que
9, porque el primer dígito es menor.
(1 11 2 4 5 6 9)
Hasta con tipos de datos mezclados, sort puede hacer un gran trabajo al proveer un resul-
tado que puede ser lo que esperas:
Existen otros casos donde el simple uso de la función sort fallará y no devolverá lo que
probablemente esperas:
say sort <a ab abc A bc BAC AC>; # -> (A AC BAC a ab abc bc)
En este caso, sort pone todas las cadenas de texto que comienzan con una letra mayúscula
antes que cualquier cadena de texto que comienza con una letra minúscula. Probablemente
no deseas esto. Esto luce peor si las cadenas de texto usan caracteres ASCII extendidos:
La razón detrás de esto es que, al ordenar cadenas de texto, la función sort usa la codi-
ficación numérica interna de las letras. Esto fue llamado alguna vez orden "ASCIIbetical"
(en contraste con orden alfabético), pero el término es ahora muy limitado y algo obsoleto,
porque Perl 6 usa Unicode y no ASCII.
Claramente, hay casos donde más técnicas avanzadas de ordenamiento son necesarias.
9.12. Más Técnicas Avanzadas de Ordenamiento 169
es equivalente a esto:
El bloque de código usado aquí como el primer argumento de la función sort usa los
parámetros marcadores (parámetros auto-declarados de posición) los cuales vimos anteri-
ormente en este capítulo. El operador cpm recibe dos argumentos y le pasa a la función sort
información sobre cuál de los artículos debería aparecer primero en el orden resultante.
Si quieres ordenar en el orden inverso, podrías solo intercambiar el orden de los parámetros
marcadores:
Ten presente que discutimos este ejemplo solo con el propósito de explicar algunas fun-
ciones de los parámetros marcadores. Para ordenar el array que hemos presentado aquí en
orden descendente, sería más fácil obtener el mismo resultado con el siguiente código:
La razón por la cual sort hace un trabajo tan grandioso hasta con una mezcla de cadenas
de texto y números es porque la función de comparación por defecto, cmp, es muy as-
tuta y puede adivinar si debería realizar una comparación de orden lexicográfico u orden
numérico al examinar sus argumentos.
170 Capítulo 9. Arrays y Listas
o esta manera:
Cuando el bloque de código de comparación recibe sus dos argumentos, el método lc los
convierte en letras minúsculas antes de realizar la comparación. Nota qu esto no tiene
ningún impacto en el caso de la salida de texto, dado que la transformación a letra minús-
cula es local al bloque de código de comparación y no tiene impacto alguno en los datos
manejados por sort. Veremos en un rato que hay una manera más fácil y eficiente de
realizar tal transformación antes de comparar los argumentos.
Si la especificación de la comparación es muy complicada, podría ser necesario escribirla
en una subrutina separada y dejar que sort llame a esa subrutina. Supón que tenemos
una lista de cadenas de texto que están todas formadas por dígitos iniciales, los cuales son
seguidos por un grupo de letras y posiblemente seguidos por otros caracteres irrelevantes,
y queremos ordenar las cadenas de texto de acuerdo al grupo de letras que siguen los
dígitos.
Comencemos con escribir la subrutina de comparación:
Nada complicada: la subrutina toma dos argumentos, usa una regex para extraer el grupo
de palabras de cada argumento, y devuelve el resultado de las cadenas extraídas a la fun-
ción cmp. En el mundo real, se necesitaría hacer algo si la extracción falla, pero, para nuestro
propósito, asumiremos que esto no sucederá.
El ordenamiento es ahora bastante directo, solo necesitamos pasar a la función sort la
función anterior:
say sort &mi_comp, < 22ac 34bd 56aa3 12c; 4abc( 1ca 45bc >;
# -> (56aa3 4abc( 22ac 45bc 34bd 12c; 1ca)
Solo necesitamos preceder la subrutina de comparación con el sigilo “&“ y funcionará bien:
las cadenas de texto son ordenadas de acuerdo a los grupos de letras que siguen los dígitos
al inicio de cada cadena de texto.
9.13. Depuración de Programas 171
En este ejemplo, debido a que el bloque de código de comparación toma un solo argu-
mento, su tarea es transformar cada uno de los artículos a ser comparados antes de aplicar
la función cmp a los argumentos. Esto no solo las cosas más simples, sino que también
es probablemente más eficaz, especialmente si el número de artículos a ser ordenados es
grande y si la subrutina de transformación es relativamente costosa (en término de proce-
samiento): los valores transformados son actualmente almacenados en caché (i.e., alma-
cenados en la memoria para uso repetido), de tal manera que la transformación se hace
solamente una vez por cada artículo, a pesar de que la subrutina de comparación se llama
muchas veces por cada artículo en un ordenamiento.
De igual manera, podríamos ordenar los números de acuerdo a sus valores absolutos:
say sort { $_.abs }, <4 -2 5 3 -12 42 8 -7>; # -> (-2 3 4 5 -7 8 -12 42)
El ejemplo “más complejo“ con dígitos y letras que requiere una subrutina separada aplica
la misma transformación a ambos argumentos. Como un ejercicio, escribe un simple pro-
grama de ordenamiento que usa una subrutina de transformación y el operador de com-
paración por defecto cmp sobre los artículos transformados. Solución: A.7.4.
Por ejemplo, si tomamos una vez más el ejemplo de las cadenas de texto mezcladas con
letras mayúsculas y minúsculas, podemos lograr un ordenamiento alfabético que no es
sensible a la diferencia entre letras mayúsculas y minúsculas en una nueva forma:
Sobra decir que los usos avanzados de la función sort presentada en esta sección son
solo más ejemplos del estilo de programación funcional. Las subrutinas de comparación
y las subrutinas de transformación pueden pasarse como argumentos a la función sort,
y generalmente, todas las funciones, las subrutinas, y los bloques de códigos usados aquí
son funciones de orden superior o funciones de primera clase.
1. Algunas de las funciones y métodos integrados de los arrays modifican sus argumen-
tos mientras otras no.
Puede ser tentativo escribir código así:
@array = splice @array, 1, 2, $nuevo-elem; # INCORRECTO!
La función splice devuelve los elementos que ha eliminado del array, no el array en
sí mismo, el cual es modificado “en el lugar“.
Antes de usar los métodos y operadores de los arrays, deberías leer la documentación
cuidadosamente y quizás probarlos en modo interactivo.
Al recorrer un array, por ejemplo con for o map, la variable tópico $_ es un apodo
para los elementos sucesivos del array, y no una copia de ellos. Esto significa que
si cambias a $_, el cambio sera reflejado en el array. Pueden haber casos donde este
comportamiento es deseable y otros casos donde no importa lo que suceda con ellos
(porque ya no necesitas el array original), pero esta técnica es propensa a errores y
quizás debería evitarse (o por lo menos usarla con precaución).
my @números = <1 2 3>;
push @dobles, $_*=2 for @números; # INCORRECTO (probablemente)
say @números; # -> [2 4 6]
El error aquí es que la sentencia $_*=2 está modificando a $_, así que el array
@números es también modificado, mientras que la intención era ciertamente poblar
los nuevos números en el array @dobles, no modificar a @números.
El mismo código aplica a una lista literal en vez de un array lo cual conduce a un
error al tiempo de ejecución, debido a que una lista es inmutable:
> push @dobles, $_*=2 for <1 2 3>; # INCORRECTO (definitivamente)
Cannot assign to an immutable value
La solución en este caso es bien fácil y consiste de usar una expresión que no modi-
fique a $_ pero devuelva el valor deseado:
push @dobles, $_ * 2 for @números; # OK
Lo mismo aplica a map:
my @números = <1 2 3>;
say map { ++$_}, @números; # INCORRECTO (probablemente)
say @números; # -> [2 3 4]
Otra vez, el uso de una expresión que no modifique a $_ pero que en cambio, de-
vuelva el nuevo valor deseado arreglará el problema:
my @números = <1 2 3>;
say map { $_ + 1}, @números; # -> (2 3 4)
say @números; # -> [1 2 3]
9.14 Glosario
Lista Una secuencia inmutable de valores.
Elemento Uno de los valores en una lista o array ( o cualquier otra secuencia). También
llamado artículos.
9.15. Ejercicios 173
Acumulador Una variable que se usa en un bucle para agregar o acumular un resultado.
Reducir Un patrón de procesamiento que recorre una secuencia y acumula los elementos
en un solo resultado.
Map Un patrón de procesamiento que recorre una secuencia y realiza una operación sobre
cada elemento. También el nombre de una función integrada de Perl que realiza el
mismo patrón de procesamiento.
Filtro Un patrón de procesamiento que recorre una lista y selecciona los elementos que
satisfacen una condición. grep es una implementación de Perl de un filtro.
9.15 Ejercicios
Ejercicio 9.1. Escribe una subrutina llamada suma-anidada que toma un array de los arrays de
enteros y añade todos los elementos de los arrays anidados. Por ejemplo:
Solución: A.7.5.
Ejercicio 9.2. Escribe una subrutina llamada suma-acumula que toma una lista de números y
devuelve la suma acumulativa, es decir, una nueva lista donde la suma del elemento ith es la suma
de los primeros elementos i + 1 de la lista original. Por ejemplo:
Solución: A.7.6.
Ejercicio 9.3. Escribe una subrutina llamada medio que toma una lista y devuelve una lista nueva
que contiene todos los elementos excepto el primero y el último. Por ejemplo:
Solución: A.7.7.
Ejercicio 9.4. Escribe una subrutina llamada corta que toma un array, lo modifica al remover el
primer y último elemento y devuelve algo no útil. Por ejemplo:
my @nums = 1, 2, 3, 4;
corta(@nums);
say @nums; # -> [2, 3]
Solución: A.7.8.
174 Capítulo 9. Arrays y Listas
Ejercicio 9.5. Escribe una subrutina llamada está-ordenada que toma una lista o un array de
números como parámetro y devuelve True si la lista está ordenada en orden ascendente y False de
lo contrario. Por ejemplo:
Solución: A.7.9.
Ejercicio 9.6. Una palabra es un anagrama de otra si las dos tienen las mismas letras, con el mismo
número de apariciones, pero en un orden diferente. Escribe una subrutina llamada es-anagrama
que toma dos cadenas de texto y devuelve True si son anagramas.
Solución: A.7.10.
Ejercicio 9.7. Escribe una subrutina llamada tiene-duplicados que toma una lista o un array y
devuelve True si hay algún elemento que aparece más de una vez. La subrutina no debería modificar
la entrada original.
Solución: A.7.11.
Ejercicio 9.8. Este ejercicio es acerca de la Paradoja del Cumpleaños. Encuentra más in-
formación sobre esta paradoja aquí: https: // es. wikipedia. org/ wiki/ Paradoja_ del_
cumplea~nos .
Si hay 23 estudiantes en tu clase, cuál es la probabilidad de que dos estudiantes cumplan años el
mismo día? Puedes estimar esta probabilidad al generar muestras aleatorias de los 23 cumpleaños y
chequear por cumpleaños duplicados. Pista: puedes generar cumpleaños aleatorios con la funciones
rand y int.
Solución: A.7.12.
Ejercicio 9.9. Escribe una subrutina que lea el archivo words.txt y construye una lista con un
elemento por palabra. Escribe dos versiones de esta función, una que usa el método push y la otra
que usa la función unshift. ¿Cuál toma más tiempo en ejecutarse? ¿Por qué?
Solución: A.7.13.
Ejercicio 9.10. Para chequear si una palabra está en nuestra lista estándar de palabras, podrías
chequear cada elemento por turno, pero sería lento porque buscaría a través de las palabras en orden.
Si las palabras están en orden alfabético (el cual es el caso en nuestra lista de palabras), podemos
acelerar las cosas con una búsqueda binaria, la cual es similar a lo que haces cuando buscas una
palabra en el diccionario. Tú comienzas en alguna parte en el medio del diccionario y chequea si la
palabra que estás buscando se encuentra antes que la palabra en el medio de la lista. Si esto sucede,
busca en la primera mitad de la lista del mismo modo. Por lo contrario, busca en la segunda mitad
de la lista.
De cualquier manera, tú reduces el tiempo de búsqueda por la mitad. Si la lista de palabras contiene
113,809 palabras, tomará alrededor de 17 pasos para encontrar la palabra o concluir que la palabra
no se encuentra en la lista.
Escribe una función llamada búsqueda-binaria que toma una lista ordenada y un valor a encon-
trar en la lista. La función debe devolver si el valor se encuentra en la lista o no.
Solución: A.7.14
9.15. Ejercicios 175
Ejercicio 9.11. Dos palabras son una “pareja de inversas“ si cada una es la inversa de la otra.
Por ejemplo, “depot“ y “toped“ forman una pareja de inversas.; otro ejemplo incluye “reward“ y
“drawer“, o “desserts“ y “stressed“.
Escribe un programa que encuentra todas las parejas de inversas en el archivo word.txt.
Solución: A.7.15.
Ejercicio 9.12. Dos palabras se “entrelazan“ si al tomar letras alternadas de ambas palabras se
forma una nueva palabra. Por ejemplo, “shoe“ y “cold“ se entrelazan para formar la palabra
“schooled“.
Escribe un programa que encuentre todos los pares de palabras que se entrelazan en el archivo
word.txt. Pista: ¡no enumeres todas la parejas, hay demasiadas!
Solución: A.7.16
Crédito: este ejercicio fue inspirado por un ejemplo en http: // puzzlers. org .
176 Capítulo 9. Arrays y Listas
10
Hashes
Este capítulo presenta otro tipo integrado conocido como un hash. Los hashes son unas
de las mejores y más comunes características de Perl; ellos constituyen los componentes
básicos de muchos algoritmos eficientes y elegantes.
> my %ingAesp;
Esto crea un hash vacío. Para agregar artículos al hash, puedes usar las llaves:
Esta línea crea un artículo que relaciona la clave 'one' al valor 'uno'.
Si la clave es una cadena de texto que contiene una sola palabra (i.e., sin ningún espacio en
el medio), existe un atajo más idiomático para crear la misma entrada de hash:
Si imprimimos el hash, vemos un par clave-valor con el operador constructor de pares =>
entre la clave y el valor:
Este formato de salida es también un formato de entrada. Por ejemplo, puedes crear un
hash nuevo con tres artículos:
> my %ingAesp = ('one' => 'uno', 'two' => 'dos', 'three' => 'tres');
one => uno, three => tres, two => dos
El uso del operador constructor de pares => entre las claves y los valores no es requerido;
puedes usar comas tambíen:
Pero el constructor de pares tiene la ventaja de mostrar gráficamente las relaciones clave-
valor. El operador constructor de pares también hace que el uso de comillas no sea man-
datario en su lado izquierdo (provisto que la clave sea una cuerda de texto sin espacios):
> my %ingAesp = (one => 'uno', two => 'dos', three => 'tres');
one => uno, three => tres, two => dos
También podrías usar una sintaxis de lista más concisa para la asignación del hash y Perl
convertirá felizmente la lista en un hash, provisto que el número de artículos en la lista de
entrada sea par:
La clave two siempre mapea al valor `dos` así que el orden de los artículos no importa.
Si la clave no se encuentra en el hash, obtienes un valor indefinido:
10.1. Un Hash es un Mapeo 179
Dado que grep usa por defecto una coincidencia inteligente, esto puede hacerse de una
forma aún más concisa:
say "¡Encontrado!" if grep {'uno'}, %ingAesp.values; # -> ¡Encontrado!
Cuando se inspecciona los valores, el programa tiene que buscar los elementos de la lista
en orden (o en secuencia), como en la Sección 7.2.2. A medida que la lista se vuelve más
larga, el tiempo de búsqueda se extiende en proporción directa.
Por el contrario, cuando se inspecciona las claves, Perl usa un algoritmo de hashing que
tiene una propiedad interesante: toma el mismo monto de tiempo sin importar cuantos
artículos se encuentren en el hash. En otras palabras, funciona bastante rápido, comparado
con el tamaño de la lista, cuando la lista que se inspecciona es larga. Ésta es la razón por
la cual la solución al ejercicio de par reverso (Ejercicio 9.11) del capítulo anterior usando
un hash fue casi tres veces más rápido que la solución de búsqueda binaria (ver Subsec-
ción A.7.15).
Como un ejercicio, usa la muestra de los datos de empleados del array multidimensional de
la Sección 9.10 (p. 166), organízala en un hash, y busca algunos salarios. Pista: no necesitas
una estructura multidimensional para hacer eso con un hash. Solución: A.8.1
1 Evaluar el valor en un contexto Booleano también funcionaría con nuestro ejemplo, pero esto devolvería algo
erróneo cuando la clave existe, pero el valor no está definido o por lo contrario evalúa a un valor falso (por
ejemplo, si es igual a False, cero, o cadena de texto vacía).
180 Capítulo 10. Hashes
Para agregar un elemento a un hash, solo asigna el hash con una clave:
Recuerda que también puedes hacer lo mismo sin encerrar las claves en comillas si usas el
operador quote-word con las comillas angulares (si las claves son cadenas de texto):
Usar push para agregar un par a un hash no es exactamente lo mismo que hacer una asig-
nación de hash: si la clave ya existe, el valor antiguo no es reemplazado por el valor
nuevo—en lugar, ambos valores son colocados en un array (o si el valor antiguo se en-
cuentra ya en el array, entonces el valor nuevo se agrega al array):
Para chequear si un valor está definido para una clave dada, usa defined:
Nota que el adverbio :delete también devuelve el valor que ha sido removido.
Para interar sobre un hash, usa:
Por ejemplo:
> for %meses.kv -> $clave, $val { say "$clave => $val" }
jan => 1
abr => 4
mar => 3
jun => 6
may => 5
feb => 2
> say keys %meses;
(jan abr mar jun may feb)
> say values %meses;
(1 4 3 6 5 2)
> say %meses.pairs;
(jan => 1 abr => 4 mar => 3 jun => 6 may => 5 feb => 2)
• Podrías crear 26 variables, una por cada letra del alfabeto. Después podrías recor-
rer la cadena de texto y, por cada carácter, aumentar el contador correspondiente,
probablemente usando una condicional encadenada grande y fea de 26 partes.
• Podrías crear un array con 26 elementos. Después podría convertir cada carácter a
un número (usando la función integrada ord), usar el número como un índice en el
array, e incrementar el contador apropiado.
• Podrías crear un hash con caracteres como claves y contadores como los valores cor-
respondientes. La primera vez que ves un carácter, agregarías un artículo al hash.
Después de eso, aumentarías el valor de un artículo existente.
Cada una de estas opciones realiza la misma computación, pero cada una de ellas imple-
menta la computación en una manera distinta.
Una implementación es una manera de realizar una computación; algunas implementa-
ciones son mejores que otras. Por ejemplo, una ventaja de la implementación con el hash
es que no tenemos que saber con antelación cuales letras aparecen en la cadena de texto y
solo tenemos que crear espacio para las letras que sí aparecen.
Aquí se muestra como el código podría lucir:
182 Capítulo 10. Hashes
El histograma indica que las letras `W` y `b` aparecen solo una vez; `a` y `i` aparecen
tres veces, `e` aparece cuatro veces, etc.
Hemos nombrado la variable de iteración $par para resaltar más claramente que el pro-
grama está iterando sobre los pares clave-valor (actualmente objetos Pair). Puedes usar
los métodos key y value (nota que están en singular) para acceder la clave y valor de un
Pair. Por ejemplo, para revertir el orden en el cual cada línea es imprimida:
> for %ingAesp -> $par { say $par.value ~ " <= " ~ $par.key; }
dos <= two
tres <= three
uno <= one
Otra vez, las claves no están en un orden particular. Para recorrer las claves ordenada-
mente, puedes usar las funciones o métodos keys (en plural) y sort:
Esta subrutina es otro ejemplo del patrón de búsqueda. Si llegamos al final del bucle,
eso significa que $val no aparece en el hash como un valor, así que devolvemos un valor
indefinido (Nil). Aquí, la responsabilidad para reaccionar a tal situación se deja a la función
que hace la llamada. Una alternativa es levantar una excepción, la cual tendría que ser
todavía la responsabilidad de la función que hace la llamada. Sin embargo, dado que la
búsqueda directa con la clave no levanta una excepción pero simplemente devuelve un
valor indefinido cuando la clave no existe, hace sentido que busqueda-inversa tenga el
mismo comportamiento cuando el valor no se encuentra.
Este es un ejemplo de una búsqueda inversa exitosa:
Otra forma más concisa de hacer la búsqueda inversa sería usar grep para extraer una lista
de valores que satisfagan nuestra condición:
Otra opción es usar una expresión con la función integrada first para extraer solo el
primer par:
my %histo = histograma('parrot');
say %histo.pairs.first: *.value == 1; # -> p => 1
184 Capítulo 10. Hashes
Este último ejemplo usa el parámetro whatever “*“ el cual no hemos discutido en este libro
todavía. Digamos que, aquí, el “*“ quiere decir devuelve sucesivamente el primer par que
coincide con la condición del valor para cada par del hash y la función first (ver Sección ??
para detalles sobre el parámetro “*“).
Una búsqueda inversa es mucha más lenta que una búsqueda directa; si tienes que hacerla
constantemente, o si el hash se vuelve grande, el rendimiento de tu programa sufrirá.
> my @lista;
[]
> push @list, 50.rand.Int for 1..10;
> say @lista;
[12 25 47 10 19 20 25 42 33 20]
Podemos usar un hash para rechazar cualquier entero aleatorio generado que ya se haya
visto. La siguiente manera sería una forma de escribir esto en código:
> my @lista;
[]
> push @list, 50.rand.Int for 1..10;
> say @lista;
[12 25 47 10 19 20 25 42 33 20]
Podemos usar un hash para rechazar cualquier entero aleatorio generado que ya se haya
visto. La siguiente manera sería una forma de escribir esto en código:
my @lista;
my %visto;
while @lista.elems < 10 {
my $aleatorio = 50.rand.Int;
next if %visto{$aleatorio}:exists;
%visto{$aleatorio} = 1;
push @lista, $aleatorio;
}
say @lista;
Cada valor entero generado se agrega al hash %visto y la lista de salida. Pero antes de
hacer eso, el entero generado se chequea con el hash %visto para verificar que no se ha
visto todavía. Cuando el programa finaliza su ejecución, la lista contiene 10 enteros únicos
y (pseudo)aleatorios.
Lo hicimos paso a paso y mantuvimos dos estructuras de datos separadas, el array de
salida @lista y el hash %visto, para hacer el proceso tan claro como sea posible. Pero si lo
piensas, @lista y %visto tienen esencialmente el mismo contenido a cada paso a través del
bucle. Realmente no tenemos que mantener un registro de los mismos datos en dos lugares.
Dado que tener un hash es importante para chequear que los valores de salida son únicos,
podemos deshacernos de @lista y escribir una versión más concisa y probablemente más
idiomática del mismo programa:
my %visto;
while %visto.elems < 10 {
my $aleatorio = 50.rand.Int;
push %visto, ($aleatorio => 1) unless %visto{$aleatorio}:exists;
}
say keys %visto; # -> (39 12 46 27 14 21 4 38 25 47)
Esto puede simplificarse aún más. No es realmente necesario chequear si el entero gener-
ado existe en el hash: si existe, el elemento antiguo del hash será reemplazado con el nuevo,
y el hash esencialmente no cambiará. Además, cuando se evalúa en un contexto numérico
escalar, un hash devuelve el número de sus elementos, así que la invocación .elems no es
necesaria. Esta es la nueva versión:
my %visto;
%visto{50.rand.Int} = 1 while %visto < 10;
say keys %visto; # -> (46 19 5 36 33 1 20 45 47 30)
Esta última versión es probable más concisa y más idiomática, pero esto significa que es
mejor. Es perfectamente aceptable si prefieres la segunda o la primera versión porque la en-
cuentras más clara. Usa la versión que desees, o tu propia versión modificada provisto que
realice la tarea prevista. Esto es Perl, hay más de una forma para hacer algo (TIMTOWTDI).
Sin embargo, nota que la versión pura de hash no mantiene el orden en el cual los números
son generados, así que la (pseudo)aleatoriedad podría no ser tan buena.
También nota que Perl tiene una función o método pick para elegir elementos de forma
aleatorio de una lista sin repetición.
186 Capítulo 10. Hashes
> my %amigos = (Gabo => 5, Miguel => 6, Salomé => 5, Gabo => 7, Jorge => 3)
Miguel => 6, Jorge => 3, Salomé => 5, Gabo => 7
Dado que dos de nuestros amigos se llaman “Gabo“, perdemos los datos asociados con el
primero. Esto es algo con lo cual deberías ser cuidadoso: las claves de un hash son únicas,
así que perderás algunos artículos si los datos asociados con tus claves tienen duplicados.
La siguiente sección mostrará algunas maneras de tratar este posible problema.
Pero esta propiedad sobre la singularidad de una clave tiene muchos aspectos positivos.
Por ejemplo, una manera típica de remover los duplicados de una lista de artículos es
asignar los artículos de una lista a las claves de un hash (el valor no importa); al final del
proceso, la lista de claves no contiene duplicados:
Como puedes ver, los duplicados han sido removidos del array de salida. En ejemplos
simples como este, la función integrada unique habría sido suficiente para remover los
duplicados de @array, pero dentro de un programa más complejo, es bien común usar un
hash (usualmente llamado %visto) para chequear si un valor ya se ha visto.
> my %meses = ene => 1, feb => 2, mar => 3, abr => 4, may => 5;
abr => 4, feb => 2, ene => 1, mar => 3, may => 5
Podemos transformar los pares clave-valor en un lista plana, invertir la lista, y asignar la
lista inversa a un nuevo hash:
Ahora tenemos un nuevo hash que mapea los números de los meses a sus nombres. Esto
puede ser útil si se conoce que un hash es biyectivo, pero esta estrategia no funciona cor-
rectamente si un valor aparece más de una vez: en tal caso, algunos pares se perderán:
10.8. Los Hashes y los Arrays 187
> my %meses = ene => 1, enero => 1, febrero => 2, febrero => 2;
feb => 2, febrero => 2, ene => 1, enero => 1
> my %meses_inv = %meses.kv.reverse;
1 => enero, 2 => febrero
Los arrays pueden aparecer como valores en un hash. Por ejemplo, si te dan un hash que
mapea las letras a las frecuencias, podrías querer invertirlo; es decir, crear un hash que
mapea las frecuencias a las letras. Dado que podría haber varias letras con las misma
frecuencia, cada valor en el hash invertido debería ser un array de letras.
La siguiente es una función que invierte un hash similar:
sub invertir-hash( %hash-entrada ) {
my %hash-salida;
for %hash-entrada.kv -> $clave, $val {
push %hash-salida{$val}, $clave;
}
return %hash-salida;
}
Cada paso a través del bucle, un artículo del hash se asigna a las variables $clave y $val, y
$clave se añade al valor %hash-salida para la clave $val; si dicho valor no existe todavía,
entonces es creado. Al final del proceso, los valores de %hash-salida son todos arrays
anónimos.
Este es un ejemplo:
my %rev-hist = invertir-hash histograma 'parrot';
say %rev-hist;
dd %rev-hist;
Esto mostrará:
1 => [p a o t], 2 => [r]
Hash %rev-hist = {"1" => $["p", "a", "o", "t"], "2" => $["r"]}
Nota que la función say provee una representación simple de los datos del hash, y que
la nueva función dd (abreviatura de “data dumper“ – “vertedero de datos“) usada en este
ejemplo provee información más detallada. dd no es comúnmente usada en programas
normales, pero puede ser útil durante la depuración de un programa para mostrar una
descripción detallada de una estructura de datos compleja.2
Por ejemplo, %hash-salida contiene dos artículos (dos pares) cuyos valores son arrays
anónimos. Puedes acceder el segundo elemento del primer array usando el valor del hash
%rev-hist{"1"} como si fuera un nombre de array ordinario, con esta simple sintaxis:
say %rev-hist{"1"}; # -> a
La figura 10.1 es un estado de diagrama que muestra a %hist y a %rev-hist. Un hash se
representa como una caja con el tipo hash encima y los pares clave-valor dentro.
Los arrays pueden ser valores en un hash, como este ejemplo muestra, pero no pueden
ser claves. Si intentas hacerlo, probablemente termines con una clave que contiene solo un
elemento del array, pero no lo que esperabas:
2 Para ser honesto, dd no es Perl 6 estándar, es una característica específica de Rakudo. Una implementación
array
2 1 'r'
my @a = 'a' .. 'c';
my %h;
%h{@a} = 5;
say %h; # -> a => 5, b => (Any), c => (Any)
Aquí, Perl interpretó la asignación %h{@a} = 5; como una asignación de rebanada, i.e.,
asumió que intentamos poblar tres artículos de una vez, una para cada elemento del array.
Como se mencionó anteriormente, un hash se implementa usando una función hash y
significa que las llaves tienen que ser hashable3 . Una función hash es una función que
toma un valor (de cualquier tipo) y devuelve un número entero. Los hashes usan estos
enteros, llamados valores de hash, para almacenar y buscar pares clave-valor.
Este sistema funciona bien si las claves son inmutables. Pero si las claves son mutables,
como con los arrays, cosas extrañas pasarían. Por ejemplo, cuando creas un par clave-valor,
Perl aplicaría una función hash a la clave y la almacenaría en la ubicación correspondiente.
Si modificas la clave y después aplica la función hash nuevamente, entonces sería alma-
cenada en una ubicación diferente. En ese caso, podrías tener dos entradas para la misma
clave, o no podría ser capaz de encontrar una clave. En ambos casos, el hash no funcionaría
correctamente.
Esa es la razón por la cual las claves deben ser hashable, y porqué los tipos mutables como
los arrays no lo son. Así que Perl hará algo más que puede ser útil (tal como crear tres
artículos de hash distintos en el ejemplo anterior), pero no aplicará la función hash al array.
Dado que los hashes son mutables, ellos no pueden ser usados como claves, pero pueden
usarse como valores, así que puedes tener hashes anidados.
10.9 Memos
Si jugueteaste con la subrutina fibonacci en la Sección 5.8, probablemente notaste que
mientra más grande el argumento, más tiempo toma la subrutina para ejecutarse. Además,
el tiempo de ejecución aumenta extremadamente rápido.
3 Esto no es del todo cierto. Las claves de un hash “normal“ deben ser del tipo hashable y por lo tanto, inmuta-
bles. Hay otro tipo de hash, objetos hash, para los cuales la necesidad de tener claves inmutables no aplica.
10.9. Memos 189
fibonacci
$n 4
fibonacci fibonacci
$n 3 $n 2
fibonacci fibonacci
$n 1 $n 0
Para entender el porqué, considera la figura 10.2, la cual muestra la gráfica de llamada para
fibonacci con n=4.
Una gráfica de llamada muestra un conjunto de cuadros de una subrutina, con líneas que
conectan cada cuadro a los cuadros de las funciones que llamas. En la parte superior de la
gráfica, fibonacci con $n=4 llama a fibonacci con $n=3 y con $n=2. En cambio, fibonacci
con $n=3 llama a fibonacci con $n=2 y a $n=1, etcétera.
Cuenta cuantas veces fibonacci(0) y fibonacci(1) son llamadas. Esta es una solución
ineficiente al problema, y se vuelve peor a medida que el argumento crece.
Una solución es mantener un registro de los valores que ya se han calculado al almacenar-
los en un hash. Un valor que se ha computado y se almacena para uso posterior se conoce
como un memo. Aquí presentamos una versión “memoizada“ de fibonacci:
%conocidos es un hash que mantiene un registro de los números Fibonacci que ya sabemos.
Comienza con dos artículos: 0 y 1, los cuales mapean a 1.
Cuando se llama a fibonacci, la subrutina chequea a %known. Si el resultado ya existe, lo
devuelve inmediatamente. De lo contrario, tiene que computar el nuevo valor, agregar al
hash, y devolverlo.
Si ejecutas esta versión de fibonacci y la comparas con la original, encontrarás que es
mucho más rápida, especialmente para un argumento grande (por ejemplo, mayor que
30).
La memoización es una forma de caché, i.e., almacenar en memoria el resultado de una com-
putación (presumiblemente costosa) para evitar computarla nuevamente. Este proceso es
190 Capítulo 10. Hashes
algunas veces llamado “negociación de memoria por ciclos de CPU.“ En algunos casos, tal
como nuestro ejemplo recursivo de Fibonacci, la ganancia es absolutamente inmensa: cal-
cular el número Fibonacci 100o tomaría billones de años con la subrutina recursiva original
y toma solo segundos con la versión memoizada.
Nota que en el caso específico de la función Fibonacci, estamos almacenando valores para
cada entero sucesivo; podría haber memoizado los números Fibonacci en un array en lugar
de un hash (y podría ser hasta un poco más eficiente), pero usar un hash para tal propósito
es una solución más general, lo cual funciona hasta cuando las claves del memo no son
enteros consecutivos.
Como un ejercicio, intentar escribir la subrutina fibonacci usando un array en lugar de
un hash para memoizar los números Fibonacci ya calculados.
Esta estrategia es aburrida y sujeta a errores. El uso de una tabla de despacho es usualmente
una solución más simple.
Una tabla de despacho es una estructura de datos que mapea identificadores a referencias
de códigos u objetos de subrutinas. Si se aplica al escenario anterior, podría lucir así:
);
my $param = obtén-param();
die "Opción desconocida $param" unless %despacho{$param}:exists;
%despacho{$param}(); # ejecuta la acción especificada en %despacho
El hash %despacho define la acción dependiendo en el parámetro usado como una clave.
La sentencia %despacho{$param}() llama la acción requerida.
Esta estrategia es un poco más concisa y más limpia, pero existen otras ventajas. Es
más fácil de mantener: si necesitas agregar una opción, solo necesitas agregar una en-
trada al hash y evitas agregar código en el medio de una cadena complicada de sentencias
if {...} elsif {...} else {...} lo cual podría estropear algo.
Otra ventaja es que la tabla de despacho puede modificarse dinámicamente durante el
tiempo de ejecución, por ejemplo dependiendo en ciertas circunstancias externas (por ejem-
plo el día del mes cuando el programa se ejecuta) o de acuerdo a un archivo de configu-
ración. Esto significa que es posible modificar el comportamiento de un programa dinámi-
camente después del tiempo de compilación, mientras todavía se ejecuta. Esto abre el
camino a algunas técnicas avanzadas de programación que están fuera del alcance de este
libro.
Nota que hemos usado hashes para nuestras tablas de despacho, y esta es la manera más
común de implementarlas. Si hace sentido tener pequeños enteros como claves, podrías
también implementar una tabla de despacho como un array. Este es el caso, por ejemplo,
con los artículos numerados de un menú donde al usuario se le incita a teclear un número
para indicar la opción de menú a activar.
my $verbose = True;
sub ejemplo1 {
say 'Ejecutando ejemplo1' if $verbose;
# ...
}
Las variables globales son también usadas para variables de entorno y parámetros que se
pasan al programa, como para almacenar una estructura de datos que es la pieza central
de un programa, para evitar copiarla cuando se pasa como un argumento a las subrutinas.
Pero, a parte de esos casos específicos, es usualmente considerado mala práctica usar vari-
ables globales, porque crea dependencias y comportamientos de “acciones a distancia“
192 Capítulo 10. Hashes
inesperados entre varias partes de un programa y puede conducir a errores que son difícil
de encontrar.
En el caso de nuestra subrutina fibonacci memoizada, el hash %conocidos es útil solo
dentro de la subrutina. Podemos mejorar la implementación al usar el declarador state
dentro de la subrutina:
say fibonacci(10);
sub fibonacci( $n ) {
state %conocidos = 0 => 1, 1 => 1;
return %conocidos{$n} if %conocidos{$n}:exists;
%conocidos{$n} = fibonacci($n-1) + fibonacci($n-2);
return %conocidos{$n};
}
Otro tipo de chequeo compara los resultados de dos computaciones diferentes para
ver si son consistentes. Esto se llama “comprobación de consistencia“.
Formatea la salida Formatear la salida de un proceso de depuración puede facilitar la de-
tección de errores. Vimos un ejemplo en la Sección 5.11. La función dd muestra
detalles útiles de una estructura de datos compuesta o compleja.
Nuevamente lo repito: el tiempo que inviertes en andamiaje puede reducir el tiempo que
pierdes en depuración.
10.13 Glosario
Mapeo Una relación en la cual cada elemento de un conjunto corresponde a un elemento
de otro.
Hash Un mapeo de las claves a sus valores correspondientes.
Par clave-valor La representación de un mapeo de una sola clave a su valor.
Artículos En un hash, otro nombre para un par clave-valor.
Clave Un objeto que aparece en un hash como la primera parte de un par clave-valor.
Valor Un objeto en un hash que aparece como la segunda parte de un par clave-valor. Esto
es más específico que nuestro uso previo de la palabra “valor“.
Implementación Una forma de realizar una computación.
Tabla hash El algoritmo usado para implementa hashes.
Función hash Una función usada por una tabla hash para computar la ubicación de una
clave.
Hashable Un tipo que contiene una función hash. Los tipos inmutables como los números
y las cadenas de texto son hashables; los tipos mutables como los array y los hashes
no lo son.
Búsqueda Una operación de un hash que toma una clave y encuentra el valor correspon-
diente.
Búsqueda inversa Una operación de un hash que toma un valor y encuentra una o más
claves que mapean al valor.
Gráfica de llamada Una diagrama que muestra cada marco creado durante la ejecución de
un programa, con una flecha de cada función que hace la llamada a la función que se
llama.
Memo Un valor que se calcula para evitar computaciones futuras innecesarias.
Memoizar Almacenar un valor calculado en la memoria para evitar cálculos posteriores
del mismo. La memoización es una forma de caché.
Variable global Una variable definida fuera de cualquier subrutina u otro bloque. Las
variables globales pueden accederse desde cualquier subrutina.
Bandera Una variable booleana que se usa para indicar si una condición es verdadera.
194 Capítulo 10. Hashes
10.14 Ejercicios
Ejercicio 10.1. Escribe una subrutina que lea las palabras en words.txt y las almacene como claves
de un hash. (No importa cuales sean los valores.) Después puedes usar el adverbio exists como
una manera rápida de chequear si una cadena de texto se encuentra en el hash.
Si hiciste el Ejercicio 9.10, puedes computar la rapidez de esta implementación con un hash y la
búsqueda binaria.
Solución: A.8.2
Ejercicio 10.2. Memoiza la función Ackermann del Ejercicio 5.2 y observa si la memoización hace
posible evaluar la subrutina con argumentos más grandes. Pista: no. Solución: A.8.3.
Ejercicio 10.3. Si hiciste el Ejercicio 9.7, ya tienes una función llamada tiene-duplicados que
toma una lista como un parámetro y devuelve True si cualquier objeto aparece más de una vez en la
lista.
Usa un hash para escribir un versión más rápida y simple de tiene-duplicados. Solución: A.8.4.
Ejercicio 10.4. Dos palabras son “pares rotativos“ si puedes rotar una de ellas y conseguir la otra
(ver rotar-palabra en Ejercicio 7.3) usando el cifrado César.
Escribe un programa que una lista de palabras (por ejemplo, words.txt y encuentre todos los pares
rotativos. Solución: A.8.5.
Ejercicio 10.5. Este es otro Puzzler de Car Talk (http: // www. cartalk. com/ content/
puzzlers ):
Esto fue enviado por una persona llamada Dan O’Leary. Recientemente, él encontró
una palabra monosilábica de cinco letras que tiene la siguiente propiedad única. Cuando
se remueve la primera letra, las letras restantes forman una homófona de la palabra
original, es decir, una palabra que suena exactamente igual. Reemplaza la primera
letra, es decir, colócala devuelta y remueve la segunda letra y el resultado es aún otra
homófona de la palabra original. Y la pregunta es, cuál es la palabra?
Ahora te daré un ejemplo que no funciona. Miremos a la palabra de cinco letras, ‘wrack‘.
W-R-A-C-K, como en ‘wrack with pain’. Si remuevo la primera letra, me quedo con una
palabra de cuatro letras, ’R-A-C-K’. Como en, ‘Holy cow, did you see the rack on that
buck!It must have been a nine-pointer!’ Es un homófono perfecto. Si pones la ‘w‘
devuelta, y remueve la ‘r‘, te quedas con la palabra ‘wack‘, la cual es una palabra real,
es solamente no una homófona de las otras dos palabras.
Pero existe, sin embargo, por lo menos una palabra que Dan y todos nosotros conocemos,
que producirá dos homófonas si remueve una de las primeras dos letras para crear dos
nuevas palabras de cuatro letras. La pregunta es, cuál es la palabra?
Puedes usar el hash del Ejercicio 10.1 más arriba para chequear si una cadena de texto se encuentra
words.txt.
Para chequear si dos palabras son homófonas, puedes usar el Diccionario de Pronunciación CMU.
Puedes descargarlo de http: // www. speech. cs. cmu. edu/ cgi-bin/ cmudict .
Escribe un programa que enumere todas las palabras en words.txt (o en el diccionario CMU) que
resuelva el Puzzler. Solución: A.8.6.
11
Caso Práctico: Selección de Estructura de
Datos
A esta altura ya aprendiste sobre las estructuras de datos principales de Perl, y has visto
algunos de los algoritmos que las usan.
Este capítulo presenta un caso práctico con ejercicios que te dejan pensar acerca de la elec-
ción de estructuras de datos y practicar al usarlas.
Pero primero, me gustaría discutir brevemente dos estructuras condicionales que hemos
ignorado hasta ahora y proveer nuevas posibilidades sobre las signaturas de una subrutina.
my $result;
if $num < 10 {
$result = "Un dígito";
} else {
$result = "Más de un dígito";
}
say $result;
Esto es bien simple, pero un poco largo. Esto puede escribirse en una sola línea de código
en la siguiente forma:
El operador está conformado por dos partes: ?? y !!, las cuales separan tres expresiones
(de ahí su nombre de “operador ternario“):
196 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
Esta sentencia chequea si $num es menor que 10 y, si esto es verdadero, imprime “Un
dígito“; si la condición es falsa, entonces imprime “Más de un dígito“.
Realmente este operador no provee una nueva funcionalidad sino que ofrece una sintaxis
más concisa para escribir una condicional.
Es posible anidar varios operadores ternarios para examinar múltiples opciones sucesiva-
mente:
Esta construcción es una forma de sentencia conocida algunas veces como una sentencia de
switch porque el lenguaje C y muchos lenguajes derivados del mismo usan la palabra clave
switch para describir una condicional con opciones múltiples.
Esto es mucho más conciso y usualmente más conveniente que las condicionales anidadas
if ... después ... else, pero la siguiente sección provee un tipo de sentencia switch
mucho más poderosa.
given $valor {
when 0..9 { say "Un dígito" }
when $_ < 99 { say "Dos dígitos" }
when /^\d**3$/ { say "Tres dígitos" }
default { say "Más de tres dígitos" }
}
Solo un mensaje será imprimido, porque el proceso de coincidencia termina tan pronto
una de las condiciones ha sido satisfecha, y la cláusula default se ejecutará solo si las otras
condiciones fallaron.
Si no hay un operador específico en la cláusula when, entonces realizará una coincidencia
inteligente de la expresión en la la cláusula when contra $_:
Nota que la palabra clave given no hace más que topicalizar su argumento para el resto del
bloque. Las cláusulas when son las que hacen el trabajo pesado. De hecho, podrías usar las
cláusulas when sin given provisto que asignes el valor correcto a $_, lo cual, como podrás
recordar, puede hacerse con un bloque for:
my $val = 7;
for $val {
when 0..6 { say "menos que"}
when 7 {say "Exacto";}
when 8..* {say "mayor que";}
}
Es posible añadir una cláusula proceed al final de cualquiera de los bloques de código
condicionales para prevenir que el proceso pare después que el bloque de código haya
sido exitoso. Por ejemplo, podrías escribir esto:
given $valor {
when 0..9 { say "Un dígito" }
when 10..99 { say "Dos dígitos"; proceed }
when 42 { say "La respuesta al sentido de la vida" }
when /^\d**3$/ { say "Tres dígitos" }
default { say "Más de tres dígitos" }
}
En este ejemplo, si $valor es 42, dos mensajes se imprimirán, "Dos dígitos" y "La respuesta
del sentido de la vida y a todo el universo" porque la cláusula proceed en el segundo
bloque de código evita que el proceso termine con la primera coincidencia exitosa.
Grandioso, pero hay un problema. La cláusula proceed debería usarse con cuidado, dado
que puede fácilmente conducir a resultados inesperados. De hecho, el código de más arriba es
incorrecto: si $valor tiene dos dígitos pero no es 42 (si es, digamos, 43), el bloque default
también se ejecutará, porque la única coincidencia exitosa tenía la cláusula proceed, y dirá
que hay más ‘Más de tres dígitos” aunque esto es obviamente falso.
Como un ejercicio, prueba el código anterior con varios valores y intenta encontrar una
manera de arreglar el error con la cláusula proceed.
Solución: A.9.1
198 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
Los argumentos son suministrados en la llamada de la subrutina como una lista de pares
usando la sintaxis de constructor de pares. En la signatura, los parámetros son extraídos
en la signatura con la sintaxis de "colon-pair": el parámetro $dividendo está atado al valor
del par cuya clave es “dividendo“ (2048), y $divisor está similarmente atado a 128, sin
importar el orden de los argumentos en la llamada de la subrutina.
Estos parámetros nombrados son especialmente útiles cuando el número de argumentos
es grande. Por ejemplo, no hemos hablado de la programación orientada a objetos todavía
(ver Capítulo 12), pero esto es como podríamos crear un objeto de la clase (definida por el
usuario) Rectángulo:
my $rect = Rectángulo.new(
origen_x => 100,
origen_y => 200,
ancho => 23,
longitud => 42,
color => 'negro'
);
Al usar parámetros de posición, los parámetros opcionales siempre tienen que ser los últi-
mos en la lista (después de aquellos que son mandatorios).
Un parámetro puede también hacerse opcional al suministrar un valor por defecto:
Aquí, si el segundo argumento no se suministra, el valor por defecto (e) es usado. Inversa-
mente, si hay un segundo argumento, este anula el valor por defecto.
En algunas ocasiones, tener parámetros opcionales o por defecto no es suficiente. Por
ejemplo, la subrutina puede tener que procesar una lista que contiene un número inde-
terminado de valores. Para situaciones como esta, puedes usar un parámetro slurpy, i.e.,
un tipo de array colocado al final de la lista de parámetros que consumirá todos los argu-
mentos restantes. Este tipo de parámetro slurpy usa el sigilo *@. En el ejemplo siguiente,
la subrutina toma un parámetro mandatario (el primer número de la lista) y un lista de
argumentos adicionales que serán almacenados en el array @resto:
Ejercicio 11.2. Dirígete al Project Gutenberg (http: // gutenberg. org ) y descarga tu copia de
libro favorito en formato de texto simple.
Modifica tu programa del ejercicio previo para que lea el libro que descargaste, salta sobre la in-
formación de la cabecera al principio del archivo, y procesa el resto de las palabras como lo hiciste
anteriormente.
Después modifica el programa para que cuente el número total de palabras en el libro, y el número
de veces que cada palabra es usada.
Imprime el número de palabras diferentes usadas en el libro. Compara libros diferentes por autores
diferentes, escritos en eras diferentes. ¿Cuáles autores usan un vocabulario más extenso?
Ejercicio 11.3. Modifica el programa del ejercicio anterior para que imprima las 20 palabras más
usadas frecuentemente en un libro dado.
Ejercicio 11.4. Modifica el programa anterior para que lea una lista de palabras (ver Sección 8.2)
y después imprima todas las palabras en el libro que no están en la lista de palabras. ¿Cuántas de
ellas son errores tipográficos? ¿Cuántas de ellas son palabras comunes que deberían estar en la lista
de palabras, y cuántas de ellas son realmente recónditas?
Cuando se usa como un método, rand devuelve un número aleatorio entre 0.0 y el valor del
invocante. Por ejemplo, 10.rand devuelve un número aleatorio entre 0 y 10 (excluyendo al
10). Podrías intentarlo como un programa de una sola línea:
Con suerte, deberías obtener una salida diferente a la que obtuve. Si quieres ejecutar un
programa de una sola línea en Windows, recuerda que necesitarás reemplazar las comillas
simples con comillas dobles.
Para obtener números enteros aleatorios entre 1 y 10, podrías usar los métodos Int y rand:
Gutenberg. Para evitar tal problema asociado con cualquier cambio (y otros cambios futuros), puedes descarga
el archivo original desde este repositorio de Gitlab: https://gitlab.com/uzluisf/piensaperl6/tree/master/
suplementos/quijote.txt
202 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
Nota del traductor: En Think Perl 6, el autor usa el texto de la novela Emma, escrita por
Jane Austen2 . He decidido usar un libro en español porque creo que es más ameno y
porque quería saber cuantas palabras tenía el libro Don Quijote de la Mancha.
Aquí esta el programa que lee el archivo quijote.txt y construye un histograma de las pal-
abras que pertenecen al libro en el archivo:
my %histograma;
my $saltar = True; # bandera para saltar la cabecera y
# marcar el final del libro
El programa lee cada línea del archivo quijote.txt y, por cada línea, llama a la subrutina
procesar-línea.
La subrutina procesar-línea salta las líneas de la cabecera (i.e., todas las líneas hasta que
se encuentra la línea que contenga la cadena de texto “*** START OF THIS PROJECT“). De
igual manera, también salta cualquier línea que contenga las palabras “Abela“ o “anony-
mous“. La función reemplaza los guiones y los apóstrofos con espacios en blanco, remueve
varios caracteres de puntuación, y convierte la línea a minúscula. Después, divide la línea
en palabras individuales que son almacenadas y contadas con un acumulador en el hash
%histograma. Al final, asigna verdadero a $saltar si la línea contiene solo la palabra “fin“,
la cual marca el final del texto.
Para saber si el programa está haciendo lo que esperamos que haga, podemos mostrar unas
entradas del hash %histograma:
blob/master/Supplementary/emma.txt. De igual manera, puedes consultar Think Perl 6 para comparar tus
resultados con los del autor si decides usar este texto.
11.6. Histograma de Palabras 203
ahorcase 1
descontente 1
regüeldos 1
libras 5
manteca 1
suerte 141
voy 51
desnudaba 1
xxviii 2
benevolencia 2
vieren 3
rebenque 1
asentado 2
profetizadas 2
mirábalas 1
lata 4
cesaría 1
preguntalle 1
bailado 2
elogio 2
codicio 1
my $palabras-distintas = %histograma.elems;
say "Hay $palabras-distintas palabras distintas en el libro.";
Nota que podrías reducir lo anterior a una línea de código al interpolar un bloque de código
dentro de la cadena de texto de salida:
Y los resultados:
Las funciones sort recibe las claves del histograma y su función de comparación compara
los valores asociados con esas claves. Dado que usamos la clave $^b antes que la clave
$^a, sort producirá un ordenamiento en orden reverso (descendente). El procedimiento
sort completo está colocado dentro de paréntesis, para que el rango de subíndices [0..9]
actúe como una rebanada sobre la lista producida por sort y retenga solo las primeras 10
palabras más frecuentes. El resultado se almacena en el array @más_comunes. La siguiente
línea de código solo procesa el array para mostrar las palabras y sus frecuencias respectivas.
que 20626
de 18214
y 18188
la 10363
a 9823
en 8241
el 8210
no 6335
los 4748
se 4690
Si quieres ver más palabras interesantes, podrías, como un ejercicio adicional, filtrar el
histograma y retener solo las palabras que tienen más de, digamos, cuatros letras.
El array temporario @más_comunes no es realmente necesario. Podríamos hacer todo el
proceso con una sola sentencia:
manera más fácil de resolver el problema y el código más corto para hacerlo. Esta es la
razón por la cual he usado esta solución aquí. Pero no es la solución más eficiente desde el
punto de vista del tiempo de ejecución, porque involucra el costo de ordenar un conjunto
de datos relativamente grande, donde solo usamos una parte pequeña de los datos orde-
nados. Hay algoritmos mejores para hacer eso desde el punto de vista de la eficiencia de
ejecución, pero son más complicados. Por lo tanto, hay un sacrificio entre la eficiencia de
código y el rendimiento. Asumiendo que esto es código que queremos ejecutar solamente
una vez, he favorecido la eficiencia de código.
mostrar-más-comunes(%histograma, 5);
Esto mostrará las 5 palabras más comunes de la lista más arriba (Sección 11.7). Si la lla-
mamos sin el segundo parámetro:
mostrar-más-comunes(%histograma);
obtendremos las 10 palabras más comunes (misma lista como la que mostramos en la Sec-
ción 11.7), porque el valor por defecto para el segundo parámetro es 10.
Para encontrar las palabras en quijote.txt que no están en lemario.txt, necesitamos carga la
lista de palabras en un hash y llamar a sustraer, pasando los dos hashes como argumen-
tos. También agregamos algunas líneas de código para imprimir las primeras 20 palabras
que no se encuentran en la lista de palabras:
Nota que en lugar de usar una rebanada, he usado el método head para imprimir las
primeras 20 palabras de la lista. Esta es solo otra manera de obtener los primeros “n“
artículos de una lista o un array. También existe un método tail que extrae los últimos
“n“ artículos de una lista o un array.
Aquí están algunos de los resultados de Don Quijote:
Algunas de estas palabras son nombres y números romanos. Otras son raras y posible-
mente no en uso. Otras son verbos conjugados. ¡Pero algunas son palabras comunes que
deberían estar en la lista!
Nota que aquí he usado un hash (%palabras-desconocidas) para almacenar las palabras
de quijote.txt que no se encuentran en la lista de palabras. Dado que estamos utilizando los
datos solo para imprimir una muestra de 20 palabras, podríamos haber usado un array.
esto será suficiente para que el compilador de Perl sepa que no queremos usar la sustrac-
ción regular entre dos valores numéricos, pero algo más que aplica a dos hashes. El oper-
ador de menos está definido como un operador “infijo“, lo cual significa que el operador
será colocado entre sus dos operadores.
Llamar este operador es tan fácil como llamar el operador regular de sustracción entre dos
números; solo necesitamos usar dos hashes como operandos:
Esta facilidad para crear operadores nuevos es una de las facilidades ofrecidas por Perl 6
para extender el lenguaje desde si mismo. Regresaremos a eso y otras maneras de extender
el lenguaje en los capítulos siguientes.
Como un ejercicio, escribe una subrutina multi que crea un nuevo operador “¡‘ para calcu-
lar el factorial de un entero:
También intenta pensar cómo probarías este nuevo operador “¡‘ con varios valores de en-
trada. Solución: A.9.2
Como puedes ver, los elementos duplicados han sido removidos. Los conjuntos solo nos
dicen si al menos un elemento de un nombre dado se ha visto.
Un bag, al contrario, también mantiene un registro del número de cada artículo que se ha
visto:
208 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
Los mixes son similares a los bags, excepto que el peso de los elementos no tiene que ser
entero.
La cosa interesante acerca de estas colecciones es que pueden usar muchas de las opera-
ciones comunes de conjuntos usadas en matemáticas, tales como el operador de membresía
de conjunto ∈ (o usa (elem) si no sabes como teclear a ∈ en tu editor 3 ), el operador de
intersección de conjunto ∩ o (&), y el operador de subconjunto ⊂ o (<):
Nota que no hemos usado la palabra clave set para definir la lista <naranja banana> en el
segundo ejemplo más arriba. Esta lista ha sido coaccionada en un Set para chequear si era
un subconjunto del conjunto $s. Esta es una de las cosas grandiosas de estas colecciones:
la mayoría de estos operadores de conjuntos pueden usarse con listas, arrays, y hashes.
my %histograma;
my $saltar = True; # bandera para saltar la cabecera
sub procesar-línea( Str $línea is copy ) {
# (igual que antes)
}
Los operadores integrados \ 4 y (-) proveen la diferencia de conjunto, así que no tenemos
ni que escribir una subrutina sustraer ni construir nuestro propio operador de menos:
Hay más de 30 operadores de conjuntos disponibles, que cubren la mayoría de los oper-
adores de conjuntos usados en las matemáticas. Solo he mostrado aquellos que pueden
ser más útiles. Chequea la documentación oficial (https://doc.perl6.org/language/
setbagmix) si necesitas otros operadores.
Como un ejercicio, puedes escribir la subrutina procesar-línea de nuevo o reemplazarla
para usar un set o un bag en lugar de un hash para almacenar las palabras de quijote.txt
(y posiblemente adaptar el resto del programa donde sea necesario), para encontrar las
palabras de quijote.txt que no se encuentran en lemario.txt Solución: A.9.3
La expresión map {| (.key xx .value)} lee cada par del histograma y crea una lista con
copias .value de la cadena en .key. La función pick selecciona 30 palabras aleatorias
desde el array.
Este algoritmo funciona, pero no es muy eficiente; cada vez que eliges una o más palabras
aleatorias, se reconstruye la lista, la cual es tan grande como el libro original. Una mejora
obvia es construir la lista una sola vez y después hacer selecciones múltiples, pero la lista
es aún grande.
Una alternativa es:
2. Construir una lista que contiene la suma acumulativa de las frecuencias de las pal-
abras (ver Ejercicio 9.2). El último artículo en esta lista es el número total de palabras
en el libro, n.
3. Elegir un número aleatorio entre 1 y n. Usa la búsqueda binaria (ver Ejercicio 9.10)
para encontrar el índice donde el número aleatorio sería entonces insertado en la
suma acumulativa.
4. Usar el índice para encontrar la palabra correspondiente en la lista creada reciente-
mente.
Ejercicio 11.6. Escribe un programa que usa este algoritmo para elegir una palabra aleatoria desde
quijote.txt. Solución: A.9.4
Ten presente que Perl ofrece un atajo para realizar la tarea a mano: cuando se usa en un bag,
la función pick devuelve una selección aleatoria de elementos, ponderados por los valores
correspondientes a cada clave. Idealmente, deberíamos haber usado un bag en lugar de
un hash para almacenar nuestro histograma %histo, pero no sabíamos nada sobre los bags
en aquel entonces. Podemos, sin embargo, construir un bag desde el histograma %histo.
Considera el siguiente ejemplo:
Como puedes observar, el artículo más común, “naranja“, ha sido elegido más veces que
los otros, y el menos común, “pera“, no ha sido elegido hasta el cuarto sorteo.
Como un ejercicio, podrías querer adaptar este código para elegir palabras aleatorias desde
lemario.txt.
Una serie de palabras aleatorias raramente hace sentido porque no existe ninguna relación
entre las palabras sucesivas. Por ejemplo, en una oración real en español esperas que un
artículo definido como “la“ aparezca en frente de un sustantivo y que esté de acuerdo en
género y número. Sin embargo, no esperas que aparezca en frente de un verbo o adverbio.
Una manera de medir estos tipos de relaciones es través del análisis de Markov, el cual car-
acteriza la probabilidad de las palabras que vienen después para una secuencia de palabras
11.13. Análisis de Markov 211
dadas. Es decir, la probabilidad de que ciertas palabras aparezcan después depende sola-
mente en las palabras anteriores. Por ejemplo, el segundo capítulo de El Principito (1943), la
novela famosa escrita por el escritor francés y aviador pionero Antoine de Saint-Exupéry,
comienza:
Viví así, solo, sin alguien con quien poder hablar verdaderamente, hasta hace
seis años cuando tuve una avería en el Sahara. Algo se había estropeado en el
motor de mi avión. Como viajaba sin mecánico ni pasajero alguno, me dispuse
a realizar yo sólo, una reparación difícil. Era para mí una cuestión de vida o
muerte pues apenas tenía agua pura como para ocho días.
La primera noche me dormí sobre la arena, a unas mil millas de distancia del
lugar habitado más próximo. Estaba más aislado que un náufrago en medio
del océano. Imagínense, pues, mi sorpresa cuando al amanecer me despertó
una vocecita que decía:
- ¡Por favor... píntame un cordero!
- ¿Eh?
– ¡Píntame un cordero!
Me puse en pie de un brinco y frotándome los ojos miré a mí alrededor. De-
scubrí a un extraordinario muchachito que me observaba gravemente. Ahí
tienen el mejor retrato que más tarde logré hacer de él, aunque reconozco que
mi dibujo no es tan encantador como el original. La culpa no es mía, las per-
sonas mayores me desanimaron de mi carrera de pintor a la edad de seis años,
cuando sólo había aprendido a dibujar boas cerra das y boas abiertas.
Miré, fascinado, aquella aparición. No hay que olvidar que me encontraba a
unas mil millas de distancia de cualquier región habitada y el muchachito no
parecía ni perdido, ni muerto de cansancio, de hambre, de sed o de miedo. No
tenía la apariencia de un niño perdido en el desierto a mil millas de distancia
del lugar habitado más próximo. Cuando logré, por fin, poder hablar, pregunté:
– Pero... ¿qué haces tú aquí?
Y él repitió suave y lentamente, como algo muy importante:
- ¡Por favor...píntame un cordero!
Cuando el misterio es tan impresionante, uno no se atreve a contravenir. Por
absurdo que aquello pareciera, a mil millas de distancia de algún lugar habitado
y en peligro de muerte, saqué del bolsillo una hoja de papel y una pluma fuente.
Recordé que yo había estudiado geografía, historia, cálculo y gramática y le dije
al muchachito (algo malhumorado) que no sabía dibujar.
– No importa, ¡Píntame un cordero!
Como nunca había dibujado un cordero, repetí uno de los dos únicos dibujos
que era capaz de realizar: el de la boa cerrada. Y quedé absorto al oírle decir:
– ¡No, no! No quiero un elefante dentro de una serpiente. La serpiente es muy
peligrosa y el elefante ocupa mucho sitio. En mi tierra todo es muy pequeñito.
Necesito un cordero.
En este texto, la palabra “píntame“ es siempre seguida por la palabra “un“, y “píntame un“
es a su vez siempre seguida por “cordero“. Y la frase “unas mil“ es siempre seguida por
212 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
“millas“, pero la frase “unas mil millas“ a su vez puede ser seguida por “de distancia del
lugar habitado más próximo“ o “de distancia de cualquier región habitada“.
El resultado del análisis de Markov es un mapeo de cada prefijo (como “píntame un“ y
“unas mil millas“) a todos los sufijos posibles (como “un cordero“ y “de distancia del lugar
habitado más próximo“ o “de distancia de cualquier región habitada“).
Dado este mapeo, puedes generar un texto aleatorio al comenzar con cualquier prefijo y
eligiendo aleatoriamente de los sufijos posibles. Después, puedes combinar el final del
prefijo y el sufijo nuevo para formar el siguiente prefijo, y repetir.
Por ejemplo, si comienzas con el prefijo “píntame“, entonces la siguiente palabra tiene que
ser “un“ dado que la siguiente palabra es siempre “un“ en el texto. Si el prefijo es “unas
mil millas“, el sufijo siguiente podría ser “de distancia del lugar habitado más próximo“ o
“de distancia de cualquier región habitada“.
En este ejemplo, la longitud de los prefijos son dos o tres palabras pero puedes hacer un
análisis de Markov con un prefijo de cualquier longitud.
Ejercicio 11.7. Análisis de Markov:
1. Escribe un programa que lee un texto desde un archivo y realiza un análisis de Markov. El
resultado debería ser un hash que mapea los prefijos a una colección de sufijos posibles. La
colección podría ser una array, un hash, un set, un bag, etc.; hacer una elección apropiada
depende de ti. Puedes probar tu programa con prefijos de longitud dos, pero sería bueno
escribir el programa en una manera que facilite el proceso de intentar otras longitudes.
2. Agrega una función al programa previo para generar un texto aleatorio basado en el análisis
de Markov. Aquí presentamos un ejemplo de Don Quijote con prefijo de longitud 2:
o de ningún efecto tus fuerzas ni han de bastar todos los demás hicieron lo que le
hizo volar por los rincones de los ojos cerrados y así en las florestas y despoblados
buscando las aventuras que le había escrito el papel escrito y así creo que cuando
entrasen en alguna fuerza y así le vio dijo ¡bendito sea el caballero la hallará con la
cabeza
Para este ejemplo, la puntuación se ha dejado adjunta a las palabras. El resultado es casi
sintácticamente correcto, pero no tanto. Semánticamente, casi hace sentido pero no del todo.
¿Qué pasa si incrementas la longitud de prefijo? ¿Hace más sentido el texto aleatorio?
3. Una vez que tu programa funcione, podrías intentar hacer una mezcla: si combinas texto
de dos o más libros, el texto aleatorio que generas mezclará el vocabulario y las frases de las
fuentes en maneras interesantes.
Crédito: este caso práctico está basado en un ejemplo del libro The Practice of Programming,
escrito por Kernighan and Pike, Addison-Wesley, 1999.
Deberías intentar solucionar este ejercicio antes de proceder. Después puedes estudiar
nuestra solución en la SubsecciónA.9.5.
• Cómo representar el mapeo desde cada prefijo a la colección de de los sufijos posibles.
El último es fácil: un hash es la selección obvia para el mapeo desde las claves a los valores
correspondientes.
Para los prefijos, las opciones obvias son un cadena de texto o una lista de cadenas de texto.
Para los sufijos, una opción es una lista; otra es un histograma (hash).
¿Cómo debes hacer la elección? El primer pasos es pensar sobre las operaciones que nece-
sitarás implementar para cada estructura de datos. Para los prefijos, necesitamos ser capaz
de remover palabras desde el inicio y agregar palabras al final. Por ejemplo, si el prefijo
actual es “píntame“, y la siguiente palabra es “un“, necesitas ser capaz de formar el prefijo
siguiente, “píntame un“ para así encontrar el siguiente sufijo “cordero.“
Tu primera opción sería un array, dado que es fácil añadir y remover elementos, pero tam-
bién necesitamos usar los prefijos como claves en un hash, así que este requerimiento elim-
ina los arrays.
Para la colección de sufijos, las operaciones que necesitamos realizar incluyen agregar un
sufijo nuevo (o incrementar la frecuencia de uno ya existente), y elegir un sufijo aleatorio.
Agregar un sufijo es igualmente fácil para la implementación de una lista o un hash. Elegir
un elemento aleatorio desde una lista es fácil; elegirlo desde un hash es más difícil de hacer
eficientemente (ver Ejercicio 11.6).
Hasta ahora hemos estado discutiendo acerca de la facilidad de la implementación, pero
hay otros factores a considerar en la elección de estructuras de datos. Uno de ellos es el
tiempo de ejecución. Algunas veces existe una razón teórica al esperar que una estructura
de datos sea más veloz que otra; por ejemplo, mencionamos que la operación de búsqueda
es más rápida con los hashes que con los arrays, especialmente cuando el número de ele-
mentos es grande.
Pero usualmente no sabes con anticipación cual implementación será más rápida. Una
opción es implementarlas a ambas y observar cual es mejor. Esta técnica se conoce como
benchmarking. Una alternativa práctica es elegir la estructura de datos que es más fácil
de implementar, y después ver si es lo suficientemente rápida para la aplicación a mano.
Si lo es, no hay razón para proceder con la búsqueda. Si no es suficientemente rápida,
existen herramientas, como módulos de perfil (profile en inglés), que pueden identificar
los lugares en un programa que tardan más.
El otro factor a considerar es el espacio de almacenamiento. Por ejemplo, usar un his-
tograma para la colección de sufijos podría tomar menos espacio porque tienes que alma-
cenar cada palabra solamente una vez, sin importar cuantas veces aparezca. En algunos
casos, ahorrar espacio puede hacer que tu programa se ejecute más rápido, y en el caso
extremo, tu programa podría no ejecutarse si tu memoria se agota. Pero para muchas apli-
caciones, espacio es una consideración secundaria después del tiempo de ejecución.
Una reflexión final: en esta discusión, hemos insinuado que deberíamos utilizar una sola
estructura de datos para el análisis y la generación. Pero debido a que estas son fases
214 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
separadas, sería igualmente posible usar una estrcutura de datos para el análisis y después
convertirla a otra para la generación. Esto sería una gran ventaja si el tiempo ahorrado
durante la generación excede el tiempo que se emplea en la conversión.
my $pila = crear-pila(0);
for 1..5 {
my $valor;
($valor, $pila) = tomar-desde-pila($pila);
say "$valor -- ", $pila;
}
Esto se provee solo como un ejemplo para la construcción de una lista enlazada. Usual-
mente no hay necesidad de usar algo como esto en Perl, dado que es mucho más fácil
implementar una pila usando un array, como se puede observar en la Sección 9.4, aunque
el mismo principio puede ser usado para construir estructuras de datos más avanzadas.
Podrías aún querer, como un ejercicio, implementar una cola (ver Sección 9.4) usando una
lista enlazada.
11.15.2 Árboles
Un árbol (tree) en inglés) es usualmente una colección de artículos en la cual cada artículo
(o nodo) almacena un valor (o posiblemente varios valores) y dos o varios enlaces a otros
artículos de la colección, los hijos. Piensa sobre un árbol familiar o un árbol de directorios
en un disco duro para tener una idea general. Los últimos nodos que no tienen sus propios
hijos son usualmente llamados las hojas. Usualmente solo hay un nodo abuelo, llamado la
raíz (si hay un más de un abuelo, entonces no es realmente un árbol pero varios árboles o
un “bosque“).
Docenas de diferentes tipos de árboles han sido inventados y sus descripciones han cu-
bierto libros completos sobre algoritmos en la ciencia de la computación. Algunos son
diseñados para mantener tus datos ordenados, otros para mantener balance entre las ra-
mas de los árboles, etc. La estructura de datos es frecuentemente no tan complicada, pero
los algoritmos necesarios para mantener las propiedades requeridas son algunas veces bien
complicados.
Por ejemplo, un árbol típico podría retener un valor y dos enlaces, uno para el hijo
izquierdo y uno para el hijo derecho. Podrías implementar tal árbol binario en una man-
era similar como la lista enlazada que describimos más arriba, excepto que el valor no sería
ya un enlace al siguiente elemento sino que un array de dos elementos, los enlaces a los dos
hijos. O podrías seguir el modelo de lista enlazada de más arriba y tener pares anidados.
Por ejemplo, un árbol binario podría lucir de la siguiente manera:
my $árbol = 1 => (( 2 => 3 ) => (4 => (5 => ((6 => 7) => 8))));
El nodo raíz comienza en el índice 0. Sus hijos se encuentran en las posiciones 1 y 2. Los
hijos de 1 se encuentran en 3 y 4. Los hijos de 2 se encuentran en las posiciones 5 y 6. Los
hijos de 3 se encuentran en 7 y 8, etc.
Si se interpreta como un montículo binario, el array:
[0, 1, 2, 3, 4, 5, 6, 7, 8]
1 2
3 4 5 6
7 8
Fig. 11.1: El montículo correspondiente al array que contiene los dígitos del 0 al 8.
@array[$indice] = $valor-indice;
}
Nota que el montículo se construye en lugar (no hay un necesidad para un segundo array).
El array resultante se muestra así:
[a b g d c k j l f h e m n q p t r o i u s v]
¿Es este montículo correcto? Es muy difícil de saber a primera vista y chequearlo man-
ualmente es algo tedioso. Al escribir un programa para construir una estructura de datos
como esta, es usualmente útil escribir algunas subrutinas para mostrar el contenido en
una forma que facilite el entendimiento del resultado y el chequeo de exactitud. El código
siguiente muestras dos ejemplos de tales subrutinas:
loop {
say @array[$inicio..$final];
last if $final == $último;
$inicio += $paso;
218 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
b g
f c k j
l d h e m n q p
t r o i u s v
Fig. 11.2: El montículo correspondiente al array de letras.
$paso *= 2;
$final += $paso;
$final = $último if $final > $último;
}
}
(a)
(b g)
(d c k j)
(l f h e m n q p)
(t r o i u s v)
0 Nodo = a; Hijos: b y g
1 Nodo = b; Hijos: d y c
2 Nodo = g; Hijos: k y j
3 Nodo = d; Hijos: l y f
4 Nodo = c; Hijos: h y e
5 Nodo = k; Hijos: m y n
6 Nodo = j; Hijos: q y p
7 Nodo = l; Hijos: t y r
8 Nodo = f; Hijos: o y i
9 Nodo = h; Hijos: u y s
10 Nodo = e; Hijos: v y ' '
11 Nodo = m; No hijos
12 Nodo = n; No hijos
(...)
21 Nodo = v; No hijos
Leer Examina tu código, léelo, y chequea que diga lo que quieres decir.
Ejecutar con el depurador Un depurador es una utilidad que te permite ejecutar un pro-
grama paso a paso, para que puedas así seguir la ruta de ejecución y chequear el
contenido de variables importantes a puntos cruciales en la ejecución del programa,
para configurar puntos de interrupción, etc. Perl 6 provee un depurador, llamado
perl6-debug, que hace todas estas cosas posible. Con la venida de lenguajes mod-
ernos de alto nivel, muchas personas se resisten al uso de un depurador. Esto es un
error grave. Un depurador no te ayudará a solucionar cada tipo de problema, pero
puede ser una utilidad inmensamente útil. Ver la Sección 12.14 para información
adicional acerca del depurador de Perl.
Reflexionar ¡Toma tiempo para pensar! ¿Qúe tipo de error es? ¿Sintáctico? ¿Al tiempo de
ejecución? ¿Semántico? ¿Qué información puedes obtener de los mensajes de error,
o desde la salida del programa? ¿Qué tipo de error podría causar el problema que
estás observando? ¿Cuál fue el último cambio que hiciste, antes que el problema
apareciera?
Depurar con el patito de goma Si explicas el programa a alguien más, algunas veces en-
cuentras la solución antes de finalizar con la pregunta. Muy a menudo ni siquiera
necesitas la otra persona; podrías conversar con un patito de goma. Ese es el origen
de la muy conocida estrategia conocida como la depuración del patito de goma. ¡Y
no estoy bromeando! Lee sobre la estrategia aquí https://es.wikipedia.org/wiki/
M%C3%A9todo_de_depuraci%C3%B3n_del_patito_de_goma.
220 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
Retroceder Algunas veces, lo mejor que puedes hacer es retroceder, deshacer los cambios
recientes, hasta que puedas obtener un programa que funciona y que entiendas. De-
spués puedes comenzar la reconstrucción.
11.17 Glosario
Determinista Perteneciente a un programa que hace la misma cosa cada vez que se ejecuta,
dado la misma entrada.
Pseudo-aleatorio Perteneciente a una secuencia de números que aparece ser aleatoria,
pero es generada por un programa determinista.
Valor por defecto El valor dado a un parámetro opcional si no se provee un argumento.
Sobrescribir Reemplazar un valor por defecto con un argumento.
Benchmarking El proceso de elegir entre varias estructuras de datos y algoritmos al imple-
mentar alternativas y someterlas a pruebas ( especialmente sus tiempos de duración)
con una muestra de las entradas posibles
Depurador Un programa que permite ejecutar un programa línea por línea y chequea su
estado a cualquier paso durante su ejecución.
11.18. Ejercicio: Codificación Huffman 221
A U
B V
C W
D X
E Y
F Z
G
H
I
J
K 1
L 2
M 3
N 4
O 5
P 6
Q 7
R 8
S 9
T 0
Depuración del patito de goma Depuración que involucra explicar tu problema a un ob-
jeto inanimado tal como un patito de goma. Articular el problema puede ayudarte a
solucionarlo, aún si el patito de goma no sabe Perl.
1. Primeramente, usaremos una muestra de texto en inglés para generar una tabla de
los caracteres y sus frecuencias.
2. Después usaremos esta tabla de frecuencia para generar una tabla de código.
“Now, in English, the letter which most frequently occurs is e. Afterwards, the
succession runs thus: a o i d h n r s t u y c f g l m w b k p q x z. E however
predominates so remarkably that an individual sentence of any length is rarely
seen, in which it is not the prevailing character.” 6
La razón por la cual algunos espacios son necesarios es muy simple. Usa la tabla de código
morse anterior y supón que recibes tres puntos (...). Esto podría ser interpretado como la
letra “e“ tres veces, o como “ie“ o “ei“ o como “s“, o como el inicio de “h“, “v“, “3“, “4“,
o “5“. Los espacios agregados hacen posible distinguir entre las varias posibilidades. Pero
también hacen la transmisión de código mucho más lenta.
En 1951, David A. Huffman inventó una técnica de construcción de código que evita este
problema: provisto que sepas donde una letra dada comienza, no existe ninguna am-
bigüedad. Por ejemplo, trabajaremos más tarde con un código Huffman para un subcon-
junto pequeño del alfabeto que luce así:
5 ElEscarabajo de Oro
6 “ Ahora bien: la letra que se encuentra con mayor frecuencia en inglés es la e. Después, la serie es la siguiente:
a o y d h n r s t u y c f g l m w b k p q x z. La e predomina de un modo tan notable, que es raro encontrar una frase
sola de cierta longitud de la que no sea el carácter principal.“
11.18. Ejercicio: Codificación Huffman 223
a => ..
e => .-
s => -.-
n => -..
t => --.
d => ---.
r => ----
Si comienzas a leer una secuencia de puntos y rayas que representan un texto válido com-
puesto con estas sietes letras, siempre puedes decodificarlo sin ninguna ambigüedad. Si el
primer símbolo es un punto, entonces la letra es una “a“ o un “e“ dependiendo en el sím-
bolo siguiente. Si el primer símbolo es una raya y el siguiente es un punto, entonces la letra
deber ser una “s“ o una “n“ dependiendo del tercer símbolo. Si los primeros símbolos son
rayas, puedes similarmente determinar que la letra actual es una “t“ (si el tercer símbolo es
un punto), o una “d“ o una “r“. la cual puedes encontrar al revisar el cuarto símbolo. En
resumen, no necesitas espacios entre los símbolos, siempre es posible decodificar una letra
sin ambigüedad.
¿Cómo podemos construir un código Huffman? Hagámoslo manualmente con un alfabeto
bien simple: las cuatros letras de las bases nitrogenadas del ADN: A, C, T y G. Supón que
queremos codificar la siguiente cadena de texto de entrada:
CCTATCCTCGACTCCAGTCCA
C : 10 47.62
T : 5 23.81
A : 4 19.05
G : 2 9.52
Para construir el código Huffman, comenzamos con las dos letras menos frecuentes y las
mezclamos en un símbolo nuevo temporario, [GA], el cual pretendemos es una letra com-
puesta nueva con una frecuencia de 6. En este punto, decidimos que, entre dos letras, a la
menos frecuente se le adjuntará un punto y a la otra se le adjuntará una raya (este es una
decisión arbitraria, podría hacerse de la manera inversa). Así que decimos que el símbolo
para la letra menos común de las dos letras (“G“) será [GA]. y [GA]- será el símbolo para
la “A“.
Ahora tenemos tres letras, C, T, and [GA]. Mezclamos las dos letras menos frecuentes, “T”
y “[GA],” y podemos ahora decir que el símbolo para la “T” será [TGA]. y el símbolo para
[GA] será [TGA]-. Ahora tenemos solo dos letras restantes, “C” and “TGA“, con “C” siendo
la menos frecuente; así que “C” será un punto y “TGA“ será una raya.
Podemos ahora desenrollar nuestras letras ficticias: “T” es [TGA]., así que, reemplazando
[TGA] con su símbolo, i.e., una raya, el código final para “T“ será -.; similarmente, [GA].
ahora puede traducirse como --. Por el mismo proceso de sustitución, podemos ahora
determinar que “A“ es --- y “G“ es --.. Así que nuestra tabla final de código Huffman:
C => .
T => -.
G => --.
A => ---
224 Capítulo 11. Caso Práctico: Selección de Estructura de Datos
Nota que, debido a la construcción, la letra más frecuente (C) tiene el código más corto
mientras que las letras menos comunes (G y A) tienen los códigos más largos.
La codificación manual de cadena de texto de entrada CCTATCCTCGACTCCAGTCCA con este
código rinde el código pseudo-Morse siguiente:
..-.----...-..--.---.-...-----.-...---
Nota que nuestro código Huffman no tiene ambigüedad: el primer punto puede solo ser
una “C“, y el segundo también. El símbolo siguiente es una raya, que puede ser el inicio de
las otras tres letras, pero solo la “T“ puede tener un punto posterior. La secuencia siguiente
de símbolos tiene cuatro rayas; esto puede solamente tres rayas de una “A“, con la última
raya siendo el inicio de la letra siguiente; y -. solo puede ser una “T“, etc.
En una codificación de Huffman real para la compresión de archivos de texto, no usaríamos
puntos y rayas, pero los bits 0 y 1; sin embargo, los punto y las rayas son solo otra manera
conveniente de representar esos valores binarios. Por lo tanto, pretendamos que los puntos
y las rayas son realmente los números binarios 0 y 1.
¿Realmente logramos la compresión de datos? Nuestra cadena de pseudo-Morse tiene
38 símbolos binarios. La cadena de texto de entrada original tenía 21 caracteres o bytes, eso
son 168 bits. Los datos han sido comprimidos por un factor de 4.4.
¿Es la codificación de Huffman mejor que la codificación de longitud fija? Una repre-
sentación de una cadena de texto donde cada letra sería representada por dos bits (dos
bits pueden representar cuatros letras) requiere 42 símbolos. Por lo tanto, sí, obtuvimos
una mejor compresión de datos que una codificación de longitud fija (alrededor de 10%).
Este puede parecer un logro minúsculo, pero es actualmente un gran logro con un alfabeto
tan pequeño. Con datos textuales actuales, la codificación de Huffman puede lograr una
significante compresión de datos.
Ejercicio 11.9. 1. Escribe un programa que realiza la codificación de Huffman de una cadena
de texto de caracteres simple. Puedes comenzar con el ejemplo de ADN más arriba. No te
preocupes si no obtienes la misma tabla de Huffman de más arriba: pueden haber más código
de Huffman para una cadena de texto dada; pero chequea que obtengas un código que no sea
ambiguo.
2. Haz una prueba con cadenas de texto que tienen un alfabeto más grande (probablemente quer-
rás comenzar con un alfabeto relativamente pequeño porque, por lo contrario, puede ser tedioso
chequear el resultado manualmente).
3. Escribe una subrutina que codifique una cadena de texto de entrada en pseudo-Morse usando
la tabla de Huffman generada anteriormente.
4. Escribe una subrutina que decodifique la salida pseudo-Morse que has generado para la pre-
gunta anterior.
Solución: ver Sección A.9.6.2.
Parte II
Hacia Adelante
227
Ahora que has alcanzado el final de la primera parte de este libro, ya no deberías sentirte
un principiante. Deberías de ser capaz de leer (y escudriñar) la documentación oficial de
Perl 6 (https://docs.perl6.org) y encontrar tu camino.
Hay muchas cosas que decir acerca de la programación. Los siguientes tres capítulos serán
dedicados a conceptos más avanzados y a nuevos paradigmas de la programación, in-
cluyendo:
Cada uno de estos capítulos probablemente merece un libro completo (y podrían tener uno
en el futuro) pero esperamos decirte lo suficiente acerca de ellos para ponerte en marcha.
En mi opinión, cada programador debería saber sobre estos conceptos poderosos para ser
capaz de seleccionar la mejor forma de resolver un problema en particular.
Perl 6 es un lenguaje multiparadigmático, así que podemos cubrir estas disciplinas usando
el lenguaje Perl 6. Un número de temas que introducimos en los capítulos anteriores de-
berían facilitar la fácil adquisición de estas nuevas ideas, y esta es la razón por la cual
pienso que es posible cubrirlas apropiadamente solo con un capítulo para cada una de
estas disciplinas.
Habrán menos ejercicios en la segunda parte, porque esperamos por ahora que ya eres
capaz de idear tus propios ejercicios y hacer tus propios experimentos. Y habrán solo
algunas soluciones sugeridas, porque nos estamos acercando a un nivel donde no existe
una única solución correcta, sino muchas maneras de enfrentar y resolver un problema.
Con respecto al lenguaje Perl, hemos cubierto muchísimo material, pero, como te advertí al
principio, esto dista de ser exhaustivo. Los siguientes son los temas que no discutimos (y
no cubriremos); si deseas saber más sobre ellos, este es el momento perfecto para explorar
la documentación por ti mismo:
Interfaz para llamadas nativas Cómo llamar librerías que están escritas en otros lenguajes
de programación y seguir las convenciones de llamadas del lenguaje C. Ver https:
//docs.perl6.org/language/nativecall.
12
Clases y Objetos
A esta altura ya sabes cómo usar funciones para organizar código y tipos integrados para
organizar datos. El siguiente paso es aprender sobre la “programación orientada a objetos,“
la cual usa tipos definidos por el programador para organizar código y datos.
Cuando las aplicaciones de software comienzan a crecer, el número de detalles que se debe
manejar puede volverse agobiante. La única manera de manejar esta complejidad es usar
abstracción y encapsulamiento. La programación orientada a objetos es una manera muy
popular y eficiente de implementar abstracción y encapsulamiento.
• Los objetos usualmente representan cosas en el mundo real, y los métodos normal-
mente corresponden a las formas de interacción de las cosas en el mundo real.
• Un estado el cual es definido por algunas variables especiales (conocidas como, de-
pendiendo en el lenguaje, atributos, campos, o miembros); el estado puede cambiar
a medida que pasa el tiempo y es generalmente específico a cada objeto. En Perl, nos
referimos a dichas variables como atributos.
Nota que “foo” no es un objeto, pero una simple cadena de texto. No obstante, puedes
invocar el método say sobre la misma, debido a que Perl puede tratarla internamente como
un objeto cuando es necesario. En algunos otros lenguajes POO, esta conversión implícita
de un tipo nativo a un objeto es conocida como autoboxing.
Probablemente recuerdas que los métodos pueden ser encadenados en un proceso donde
el valor devuelto por un método se convierte en el invocante para el siguiente método:
En POO, los métodos que se pueden aplicar a los objetos son usualmente definidos dentro
de las clases, regularmente la clase que también definió el objeto o alguna otra clase con
12.2. Tipos Definidos por el Programador 231
una cercana relación. En Perl 6, los métodos pueden también ser definidos en un rol, el cual
es otro tipo de paquete de código que se parece a una clase, como veremos más adelante.
La idea básica de la programación orientada a objetos es que un objeto es un tipo de caja
negra que oculta su estructura interna (datos y código) del usuario; el usuario puede con-
sultar o cambiar el estado de un objeto a través de los métodos. El proceso de ocultar
la estructura interna de los objetos se conoce como encapsulación. Esto usualmente per-
mite tener una perspectiva más general y una mejor abstracción de datos con respecto a lo
que hemos visto hasta ahora; esto resulta en programas con menos errores (especialmente
programas extensos).
En adición, POO usualmente ofrece los siguientes conceptos:
Crear un nuevo tipo es un poco más complicado que las otras opciones, pero tiene ventajas
que serán aparentes muy pronto.
Un tipo definido por el programador es usualmente creado con una clase (o un rol, pero
regresaremos a esto más tarde). La definición de una clase bien simple para un tipo que
representa a un punto luce así:
class Punto2D {
has $.abscisa; # valor "x"
has $.ordenada; # valor "y"
}
232 Capítulo 12. Clases y Objetos
La cabecera indica que la nueva clase se llama Punto2D. El cuerpo define dos atributos,
i.e., propiedades con nombres asociadas con la clase, aquí la abscisa y la ordenada (o las
coordenadas x y y) del punto.
class Punto2D {
has $.abscisa; # valor "x"
has $.ordenada; # valor "y"
}
La cabecera indica que la nueva clase se llama Punto2D. El cuerpo define dos atributos,
i.e., propiedades con nombres asociadas con la clase, aquí la abscisa y la ordenada (o las
coordenadas x y y) del punto.
La definición de una clase con el nombre Punto2D crea un tipo objeto.
El tipo objeto es como una factoría para crear objetos. Para crear un punto, llamas el método
new sobre la clase Punto2D:
my $punto = Punto2D.new(
abscisa => 3,
ordenada => 4
);
say $punto.WHAT; # -> (Punto2D)
say $punto.isa(Punto2D) # -> True
say $punto.abscisa; # -> 3
12.3 Atributos
Los atributos que hemos definido son las propiedades asociadas con las clase Punto2D pero,
al mismo tiempo, son específicas a la instancia de la clase que hayamos creado. Ellos son
los atributos de la instancia. Si creamos otro objeto Punto2D, también tendrá los mismos
atributos, pero los valores de tales atributos serán probablemente diferentes.
La figura 12.1 muestra el resultado de estas asignaciones. Un diagrama de estado que
muestra un objeto y sus atributos es conocido como un diagrama de objeto.
12.3. Atributos 233
Punto2D
punto abscisa 3.0
ordenada 4.0
class Punto2D {
has Numeric $.abscisa; # valor "x"
has Numeric $.ordenada; # valor "y"
}
Los atributos de la instancia son privados para la clase, lo cual significa que no pueden
accederse fuera de la clase: usualmente necesitarías invocar un método de la clase (i.e., un
tipo de subrutina definida dentro de la clase), para obtener su valor. No obstante, cuando
se define un atributo con un punto como en $.abscisa:
has $.abscisa;
Perl crea un método implícito de acceso, i.e., un método que comparte el nombre del atributo
y devuelve el valor de dicho atributo. Así que, cuando escribimos:
no estábamos accediendo el atributo abscisa del objeto $punto directamente, sino que
estábamos llamando al método abscisa sobre el objeto, el cual devolvió el valor de ese
atributo.
Puedes usar dicho método de acceso con la notación de punto como parte de cualquier
expresión. Por ejemplo:
También hay otra manera de declarar un atributo en una clase, usando el twigil de signo
de exclamación en vez de un punto:
has $!abscisa;
En ese caso, Perl no crea un método implícito de acceso y el atributo solo se puede acceder
desde otros métodos dentro de la clase. Tal atributo es ahora completamente privado. Sin
embargo, si declaras atributos de esta manera, no podrás asignarles ningún valor durante
234 Capítulo 12. Clases y Objetos
la creación del objeto con el constructor por defecto new y necesitarás crear tu propio con-
structor (or modificar new indirectamente). Así que no intentes esto por el tiempo presente,
dado que no serás capaz de hacer mucho con tus objetos en este punto. Regresaremos a
esto más tarde.
Por defecto, los atributos de un objeto no son mutables; solo se pueden leer. Esto significa
que no puedes modificarlos una vez que se ha creado el objeto. Esto funciona bien para
algunos atributos: si un objeto representa a una persona, es poco probable que el nombre
de la persona y su fecha de nacimiento cambien. Otros atributos necesitan ser actualizados,
muchas veces muy frecuentemente. En tales casos, los atributos pueden declararse como
mutables con el rasgo (del inglés trait) is rw:
class Punto2D {
has Numeric $.abscisa is rw; # valor "x"
has Numeroc $.ordenada is rw; # valor "y"
}
Ahora es posible modificar estos atributos. Por ejemplo, podemos cambiar la abscisa del
punto recién creado:
Casi toda la información presentada aquí acerca de los atributos está relacionada con los
atributos de la instancia, i.e., propiedades relacionada a objetos individuales. También
puedes tener atributos que pertenecen a la clase entera, los cuales se conocen como atributos
de la clase. Son menos comunes que los atributos de una instancia y son declarados con el
declarador my (en vez de has). Un ejemplo típico de una atributo de una clase sería un
contador al nivel de la clase para mantener un récord del número de objetos que se han
instanciado.
class Punto2D {
has Numeric $.abscisa;
has Numeric $.ordenada;
method distancia-al-centro {
12.4. Creando Métodos 235
Aquí, el resultado sería el mismo, pero esta nueva sintaxis no es equivalente: $.abscisa es
una invocación de método, mientras que $!abscisa provee acceso directo al atributo. La
diferencia es que $!abscisa está disponible dentro de la clase (y podría ser un poco más
rápida), mientras la sintaxis de invocación de método puede ser usada en cualquier otra
parte (por ejemplo dentro de otra clase). Veremos en la siguiente sección ejemplos de esta
distinción y sus consecuencias.
Podemos ahora crear un objeto e invocar nuestros métodos:
my $punto = Punto2D.new(
abscisa => 4,
ordenada => 3
);
say $punto.coordenadas; # -> (4 3)
say $punto.distancia-al-centro; # -> 5
say $punto.coordenadas-polares; # -> (5 0.643501108793284)
Podría ser que recuerdes que si usas un método sin nombrar el invocante explícitamente,
entonces el método se aplica a la variable tópico:
.say for <one two three>; # -> one two three (cada uno en su propia línea)
Ahora que hemos creado un objeto con algunos métodos, también podemos tomar ventaja
de la misma sintaxis. Por ejemplo, si usamos for o given para poblar la variable tópico $_
con el objeto $punto, podemos escribir:
given $punto {
say .coordenadas; # -> (4 3)
say .distancia-al-centro; # -> 5
.coordenadas-polares.say; # -> (5 0.643501108793284)
}
class Punto2D-mutable {
has Numeric $.abscisa is rw;
has Numeric $.ordenada is rw;
# Modificando la ordenada:
$punto.nueva-ordenada(6);
say $punto; # -> Punto2D-mutable.new(abscisa => 3, ordenada => 6)
En este momento es difícil decir cuál de las opciones es mejor, así que implementaremos la
primera, como un ejemplo.
Aquí está la definición de la clase:
class Rectángulo {
has Numeric $.ancho;
has Numeric $.altura;
has Punto2D $.esquina; # vértice inferior izquierdo
method superior-izq {
$!esquina.abscisa, $!esquina.ordenada + $!altura;
}
238 Capítulo 12. Clases y Objetos
Rectángulo
rect ancho 5
Punto2D
altura 10
esquina abscisa 3.0
ordenada 4.0
Podrás haber notado que los argumentos que pasan al constructor Rectángulo.new no
están en el mismo orden como en la definición de la clase. Hice eso a propósito para
mostrar que el orden de los argumentos no importan porque estamos usando argumentos
nombrados.
El uso de un objeto como un atributo de otro objeto, posiblemente de otra clase, se conoce
como composición de objeto. Un objeto que es un atributo de otro objeto es un objeto em-
bebido (del inglés embedded). La composición de objeto hace posible definir capas anidadas
de abstracción y es una característica poderosa de la programación orientada a objetos. En
nuestro ejemplo de “geometría“, comenzamos a definir un objeto a un nivel inferior, una
instancia Punto2D, y después usamos ese punto para construir un tipo a un nivel superior,
Rectángulo.
12.6. Instancias como Valores de Retorno 239
method punto-superior-der {
return Punto2D.new(
abscisa => $!esquina.abscisa + $!ancho,
ordenada => $!esquina.ordenada + $!altura
);
}
# otros métodos para las otras esquinas
Nota que no tenemos que molestarnos en darle un nombre al punto superior derecho
(aunque podríamos si quisiéramos); podemos crearlo con el constructor y devolverlo in-
mediatamente.
Podemos usar el nuevo método como sigue:
my $puntoSupDer = $rect.punto-superior-der;
say "Esquina superior derecha: ", $puntoSupDer;
# -> Esquina superior derecha: Punto2D.new(abscisa => 9, ordenada => 13)
Aunque esto no es muy útil en casos simples como este, podríamos asegurarnos y declarar
un tipo Punto2D para $puntoSupDer:
De este modo, el código levantará un error si punto-superior-der devuelve algo más que
una instancia Punto2D.
Similarmente, el método encontrar-centro invocado sobre un Rectángulo devuelve una
instancia de Punto2D que representa el centro del Rectángulo:
12.7 Herencia
La herencia es probablemente la característica más emblemática de la programación orien-
tada a objetos. Es un mecanismo a través del cual es posible derivar una clase de otra clase.
La herencia es una de las maneras estándares de implementar la reutilización de código en
la programación orientada a objeto. Es también otra manera útil de definir capas sucesivas
de abstracción y una jerarquía de tipos.
240 Capítulo 12. Clases y Objetos
La nueva clase hereda las propiedades de la clase Punto2D gracias al rasgo is Punto2D, ex-
cepto posiblemente aquellos que son explícitamente modificados (o anulados) o agregados
en la nueva clase. Esta nueva clase es algunas veces llamada una clase hija o subclase,
mientras que Punto2D es la clase padre, clase base o superclase. La creación de esta nueva
clase basada en Punto2D se conoce como subclassing de la clase base Punto2D.
La nueva clase hija hereda los atributos abscisa y ordenada de la clase padre Punto2D
(y sus propiedades específicas si tienen algunas), al igual que los métodos tales como
coordenadas el cual es definido en la clase padre. La clase hija tiene un nuevo atributo
(el color) y dos métodos nuevos.
Instanciar un objeto Pixel es tan fácil como antes, solo necesitamos agregar un argumento
adicional correspondiente al nuevo atributo cuando invoquemos el constructor new:
my $pix = Pixel.new(
:abscisa(3.3),
:ordenada(4.2),
color => {rojo => 34, verde => 233, azul => 145},
);
En la definición de la clase Pixel, escribimos dos métodos diferentes para cambiar el color
solo para ilustrar posibles formatos de sintaxis, para propósitos pedagógicos. El primer
método recibe un hash como un parámetro, y el segundo usa parámetros de posición, los
cuales forzan al usuario recordar el orden (RGB) en el cual los argumentos deben pasarse;
esto puede ser una fuente de errores y debería evitarse cuando el número de parámetros
excede un cierto límite (lo cual se dejará al lector para determinar). Por otro lado, cualquier
persona que trabaja con gráficas sabe de memoria la convención estándar del orden de los
colores (i.e., RGB). También, el segundo método tiene la ventaja de habilitar el chequeo de
tipo (los argumentos deben ser números enteros). Esto es un ejemplo simplificado; en la
vida real, sería deseable chequear si los parámetros son octetos, i.e., enteros entre 0 y 255
(lo cual se podría hacer al añadir un una restricción de tipo o al definir un subconjunto del
tipo entero).
Usar la nueva clase Pixel es bien directo:
Colores originales: {azul => 145, rojo => 34, verde => 233}
Colores modificados: {azul => 70, rojo => 195, verde => 110}
Nuevas características del píxel:
Abscisa: 3.30
Ordenada: 4.20
Colores: R: 195, G: 110, B: 70
Nuevos colores:
R: 90, G: 180, B: 30
Para decir la verdad, no era necesario usar dos métodos con nombres diferentes,
cambiar_color y cambiar_color2, como hicimos en la definición de la clase Pixel para
simplificar el asunto. Funcionaría de la misma manera si usáramos estas definiciones:
Debido a que el método multi se define dos veces, con el mismo nombre pero diferentes
signaturas, el sistema de objeto es capaz de despachar la invocación al método correcto.
242 Capítulo 12. Clases y Objetos
La nueva clase hereda las propiedades de Punto2D gracias al rasgo is Punto2D, excepto
que aquellos que son explícitamente modificados (o anulados) o agregados en la nueva
clase. Los métodos que existen en la clase padre y se redefinen en la clase hija se dice que
son anulados dentro esa clase.
Aquí, los atributos $.abscisa y $.ordenada se redefinen con acceso de lectura y escritura
(a través del rasgo is rw) y un método nuevo, mover, es definido para modificar la posición
de un punto al añadir los parámetros recibidos a las coordenadas del punto.
Nota que hemos usado parámetros de posición para el método mover. Dijimos que es
usualmente mejor por motivos de claridad usar parámetros nombrados, pero hemos usado
solo dos parámetros aquí; debido a que es bastante simple recordar que el parámetro $x
debería venir antes que el parámetro $y. Esto fue una ocasión para ilustrar la posibilidad
de usar parámetros de posición.
Ahora podemos probar nuestra clase hija, crear una instancia PuntoMovible, mostrar sus
características, moverla a una posición diferente, y mostrar la posición nueva.
my $punto = PuntoMovible.new(
abscisa => 6,
ordenada => 7,
);
Coordenadas: (6 7)
Distancia al origen: 9.22
Coordenadas polares: radio = 9.2195, theta (rad) = 0.8622
--> Moviendo el punto.
Nuevas Coordenadas: (10 12)
Distancia al origen: 15.62
Coordenadas polares: radio = 15.6205, theta (rad) = 0.8761
Un clase puede tener varias clases padres y, por lo tanto, ser una subclase de otras clases.
Esto es lo que se conoce como herencia múltiple. Podríamos construir una nueva clase
PixelMovible la cual hereda de PuntoMovible y de Pixel (e, indirectamente, de Punto2D).
Técnicamente, puedes hacer esto fácilmente en Perl:
Esto luce bastante prometedor, pero sucede que tiende a ser más complicado de lo es-
perado en situaciones reales. Si existe un conflicto (por ejemplo una colisión de nombre
entre dos método), cuál debe prevalecer? Algunos mecanismo existen para manejar tales
situaciones (por ejemplo en el lenguaje de programación C++), y Perl tiene métodos meta-
objetos para encontrar información sobre el orden de resolución de los métodos (MRO por
sus siglas en inglés), pero esto puede rápidamente conducir a varios problemas de diseño
y a errores (bugs) que son sutiles y complicados. En resumen, mientra la herencia múlti-
ple originalmente lucía como una idea atractiva, resultó ser en algo muy complicado de
244 Capítulo 12. Clases y Objetos
dominar, porque crea dependencias múltiples, y usualmente implícitas, que son difíciles
de arreglar.
Esta es la razón por la cual, contrario a C++, lenguajes de programación orientados a ob-
jetos que son relativamente recientes como Java (el cual surgió no tan recientemente, en
1995) han decidido no implementar herencia múltiple.
Perl 6 no quiere prohibir tales cosas y como resultado te permite hacer uso de la herencia
múltiple si deseas, la cual puede ser muy útil para casos simples. Así que no la descarte
innecesariamente, pero recuerdas que, contrario a las expectaciones originales, la herencia
múltiple usualmente conduce a un desastre y resulta ser incontrolable.
Perl ofrece mejores conceptos para encargarse de tales situaciones, como veremos en un
momento.
1. Polígono
2. Cuadrilátero (un polígono con cuatro lados y cuatro esquinas)
3. Trapezoide (un cuadrilátero con un par de lados paralelos)
4. Paralelogramo (un trapezoide con dos pares de lados paralelos y lados opuestos de
la misma longitud)
5. Rectángulo (un paralelogramo con cuatro ángulos rectos)
6. Cuadrado (un rectángulo con cuatro lados iguales)
Es relativamente fácil de imaginar una serie de clases con un árbol de herencia jerárquica
que refleje esas propiedades. Se vuelve más complicado, sin embargo, si agregamos el
rombo (un paralelogramo con todos los lados iguales), porque el cuadrado es ahora también
un rombo con cuatro ángulos rectos. La clase cuadrado sería una subclase del rectángulo y
del rombo, y podríamos tener un posible caso de herencia múltiple.
Similarmente, podemos pensar acerca de un árbol de clases con herencia anidada que rep-
resentan varios tipos de números (e.g., entero, racional, real, complejo) o especies de ani-
males (e.g., vertebrado, mamífero, carnívoro, canino, perro, setter irlandés).
Estos son ejemplos grandiosos de herencia, pero el mundo real es raramente jerárquico, y
es usualmente difícil de forzar cada caso a encajar en un modelo jerárquico.
Esta es una de la razones por la cual Perl introduce la noción de roles. Un rol es un conjunto
de comportamientos o acciones que pueden compartirse entre varias clases. Técnicamente,
un rol es una colección de métodos (posiblemente con algunos atributos); es por lo tanto
similar a una clase, pero la primera diferencia obvia es que un rol no está diseñado para ser
instanciado como un objeto (aunque los roles pueden ser promovidos al estado de clases).
La segunda diferencia, quizás la más importante, es que los roles no se heredan: ellos se
usan al ser aplicados a una clase o/y una composición.
12.8. Roles y Composición 245
role Animal-mascota {
method es-compañero() {...}
# otros métodos
}
role Ovejero { ... } # pastor de ovejas
role Salvaje { ... } # animal en un ambiente salvaje
role Guía { ... } # guía de los ciegos
role Humano-comp { ... } # animal que se comporta como un humano
# ...
246 Capítulo 12. Clases y Objetos
Un rol se aplica a una clase o a un objeto con el rasgo does (opuesto a is para la heren-
cia). Estas dos palabras claves diferentes reflejan la diferencia semántica asociada con ellos:
componer un rol en una clase u objeto provee dicha clase u objeto con el comportamiento su-
plementario asociado con el rol, pero esto no significa que al objeto recibir el rol es la misma
cosa o de la misma naturaleza como el rol.
Si los roles Animal-mascota y Salvaje hubiesen sido definidos como clase, entonces las
clases Gato-mascota y Gato-salvaje hubiesen experimentado herencia doble, con los
problemas potenciales asociados con eso. Al aplicar un rol a una clase, evitas construir
un árbol de herencia múltiple que probablemente no se puede justificar y que puede ser
difícil de conceptualizar y manejar. Un uso juicioso de clases y roles puede conducir a
un modelo que es más simple, más natural, y más cercano a las relaciones reales entre las
entidades y los comportamientos bajo escrutinio.
En adición, si compones varios roles inadvertidamente con dos métodos que poseen el
mismo nombre, esto inmediatamente levanta un error (a menos que un método con el
mismo nombre exista dentro de la clase y en tal caso, prevalece), en lugar de despachar
silenciosamente a una de los métodos como en el caso de herencia múltiple. En ese caso, los
conflictos de nombre son identificados inmediatamente (al tiempo de compilación), lo cual
tiene el beneficio de encontrar un error inmediatamente, el cual podría no ser perceptible
por un largo tiempo.
role Dibujable {
has $.color is rw;
method dibujar { ... }
12.8. Roles y Composición 247
}
class Figura {
method area { ... }
}
class Rectángulo is Figura does Dibujable {
has $.ancho;
has $.altura;
method area {
$!ancho * $!ancho;
}
method dibujar() {
for 1..$.altura {
say 'x' x $.ancho;
}
}
}
Rectángulo.new(ancho => 10, altura => 4).dibujar;
Ten presente que la intención de los puntos suspensivos ... usados en el código anterior
es representar código que se deja para tu implementación. Sin embargo, esto es actual-
mente código válido y se compilará y hasta se ejecutará sin ningún problema. Los puntos
suspensivos son usados para representar una funcionalidad que no se ha implementado
todavía pero que se hará en el futuro. Esto funcionará siempre y cuando no invoques estos
métodos (lo que producirá un error de ejecución) o crees una situación donde ellos necesi-
tarían estar definidos (lo que ocasionará un error de compilación). En el caso del método
dibujar en el role Dibujable, la composición de rol en la clase Rectángulo funciona solo
porque dibujar es redefinido en la clase Rectángulo; sin esta redefinición, hubiese levan-
tado un error al tiempo de compilación. Similarmente, el código method area { ... } de
la clase Figura levantaría un error al tiempo de ejecución si fuera llamado sin haber sido
redefinido en la clase Rectángulo. Los puntos suspensivos se han usado aquí como una
manera conveniente de representar código cuyo implementación no es importante para
nuestro ejemplo porque se está redefiniendo de cualquier modo. En código real, es proba-
blemente recomendado no usar puntos suspensivos, excepto como expediente temporario
para código que no se ha desarrollado todavía pero que será implementado.
El código de más arriba dibuja un rectángulo ASCII:
$ perl6 test_drawable.pl6
xxxxxxxxxx
xxxxxxxxxx
xxxxxxxxxx
xxxxxxxxxx
Los roles y las clases son diferentes, pero ambos son tipos o definen tipos. Esto significa
que un rol puede usarse como un tipo para una declaración de variable donde esperaría
el nombre de una clase. Por ejemplo, el rol Guía en el fragmento de código anterior efec-
tivamente crea un tipo Guía. Así que un rol Invidente para un humano podría tener un
atributo de tipo Guía, el cual representaría un perro-guía, un caballo-guía, un humano-guía
o hasta un robot-guía.
class Humano {
has Perro $perro; # Puede contener cualquier perro,
# con o sin un rol guía
}
role Invidente {
has Guía $guía; # Puede contener un tipo Guía,
# un perro, un caballo, un humano, o un robot
}
Una gran mayoría de los tipos integrados de Perl 6 son definidos por roles y no clases, tales
como IO, Iterable, Iterator, Numeric, Rational, Real, etc.
class ClaseBase {
method Don-Quijote() { "Cervantes" }
method Hamlet() { "Shakespeare" }
method Three-Sisters () { "Chekhov" }
method Don-Carlos() { "Schiller" }
}
class Uses {
has $.base is rw handles < Don-Quijote Hamlet Three-Sisters >;
}
my $user = Uses.new;
12.10. Polimorfismo 249
Cervantes
Shakespeare
Chekhov
Method 'Don-Carlos' not found for invocant of class 'Uses'
in block <unit> at delegate.pl6 line 16
El programa muestra apropiadamente los nombres de los escritores que los primeros tres
métodos devuelve, porque ellos han sido más o menos “importados“ en la clase Uses,
pero falla con el último, porque “Don-Carlos“ no es parte de la lista del handler. El error
en el último método es una excepción al tiempo de ejecución y el programa terminaría la
ejecución ahí aún hubiera algún código correcto después.
Nota que la clase Uses no sabe de donde los métodos serán importados; solo sabe sobre
los nombres de los métodos que serán importados. Solamente cuando el objeto $user es
creado y el atributo $user.base es añadido, el objeto es dinámicamente con los métodos
definidos en ClaseBase. Casualmente, este proceso puede hacerse en un solo paso:
No hay necesidad de enumerar los métodos que serán manipulados. La clase Uses puede
importar todos los métodos de ClaseBase:
class Uses {
has $.base is rw handles ClaseBase;
}
Esto funcionará como antes, excepto que no fallará con el método Don-Carlos ahora, de-
bido a que este método fue importado también:
Cervantes
Shakespeare
Chekhov
Schiller
12.10 Polimorfismo
El polimorfismo es una manera de suplir una interfaz común o relacionada a tipos
diferentes. En cierta manera, los ejemplos de herencia que estudiamos anteriormente
ofrecen una forma de polimorfismo: los métodos coordenadas, distancia-al-centro,
y coordenadas-polares son polimórficos, debido a que se pueden aplicar a los tipos
Punto2D, PuntoMovible y Pixel. Hablaremos de polimorfismo cuando los métodos rele-
vantes o funciones hacen cada uno algo diferente, por lo menos al nivel de implementación,
aún compartan el mismo nombre y la misma interfaz.
250 Capítulo 12. Clases y Objetos
Fuera de la programación orientada a objeto (POO), las subrutinas multi de Perl implemen-
tan una forma de polimorfismo, dado que se comportan de forma diferente dependiendo
del tipo y número de sus argumentos. Dentro del contexto POO, es usualmente el tipo
del invocante (su clase o posiblemente uno de sus roles) que determinará, usualmente al
tiempo de ejecución, cuál de los posibles métodos será invocado.
Por ejemplo, podríamos desear crear una nueva clase para puntos en el espacio tridimen-
sional. Los métodos tendrán que ser diferentes, pero sería interesante ofrecer al usuario un
interfaz que es igual (o casi igual) a aquella de los puntos en dos dimensiones:
class Punto3D {
has Numeric $.x;
has Numeric $.y;
has Numeric $.z;
Los métodos en esta nueva clase no son los mismos que en Punto2D, pero los métodos con
semánticas similares tienen el mismo nombre; así que es posible usar cualquiera de las
clases sin perderse con nombres diferentes.
Por favor nota que los matemáticos, físicos, astrónomos, ingenieros, geógrafos, y naveg-
adores todos usan el mismo sistema básico para las coordenadas esféricas, pero sus con-
venciones son diferentes con respecto al origen, rango del ángulo, unidades medidas de
ángulos y dirección de rotación, y el nombre de los varios valores o símbolos asociados con
los mismos. Así que podrías encontrar diferentes fórmulas en un libro de texto. Las con-
venciones y fórmulas que hemos usado son comúnmente utilizadas en geografía y algunas
ramas de las matemáticas. Una clase real de propósito general tendría que tomar en cuenta
toda estas convenciones e implementar las convenciones necesarias.
12.11 Encapsulación
La encapsulación es la idea de ocultar los datos y código de una librería o un módulo del
usuario. El concepto no es específico a la programación orientada a objeto, pero es parte
fundamental de la misma.
En la programación orientada a objetos, la encapsulación consiste en proteger los datos
en un objeto de la manipulación directa (y de manera inconsistente) por el usuario, quien
accede tales datos a través de los métodos. Esto se logra al proveer al usuario métodos que
son conocidos como métodos de acceso (o getters) y mutadores (o setters). Esto hace posible
asegurar que las propiedades del objeto serán validadas por sus métodos.
La encapsulación es una forma poderosa de abstracción y abstracción procedimental. Visto
desde afuera, un objeto es una caja negra que tiene propiedades y comportamientos especí-
ficos. De esta forma, estas propiedades y comportamientos están ocultos del usuario. No
están ocultos en el sentido de que el usuario no puede saber sobre ellos (por lo menos en
el mundo de código abierto (open source), es fácil saber esto), pero que están ocultos en
el sentido de que es usualmente no posible usar ese conocimiento para eludir la interfaz
proveída. Esto significa que la implementación interna del objeto puede cambiar sin tener
que modificar el comportamiento externo. Si usas conocimiento privilegiado, tu código
probablemente no funcionará correctamente cuando la implementación interna sea modi-
ficada. Por lo tanto, no hagas eso.
Varios lenguajes de programación no tienen las mismas reglas para garantizar encapsu-
lación. Algunos son más estrictos que otros, algunos son menos estrictos sobre el acceso
de lectura que el acceso de escritura, otros no hacen tal distinción pero más bien dependen
en el nivel de visibilidad especificado por un atributo, por ejemplo “public“ o “private“
(algunas veces con un nivel intermedio “protegido“).
Perl 6 te permite eligir el modelo de encapsulación que quieres aplicar a tus objetos y atrib-
utos. Todos los atributos son privados. Si declaras una clase así:
class Punto2D {
has $!abscisa;
has $!ordenada;
# ...
method valor_x { return $!abscisa }
method valor_y { return $!ordenada }
}
las coordenadas $!abscisa y $!ordenada serán accesible solo dentro de la clase. Esta es la
razón por la cual hemos añadido métodos de acceso. Además, los atributos son inmutables
por defecto.
252 Capítulo 12. Clases y Objetos
class Punto2D {
has $.abscisa;
has $.ordenada;
# ...
}
las coordenadas serán todavía atributos privados, pero Perl 6 generará métodos de acceso
automáticamente con los mismos nombres de los atributos. De esta manera, se pueden
acceder fuera de la clase casi como si fueran públicos:
class Punto2D {
# ...
}
my $punto = Punto2D.new(abscisa => 2, ordenada => 3);
say $punto.abscisa; # -> 2
$mi-objecto!comportamiento-privado($val1, $val2)
Los métodos privados son realmente internos de una clase dada. En particular, ellos no
son heredados por las clases hijas.
class Punto3D {
has $.x;
has $.y;
has $!z;
method coord_valores {
return ($!x, $!y, $!z);
}
};
En este ejemplo, hemos declarado a $.x y $.y como atributos “públicos“ (por así decirlo),
y a $!z como un atributo verdaderamente privado. La ejecución de este código muestra lo
siguiente:
23
42
(Any)
¿Qué está sucediendo? Parece que el método coord_valores es incapaz de leer el con-
tenido de $!z, dado que devuelve un valor indefinido. Este método es definido dentro de
la clase y debería ser capaz de acceder este atributo. De hecho, coord_valores no es el
problema, sino que $!z no está definido dentro del objeto, porque no se inicializó apropi-
adamente durante la creación del objeto. La culpa yace con el constructor implícito new
que, por defecto, inicializa solo los atributos “públicos“.
Aquí la solución más simple es probablemente añadir el submétodo BUILD en la definición
de la clase.
Un submétodo es un método público de una clase que no es heredado por sus clases hijas.
Semánticamente, es realmente equivalente a una subrutina, pero se invoca con una sintaxis
de método (de ahí su nombre). Los submétodos son especialmente útiles al realizar las
tareas de construir y destruir un objeto que no deberían ser heredadas por las subclases,
también como para las tareas que son tan específicas para tipo particular que las clases
derivadas tendrán que seguramente modificarlas.
Inicializar los atributos privados durante la instanciación de un objeto podría lucir así:
class Punto3D {
has $.x;
has $.y;
has $!z;
¡Inicialización!
23
42
2
Esto funciona porque el constructor por defecto new, un método definido la superclase
suprema Mu y heredado por cualquier clase de Perl 6, llama al submétodo por defecto
BUILD. Si redefinimos a BUILD en nuestra clase, entonces reemplazará el submétodo por
defecto que new llama. Al redefinir a BUILD, forzamos el constructor a tomar en cuenta el
atributo privado que no se usó previamente.
Un poco de simplificación es posible. Dado que los argumentos que se pasan a una rutina
enlazan los argumentos a los parámetros, un paso de enlace separado es innecesario si los
atributos se usan como parámetros. Por lo tanto, el submétodo BUILD en el ejemplo anterior
podría también escribirse tan simple como esto:
class Punto2D {
has Numeric $.abscisa;
has Numeric $.ordenada;
Esto mostrará las dos coordenadas. bless es un método de nivel inferior para la con-
strucción de objetos, heredado desde Mu y llamado automáticamente cuando invocas a new
para construir un objeto. Usualmente no necesitas saber sobre ese método, excepto cuando
quieres escribir tu propio constructor personalizado.
class Punto2D {
has Numeric $.abscisa;
has Numeric $.ordenada;
Aunque deberías pensar dos veces antes de anular a new o crear tu propio constructor
personalizado con un nombre diferente, dado que podría hacer más difícil la creación de
subclases de la clase Punto2D.
Un principio de diseño que ayuda a lograr esa meta es mantener la interfaz separada de
las implementaciones. Para objetos, esto significa que la interfaz pública de los métodos
proveídos por una clase no debería depender en cómo los atributos son representados.
Por ejemplo, diseñamos una clase Punto2D en la cual los atributos principales eran las
coordenadas cartesianas del punto. Podemos descubrir que, para el propósito de nuestra
aplicación, sería más fácil o más rápido almacenar las coordenadas polares del punto en los
atributos del objeto. Es totalmente posible cambiar la implementación interna de la clase,
y aún así mantener la misma interfaz. Para hacer esto, necesitamos que el constructor con-
vierte los parámetros de entrada desde coordenadas cartesianas a polares, y almacenar las
coordenadas polares en el atributo del objeto. El método coordenadas-polares devolvería
los atributos almacenados, mientras que los métodos que devuelven las coordenadas carte-
sianas podrían tener que hacer la conversión opuesta (o quizás ser almacenados en atrib-
utos privados). En general, el cambio ser podría hacer con la refactorización de la clase
Punto2D, pero los usuarios de la clase todavía usarían la misma clase sin notar diferencia
alguna.
256 Capítulo 12. Clases y Objetos
Después de desplegar una nueva clase, podrías descubrir una mejor implementación. Si
otras partes del programa están usando tu clase, podría ser un proceso largo y proclive a
errores cambiar la interfaz.
Pero si diseñaste la interfaz cuidadosamente, puedes cambiar la implementación sin cam-
biar la interfaz, lo cual significa que las otras partes del programa no tienen que cambiar.
12.13.2 La Moraleja
Podemos aprender varias cosas de esta parábola.
12.13.2.1 Delegación
Para manejar complejidad, delega a una entidad confiable, e.g., el granjero delegó parte de
sus responsabilidades al $niño-pastor.
12.13.2.2 Encapsulación
$perro-ovejero.vigilar_rebaño();
$perro-ovejero.cerebro.tarea.vigilar_rebaño();
A un nivel superior, no nos importa particularmente la estructura interna del objeto. Solo
nos importa lo que el objeto puede hacer.
Mientra más se expone la estructura interna de un objeto, más difícil se vuelve cambiarlo.
12.13.2.3 Polimorfismo
$perro-ovejero y $niño-pastor ambos entienden los mismos comandos, así que el reem-
plazo del último con el primero fue más fácil de lo que hubiese sido por el contrario.
La fábula en esta sección es una adaptación de un post de “Arunbear“‘en el sitio web “PerlMonks“:
http: // www. perlmonks. org/ ?node_ id= 1146129 . Gracias a “Arunbear“ por autorizarme a
reutizarla.
Esto quiere decir que está cargando el programa while_done.pl6, y muestra las primeras
líneas del programa; la última línea al fondo (“>”) es un prompt donde puedes entrar
algunos comandos. El programa para en la primera sentencia que actualmente hace algo y
espera por tu entrada. La línea de código que espera ser ejecutada es resaltada en un color
diferente.
> h
<enter> single step, stepping into any calls
s step to next statement, stepping over any calls
so step out of the current routine
[...]
q[uit] exit the debugger
>
Toma el tiempo para ejecutar ese comando y lee las posibles instrucciones que puedes
entrar. Describiremos las más comunes. Como puedes ver arriba, solo usas “q“ o “quit“
para salir del depurador.
A cualquier punto del proceso, puedes mirar el contenido de las variables o hasta métodos
sobre ellas. Para ver una variable, escribe su nombre y después presiona Enter:
> $line
"foo"
También puedes mirar un array o un hash, o usar el índice o la llave, por ejemplo
@array[10] o %hash{"bar"}, para visualizar un elemento específico del array o del hash.
De igual modo, puedes utilizar “s“ (o “say“) o “p“ (o “print“) para evaluar y mostrar una
expresión en el ámbito actual.
Puedes ver todos los puntos de interrupción (bp list), remover un punto de interrupción
(bp rm line), o remover todos los puntos de interrupción (bp rm all). También puedes
crear un punto de interrupción en otro archivo (por ejemplo si estás usando un módulo)
al usar la siguiente sintaxis: bp add archivo:línea, donde “archivo“ es el nombre del
archivo.
Probablemente ya sabes lo suficiente para hacer buen uso del depurador de Perl 6, recorrer
tu programa y descubrir donde hace algo que no está supuesto hacer. No fue tanto que
aprender, cierto? ¡Inténtalo!
Por ejemplo, lo usamos para registrar la variable $palabra-rotada en la solución del ejerci-
cio sobre el cifrado César (ver Subsección A.5.9) para la cadena de texto de entrada “ABCD-
abcd” con una rotación de 25 letras; el comando tp show mostró cómo la cadena de texto
codificada fue progresivamente poblada letra por letra:
> tp show
>>> rotar.pl6:23
*
* Z
* ZA
* ZAC
* ZACB
* ZACBz
* ZACBza
* ZACBzab
El depurador puede también proveer información útil cuando el código está intentando
coincidir un regex. Por ejemplo, supón que ejecutamos un programa con el depurador con
el siguiente código:
"foobar" ~~ /f.+b/;
Si ejecutas el regex paso a paso, la coloración de sintaxis mostrará átomo por átomo donde
está en el regex y que parte de la cadena de texto coincide. (No podemos mostrar la col-
oración de sintaxis aquí, pero deberías intentarlo para verlo.)
Con el regex arriba, verás que el motor de regex intenta coincidir el carácter “f“del patrón
y encuentra un “f“ al principio de la cadena de texto; siguiente, verás que el motor de
regex intenta coincidir el subpatrón “+.“ y que coincide la cadena de texto entero; después,
cuando el motor de regex intenta coincidir la última “b“ del patrón, verás que el motor de
regex retrocede, se deshace de la “r“ y después de la “a“; finalmente, el motor de regex
tiene éxito con ‘foob“.
Si tienes dificultad entendiendo cómo los regexes trabajan o estás mistificado por el retro-
ceso (backtracking), solo ejecuta el depurador sobre algunos regexes y observa lo que sucede
paso a paso. No tienes ni que escribir un programa; puedes usarlo en el modo de una sola
línea (one-liner). Por ejemplo, para probar el regex de arriba como un one-liner en Win-
dows, solo escribe el comando siguiente en el prompt:
Como es usual, cambia las comillas dobles a comillas simples si estás usando cualquier
plataforma de Unix.
Nuestra nota final sobre el depurador: recuerda siempre entrar “h“ para conseguir ayuda
sobre el comando que necesitas.
12.15. Glosario 261
12.15 Glosario
Objeto Una entidad que encapsula su estado (atributos) y su comportamiento (métodos).
Clase Un tipo definido por el programador. La definición de una clase crea un nuevo
tipo de objeto (una forma de abstracción) y hace posible la instanciación de objetos
concretos que representan datos reales.
Método Un tipo especial de subrutina dentro de una clase o un rol, que puede llamarse
usando la sintaxis de la notación del punto.
Tipo objeto Un objeto que contiene información sobre un tipo definido por el progra-
mador. El objeto de tipo puede usarse para crear instancias del tipo.
Instancia Un objeto que pertenece a una clase y contiene datos reales.
Instanciar Crear un nuevo objeto.
Atributo Una propiedad de estado dentro de la estructura POO que es similar a una vari-
able. El atributo de una instancia es uno de los valores nombrados asociados con un
objeto. Los atributos de una clase son las variable asociadas con la clase entera.
Objeto embebido Un objeto que es almacenado como un atributo de otro objeto.
Composición de objeto Uso de un objeto como parte de la definición de otro objeto, espe-
cialmente el uso de un objeto como un atributo de otro objeto.
Diagrama de objeto Un diagrama que muestra objetos, sus atributos, y los valores de di-
chos atributos.
Rol Una colección de métodos similar a una clase pero que no está diseñada para construir
objetos. Un rol contiene métodos que pueden aplicarse a una clase o a un objeto para
añadir comportamientos a ellos.
Polimórfico Perteneciente a una función que funciona con más de un tipo.
Encapsulación El principio que dicta como la interfaz proveída por un objeto no debería
depender en su implementación, en particular la representación de sus atributos.
Esto también se conoce como ocultación de información.
Herencia La habilidad de crear una nueva clase la cual es una versión modificada de otra
clase previamente definida.
Clase padre La clase desde la cual la clase hija hereda. También conocida como superclase
o clase base.
Clase hija Una nueva clase que hereda de una clase existente; también llamada subclase.
Subclassing El proceso de crear una clase hija derivada de una clase padre.
Anular Cuando un método de una clase padre es redefinido en una clase hija, se dice que
ha sido anulado dentro de la clase hija.
Herencia múltiple Una situación en la cual una clase hija es derivada y hereda desde más
de una clase padre.
Delegación Definir una clase o un rol en el cual es posible invocar métodos que pertenecen
a otro objeto.
262 Capítulo 12. Clases y Objetos
13
Regexes y Gramáticas
Las expresiones regulares o regexes fueron introducidas de las Secciones 7.5 a 7.9. Es re-
comendable que leas esas secciones nuevamente antes de leer este capítulo si no recuerdas
mucho sobre los regexes. No necesitas recordar los detalles de todo lo que discutimos
anteriormente y explicaremos de nuevo, sino brevemente, las partes específicas de la fun-
cionalidad que usaremos, pero se espera que entiendas cómo los regexes funcionan en
general.
my $cad = "foo13abbccbcbcbb42Cbar";
say ~$/ if $cad ~~ /a <[bc]>+ (\d*) [B|C]/; # -> abbccbcbcbb42C
say ~$0; # -> 42
• <[bc]>+: el átomo <[bc]> es una categoría de caracteres que se refiere a la letra “b“
o “c“; el cuantificador + dicta que los caracteres que coinciden con la categoría de
caracteres “b“ o “c“ pueden repetirse una o más veces.
• [B|C]: B|C es una alternación (una “B“ o un “C“), y los corchetes reagrupan esta
alternación en un (y también habilita precedencia adecuada).
Esto es lo que se llama coincidencia a un bajo nivel: el reconocimiento del patrón se hace
mayormente al nivel individual de un carácter. Perl 6 ofrece varias maneras de agrupar y
nombrar los patrones de regex para que estos patrones individuales puedan utilizarse pos-
teriormente como componentes básicos para coincidencia a un alto nivel: el reconocimiento
de palabras y secuencias de palabras (más que caracteres), para el propósito de hacer lo que
se conoce como análisis léxico (o lexing) y análisis sintáctico (o parsing) en una pieza de
texto.
Este capítulo está mayormente dedicado este tipo de coincidencia, que conduce a la
creación de gramáticas completas que analizan textos estructurados tales como textos XML
o HTML, documentos JSON o YAML, o hasta programas de computadora: los programas
de Perl 6 son actualmente analizados sintácticamente usando una gramática de Perl 6 es-
crita en Perl 6.
Las gramáticas son un tema muy importante en la ciencia de la computación pero, obvia-
mente, no todos los programadores escriben gramáticas completas para analizar sintácti-
camente lenguajes de programación. No obstante, la escritura de una gramática simple y
un analizador sintáctico simple podría ser, o quizás debería ser, una actividad mucho más
común.
Algunas veces, también necesitas desarrollar un lenguaje de dominio específico (DSL por
sus siglas en inglés), i.e., un sub-lenguaje relativamente pequeño (también conocido como
una jerga) adaptado a un campo específico del conocimiento (ciencia, ingeniería, negocios,
arte, u otro) con sus propias convenciones, símbolos, operadores, etc. Con una gramática
y la habilidad de Perl para crea sus propios operadores, puedes usualmente expresar
conocimiento especializado dentro del marco de terminología de los expertos de una área
del conocimiento en particular.
13.2. Programación Declarativa 265
13.3 Capturas
Como explicamos en los ejemplos de regex al comienzo de este capítulo, los paréntesis no
solo agrupan cosas sino que también capturan datos: ellos hacen que la cadena de texto
que coincide con el subpatrón dentro de los paréntesis se encuentre disponible dentro de
una variable especial:
Aquí, el patrón coincidió la cadena de texto $cad, y la parte del patrón dentro de los parén-
tesis fue capturada en la variable especial $0. Si hay varios grupos con paréntesis, ellos
serán capturados en las variables $0, $1, $2, etc. (de izquierda a derecha):
Esto funciona muy bien con capturas simples, pero el número de capturas puede volverse
tedioso si hay muchas capturas y algo complicado cuando hay paréntesis anidados en el
patrón:
Cuando se vuelve complicado, es usualmente mejor usa otra características conocida como
capturas nombradas. La manera estándar de nombrar una captura es de la siguiente forma:
Asignar el objeto de coincidencia a un hash ofrece acceso programático fácil a todas las
capturas nombradas:
Pero podrías hacer la misma cosa directamente en el objeto de coincidencia sin tener que
hacer una asignación de hash extra:
Recuerda que, en el código más arriba, $<todo> es realmente un atajo para $/<todo>, i.e.,
para un tipo de acceso hash al objeto de coincidencia $/.
No obstante, existe una manera más conveniente de obtener capturas nombradas la cual
discutimos en la siguiente sección.
Nota que la sintaxis con un bloque de código se parece a la definición de una subrutina o un
método. Esto no es una casualidad; veremos que las reglas nombradas son muy similares
a los métodos. Notablemente, las reglas pueden llamarse unas a las otras (o hasta algunas
13.4. Reglas Nombradas (o Subreglas) 267
veces ellas mismas de forma recursiva) al igual que los métodos y las subrutinas, y también
veremos que esto es una característica muy poderosa y expresiva.
Un regex nombrado puede declararse con my regex nombre { cuerpo del regex }, y
llamarse con <nombre>.
Como puedes ver en el ejemplo más arriba, un regex nombrado que es exitoso crea una
captura nombrada con el mismo nombre. Si necesitas un nombre diferente para la cap-
tura, puedes hacer esto con la siguiente sintaxis <nombre-captura=nombre-regex>. En
este ejemplo, llamamos el mismo regex nombrado dos veces y, por conveniencia, usamos
un nombre diferente para distinguir las dos capturas:
En este ejemplo, usamos las invocaciones del método chomp para remover el carácter de
nueva línea de las capturas. Existe de hecho una manera para coincidir con el carácter de
nueva línea pero excluirla de la captura:
Este poco conocido token, ")>", marca el punto final de la captura completa de una co-
incidencia. Cualquier cosa después del token participará en la coincidencia pero no será
capturada por el regex nombrado. Similarmente, el token ")>" indica el inicio de la cap-
tura.
Los regexes nombrados son solo una forma (y probablemente no la más común) de las
reglas nombradas, que vienen en tres sabores distintos:
• Reglas nombradas, en la cual el regex tiene un adverbio :ratchet implícito, como los
tokens nombrados, y también un adverbio :sigspace nombrado, lo cual significa que
los espacios en blanco dentro del patrón (o, más específicamente, entre los caracteres
de palabra) no son ignorados
En los dos ejemplos arriba, no necesitamos que los regexes volvieran atrás. Podríamos (y
probablemente deberíamos) haber usado un token nombrado en lugar de un regex nom-
brado:
268 Capítulo 13. Regexes y Gramáticas
Pero, para que una regla coincida, tendríamos que remover el espacio desde dentro del
patrón:
if $cadena ~~ /<fecha>/ {
say ~$/; # -> 2016-12-25
say "Día\t= " , ~$/<fecha><día>; # -> 25
say "Mes\t= " , ~$/<fecha><mes>; # -> 12
say "Año\t= " , ~$/<fecha><año>; # -> 2016
}
Los primeros cuatro tokens nombrados definen los componentes básicos para coincidir con
el año, el mes, el día, y los posibles separados. Después, la regla nombrada fecha usa los
componentes básicos para definir una alternancia entre los posibles tres formatos de fecha.
Este código chequea que el día en el mes esté entre 1 y 31 y que el mes esté entre 01 y
12, y esto es probablemente suficiente para reconocer fechas en un texto en la mayoría
de los casos, pero esto coincidiría con “2016-11-31” aunque Noviembre tiene solo 30 días.
Podemos ser un poco más estrictos sobre las fechas válidas y prevenir eso al agregar una
inserción de código negativa a la regla nombrada fecha:
13.5. Gramáticas 269
Esto es mejor, pero todavía podemos coincidir con una fecha inválida tal como “2016-02-
30”.
Ejercicio 13.1. Como un ejercicio, cambia la aserción de código para rechazar una fecha como “Feb.
30“. Si te sientes valiente, podrías hasta querer chequear el número de días en febrero dependiendo
si la fecha ocurre en un año bisiesto. También podrías intentar definir y probar otros formatos de
fecha. Solución: A.10.1
Las reglas pueden (y usualmente deberían) agruparse en gramáticas; esa es la razón por la
cual fueron diseñadas.
13.5 Gramáticas
Las gramáticas son una herramienta poderosa que se utilizan para analizar datos textuales
y usualmente devuelven estructuras de datos que se crean al interpretar dichos textos.
Por ejemplo, cualquier programa de Perl 6 se analiza sintácticamente y se ejecuta usando
una gramática de Perl 6 escrita en Perl 6, y podrías escribir una gramática para analizar
sintácticamente (casi) cualquier otro lenguaje de programación. Para ser honesto, los pro-
gramadores raramente escriben gramáticas para analizar sintácticamente lenguajes de pro-
gramación. No obstante, las gramáticas son muy útiles para realizar muchas tareas que son
mucho más comunes que analizar programas sintácticamente.
Si alguna vez intentaste usar regexes para analizar una pieza de texto HTML (o XML)1 ,
probablemente encontraste que se volvió casi imposible, excepto quizás por las datos de
HTML más fáciles. Para analizar cualquier pieza de tales datos, necesitas un analizador
sintáctico el cual, por su parte, estará basado en una gramática.
Si no te gustó la gramática en la escuela, no dejes que eso te intimide. Las gramáticas de
Perl 6 no son complicadas; ellas solo te permiten agrupar reglas nombradas, en la misma
forma que las clases te permiten agrupar métodos de código regular.
Una gramática crea un espacio de nombres (del inglés namespace) y se introduce con la pal-
abra clave grammar. Usualmente agrupa un número de reglas nombradas, en la misma
manera que una clase agrupa un número de métodos. Una gramática es actualmente
una clase que hereda de la superclase Grammar, la cual provee métodos tales como parse
para analizar sintácticamente una cadena de texto y .parsefile para analizar un archivo.
Además, puedes actualmente escribir algunos métodos en una gramática, e incluso impor-
tar algunos roles. Y, como veremos, las gramáticas son usualmente asociadas con algunas
clases de acciones o objetos de acciones.
A menos que se diga lo contrario, los métodos de análisis sintáctico buscarán una regla
nombrada por defecto llamada “TOP“ (la cual puede ser un regex nombrado, un token,
o una regla) para comenzar el análisis. Las reglas de análisis sintáctico de una fecha que
usamos más arriba pueden ensamblarse en una gramática de la siguiente manera:
1 No intentes hacerlo. Ya te lo advertí: no lo hagas.
270 Capítulo 13. Regexes y Gramáticas
grammar Mi-fecha {
rule TOP { \s*?
[ <año> (<separador>) <mes> $0 <día>
|| <día> (<separador>) <mes> $0 <año>
|| <mes>\s<día>',' <año>
] \s*
<!{ ($<día> > 30 and $<mes> == 2|4|6|9|11)}>
}
token año { \d ** 4 }
token mes { 1 <[0..2]> || 0 <[1..9]> }
token día { (\d ** 2) <?{1 <= $0 <= 31 }> }
token separador { '/' || '-' || '.' }
}
for " 2016/12/25 ", " 2016-02-25 ", " 31/04/2016 " -> $cadena {
my $coincidencia = Mi-fecha.parse($cadena);
say ~$coincidencia if defined $coincidencia;
}
Esto imprimirá:
2016/12/25
2016-02-25
La aserción de código dentro de la regla “TOP“ previene que fechas como “31/04/2016”
coincidan; necesitarías añadir más código para manejar los finales de las fechas de Febrero,
como hicimos en la solución del ejercicio anterior (ver la Subsección A.10.1) si esto es im-
portante. Podrías querer hacerlo como un ejercicio.
Además de eso, este código no es tan diferente de nuestro código anterior, pero hay algunos
cambios que son significativos.
Nombré la regla fecha como TOP porque este es el nombre por defecto que el método parse
busca para la regla en el nivel superior. Una gramática crea su propio espacio de nombres
y ámbito lexical, y ya no necesito declarar las reglas con el declarador my (el cual se requiere
para las reglas que se declaran fuera de una gramática).
Dentro de una gramática, el orden en el cual las reglas se definen es usualmente irrele-
vante, así que yo podría definir la regla TOP primero, aún si usa los tókenes que se definen
después (lo cual no habría sido posible con las reglas usadas fuera de una gramática). Esto
es importante porque, dentro de una gramática, puedes tener reglas que se llaman unas a
las otras (o reglas que se llaman a ellas mismas de forma recursiva), lo cual no sería práctico
si el orden de las definiciones de las reglas fuera importante
Si analizas la cadena de texto de entrada con el método .parse, la regla TOP es automáti-
camente anclada al inicio y al final de la cadena de texto, lo que significa que la gramática
tiene que coincidir con la cadena de texto completa para ser exitosa. Esta es la razón por la
cual tuvimos que añadir patrones para los espacios en blanco al inicio y al final de nuestra
regla TOP para coincidir con nuestras cadenas de texto que tienen espacios en blanco antes
y después de la fecha. Existe otro método, .subspace, el cual no tiene que alcanzar el final de
la cadena de texto para ser exitoso, pero todavía debemos tener el patrón para los espacios
en blanco al inicio de la regla.
13.6. Herencia de una Gramática 271
Considera esta gramática simple (casi simplista) para analizar sintácticamente un correo
electrónico:
grammar Mensaje {
rule TOP { <saludo> $<cuerpo>=<línea>+? <final> }
rule saludo { [Saludos||Hola||Hey] $<a>=\S+? ',' }
rule final { Nos vemos luego ',' $<desde>=.+ }
token línea { \N* \n}
}
my $coincidencia = Mensaje.parse($msg);
if defined $coincidencia {
say "Saludo \t= ", ~$coincidencia<saludo>.chomp;
say "Destinatario\t= $coincidencia<saludo><a>";
say "Autor \t= $coincidencia<final><desde>";
say "Contenido \t= $coincidencia<cuerpo>";
}
Ahora supón que queremos una gramática similar para analizar un mensaje más formal
y notamos que podemos reutilizar parte de la gramática Mensaje. Podemos tener nuestra
propia gramática hija que hereda de la gramática padre:
Veamos si funciona:
272 Capítulo 13. Regexes y Gramáticas
Esto imprimirá:
grammar GramAritmética {
token TOP { \s* <número> \s* <operación> \s* <número> \s*}
token operación { <[^*+/-]> }
token número { \d+ | \d+\.\d+ | \.\d+ }
}
class AccionesAritm {
method TOP($/) {
given $<operación> {
13.8. Una Gramática para Analizar JSON 273
posible, por ejemplo, el intercambio de las mismas entre plataformas diferentes y lenguajes
de programación diferentes, para enviarlas a través de la red, y almacenarlas permanente-
mente en archivos en los discos.
Los valores pueden ser objetos o arrays como se definieron anteriormente, o tipos básicos
de datos, tales como: cadenas de texto, números, Booleano ( true o false), y null (valor
vacío o valor indefinido). Una cadena de texto es una secuencia de caracteres Unicode
entre comillas, y los números son números decimales con signo que pueden contener una
parte fraccional y pueden usar la notación “E“ de exponentes.
my $cadena-JSON = '
{
"nombre": "Juan",
"apellido": "Duarte",
"estaVivo": true,
"edad": 25,
"direccion": {
"calle": "21 2nd Street",
"ciudad": "New York",
"estado": "NY",
"codigoPostal": "10021-3100"
},
"numerosTelefono": [
{
"tipo": "hogar",
"numero": "212 555-1234"
},
{
"tipo": "oficina",
"numero": "646 555-4567"
},
{
"tipo": "movil",
13.8. Una Gramática para Analizar JSON 275
13.8.3.1 Números
El ejemplo del documento JSON más arriba solo tiene números enteros y decimales, pero
debemos de ser capaces de reconocer números como “17,” “-138.27”, “1.2e-3”, “.35”, etc.
Podemos usar el siguiente token para hacer eso:
token número {
[\+|\-]? # signo (+/-) opcional
[ \d+ [ \. \d+ ]? ] # parte entera y parte fraccional opcional
| [ \. \d+ ] # o solo parte fraccional
[ <[eE]> [\+|\-]? \d+ ]? # exponente opcional
}
Existen muchos patrones para definir una cadena de texto. Para nuestro documento de
JSON, la siguiente regla será suficiente:
token cadena {
\" <[ \w \s \- ' ]>+ \"
}
Esto coincidirá con una secuencia entre comillas dobles de caracteres alfanuméricos, espa-
cios, guiones, y apóstrofos.
Para un analizador sintáctico real de JSON, una regla que usa una categoría negativa de
caracteres que excluye cualquier cosa que no pertenece a una cuerda podría ser mejor, por
ejemplo:
token cadena {
\" <-[\n " \t]>* \"
}
276 Capítulo 13. Regexes y Gramáticas
i.e., una secuencia entre doble comillas de caracteres excepto comillas dobles, nuevas líneas
y tabuladores.
Podrías desear estudiar los estándares de JSON2 para averiguar exactamente lo que es
aceptado o prohibido en una cadena de texto de JSON. Para nuestros propósitos, la primera
regla más arriba será suficiente.
Podemos ahora usar una característica de regex que no hemos visto. Esta es el modificador
de cuantificador, que podemos usar para simplificar la regla listaPar. Para coincidir con
cosas como valores separados por comas más fácilmente, puedes agregar un modificador %
a cualquiera de los cuantificadores regulares para especificar un separador que debe ocurrir
entre cada una de las coincidencias. Así que, por ejemplo, /a+ % ','/ coincidirá con “a“
o “a,a“, o “a,a,a“, etc.
Por lo tanto, la regla listaPar puede escribirse de la siguiente manera:
o:
grammar Gramática-JSON {
token TOP { \s* [ <objeto> | <array> ] \s* }
rule objeto { '{' \s* <listaPar> '}' \s* }
rule listaPar { <par> * % \, }
rule par { <cadena>':' <valor> }
rule array { '[' <listaValor> ']'}
rule listaValor { <valor> * % \, }
token cadena { \" <[ \w \s \- ' ]>+ \" }
token número {
[\+|\-]?
[ \d+ [ \. \d+ ]? ] | [ \. \d+ ]
[ <[eE]> [\+|\-]? \d+ ]?
}
token valor {
| <objeto> | <array> | <cadena> | <número>
| true | false | null
}
}
Podemos ahora probar la gramática con nuestra muestra de cadena de texto de JSON y
tratar de imprimir el objeto de coincidencia:
my $coinc = Gramática-JSON.parse($cadena-JSON);
say ~$coinc if $coinc;
{
"nombre": "Juan",
"apellido": "Duarte",
"estaVivo": true,
"edad": 25,
"direccion": {
"calle": "21 2nd Street",
"ciudad": "New York",
"estado": "NY",
"codigoPostal": "10021-3100"
},
"numerosTelefono": [
{
"tipo": "hogar",
"numero": "212 555-1234"
},
{
"tipo": "oficina",
"numero": "646 555-4567"
278 Capítulo 13. Regexes y Gramáticas
},
{
"tipo": "movil",
"numero": "123 456-7890"
}
],
"infantes": [],
"esposa": null,
"cuentaBancaria": {
"credito": 2342.25
}
}
class acciones-JSON {
method TOP($/) {
make $/.values.[0].made;
};
method objeto($/) {
make $<listaPar>.made.hash.item;
}
13.8. Una Gramática para Analizar JSON 279
method listaPar($/) {
make $<par>».made.flat;
}
method par($/) {
make $<cadena>.made => $<valor>.made;
}
method array($/) {
make $<listaValor>.made.item;
}
method listaValor($/) {
make [$<valor>.map(*.made)];
}
method cadena($/) { make ~$0 }
method número($/) { make +$/.Str; }
method valor($/) {
given ~$/ {
when "true" {make Bool::True;}
when "false" {make Bool::False;}
when "null" {make Any;}
default { make $<val>.made;}
}
}
}
Para que esta clase de acciones funcione, necesitamos hacer un pequeño cambio a la
gramática. El método valor usa una captura nombrada val para acceder su contenido;
necesitamos agregar las capturas nombradas relevante al token valor en la gramática:
También necesitamos añadir paréntesis de captura al token cadena para que el método
cadena sea capaz de extraer la cadena de texto correspondiente:
my $j-acciones = acciones-JSON.new();
my $coinc = Gramática-JSON.parse($cadena-JSON, :actions($j-acciones));
say $coinc.made;
Observa que, aquí, hemos usado un objeto de acciones en lugar de simplemente una clase
de acciones, pero esto es solo con el propósito de mostrar cómo hacerlo; podríamos haber
usado la clase directamente como antes.
La última sentencia en el código más arriba imprime el AST. Hemos reformateado la salida
para mostrar mejor la estructura del AST:
{
cuentaBancaria => {
280 Capítulo 13. Regexes y Gramáticas
En este caso, la estructura superior es un hash (también podría ser una array con una
diferente cadena de texto de JSON). Podemos ahora explorar este hash para encontrar los
datos que son de interés para nosotros. Por ejemplo:
Algunos Valores:
Juan
Duarte
True
New York
Números telefónicos:
(hogar 212 555-1234)
(oficina 646 555-4567)
(movil 123 456-7890)
13.9. Herencia y las Gramáticas Mutables 281
grammar Mi-Fecha {
token TOP { \s* <año> '-' <mes> '-' <día> }
token año { \d ** 4 }
token mes { 1 <[0..2]> || 0 <[1..9]> }
token día { (\d ** 2) <?{1 <= $0 <= 31 }> }
282 Capítulo 13. Regexes y Gramáticas
}
my $cadena = " 2016-07-31 ";
say so Mi-Fecha.parse($cadena); # -> False
Estos tres tókenes parecen funcionar apropiadamente. En este punto, podrías adivinar
dónde se encuentra el problema, pero asumamos que no sabes.
Necesitamos depurar el token “TOP“. Podemos usar el método común de depuración de
imprimir donde estamos a varias etapas del programa. Puedes insertar una sentencia de
impresión en una regla nombrada. Intentemos cambiar el token TOP a esto:
año coincidido
mes coincidido
día coincidido
Incluso el token “TOP“ parece funcionar hasta el final. En este punto, deberíamos ser capaz
de deducir que nos falta el espacio final en el token “TOP“.
Así que podríamos agregar un espacio adicional al final del token:
o cambiar la regla:
o era posible probar la cadena de texto que era incorrecta (porque no podría tener espacios)
y por lo tanto, necesitaba ser arreglada.
13.11. Glosario 283
Si tiene una clase de acciones, también puedes agregar sentencias de impresión a los méto-
dos de acciones.
Igualmente recuerda que el depurador de Perl 6 (ver la Sección 12.14) puede ser muy útil.
Brevemente mostramos en la Subsección 12.14.6 (p. 260) cómo recorrer una coincidencia de
regex paso a paso. La mayor parte de lo que se describió ahí también aplica a la depuración
de gramáticas.
use Grammar::Tracer;
a tu programa, entonces cualquier gramática dentro del ámbito lexical imprimirá infor-
mación de depuración sobre las reglas con la cual intentó coincidir, aquellas que fueron
exitosas y aquellas que fallaron.
use Grammar::Debugger;
para hacer lo mismo paso a paso. Solo escribe “h“ en la prompt para la lista de comandos
disponibles.
13.11 Glosario
Gramática Una herramienta de alto nivel para realizar análisis léxico y gramático de un
texto estructurado. En Perl 6, una gramática es específicamente un espacio de nom-
bres que contiene una colección de reglas nombradas destinada a este tipo de análisis.
Objeto de coincidencia En Perl 6, un objeto (de tipo Match), denotado usualmente por $/,
el cual contiene información (algunas veces muy) detalla sobre lo que coincidió exi-
tosamente con un regex o una gramática. El objeto de coincidencia $/ se le asignará
Nil si la coincidencia falla.
Captura El hecho por el cual partes de una cadena de texto que coinciden con un regex (o
una gramática) pueden extraerse a través del uso de un número de variables espe-
ciales.
284 Capítulo 13. Regexes y Gramáticas
Regla En términos generales, las reglas nombradas son regexes que usan una sintaxis de
método y usualmente se almacenan en una gramática. Más específicamente, una
categoría de estas reglas nombradas (en conjunto con los regexes nombrados y los
tókenes).
Árbol de sintaxis abstracta (AST) Una estructura de datos que usualmente resume el ob-
jeto de coincidencia y se usa para la explotación de datos útiles. El objeto de coin-
cidencia es poblado automáticamente por Perl, donde ek AST contiene información
considerada útil y explícitamente insertada por el programador.
Clases de acciones Una clase usada en conjunción con una gramática para realizar ciertas
acciones cuando una regla de una gramática coincide con algo en los datos de en-
trada. Si un método con el mismo nombre de una regla en la gramática existe en la
clase de acciones, entonces será invocado siempre que la regla coincide.
• Expresiones con varios operadores diferentes (entre los cuatro operadores aritméticos
básicos) y operandos múltiples
• Reglas estándares de precedencia entre los operadores (por ejemplo, las multiplica-
ciones deben hacerse antes que las adiciones)
Estos son algunos ejemplos de expresiones que la calculadora debería analizar sintáctica-
mente y calcular correctamente:
3 + 4 + 5;
3 + 4 * 5; # resultado debería ser 23
(3 + 4) * 5; # resultado debería ser 35
Ejercicio 13.2. Tu misión, [Juan|Salomé], así elijas aceptarla, es escribir tal gramática. Como es
usual, si fallas, el Gobierno debería negar cualquier conocimiento de tu clase de acciones.
Hay varias maneras posibles de lograr esto; la solución presentada en la Sección A.10.2 es solo una
de ellas.
14
Programación Funcional en Perl
Perl no es un lenguaje funcional dado que también usa otros paradigmas de programación
que hemos visto en abundancia en este libro. Sin embargo, Perl ofrece características y
capacidades extensivas de la programación funcional, algunas de las cuales han sido intro-
ducidas en varias secciones de este libro y que revisaremos brevemente antes de entrar en
más detalles.
El sigilo & colocado antes del nombre de la subrutina saludar en la lista de argumentos
(como también antes del parámetro code en la signatura y en el cuerpo de la subrutina
do-twice) le dice a Perl que estás pasando una subrutina u otro objeto código que puede
ser llamado.
En la ciencia de la computación, una subrutina que puede tomar otra subrutina como un
argumento es algunas veces llamada una función de orden superior.
Más ejemplos interesantes de funciones de orden superior pueden encontrarse con las fun-
ciones reduce, map, y grep que estudiamos en la Sección 9.8 (p. 161), al igual que la función
sort (Sección 9.11 y Sección 9.12).
Consideremos por ejemplo la tarea de ordenar registros por fechas, los cuales consisten
de un identificador seguido por una fecha en el formato DD-MM-YYYY, tal como “id1;13-
05-2015” o “id2;17-04-2015”. Los registros necesitan un poco de transformación antes que
podamos compararlos con el propósito de encontrar el orden cronológico en el cual de-
berían ser ordenados, así que podríamos tener una función separada para la comparación:
Cada registro modificado se construye al encadenar tres funciones. Estas líneas deberían
leerse de derecha a izquierda: primero, el valor de entrada se separa en cuatro artículos;
estos artículos son después invertidos y consecuentemente unidos. Así que el resultado
para “id1;13-05-2015” es “2015,05,13,id1”, el cual es adaptado para una comparación con
el operador cmp. Más tarde regresaremos a esta forma de programación de tubería y otras
maneras de realizar estas operaciones.
Ahora podemos pasar la subrutina comparar a la función sort:
Esto muestra:
14.1. Funciones de Orden Superior 287
id4;12-01-2015
id2;17-04-2015
id1;13-05-2015
id3;21-02-2016
Por favor nota que esto se provee como un ejemplo de funciones de retrollamada usadas
con la función integrada sort. Veremos al final de la siguiente subsección una manera más
simple de lograr el mismo tipo de ordenamiento usando una función anónima.
my $saludar = sub {
say "Hola Mundo!";
};
do-twice $saludar; # imprime "Hola Mundo!" dos veces
Dado que nuestra subrutina anónima no toma ningún argumento y tampoco devuelve un
valor útil, podemos simplificar la sintaxis aún más y pasar un bloque de simple código
anónimo a do-twice:
Ya has visto varios ejemplos útiles de subrutinas anónimas en este libro (ver Sección 9.8
para más detalles):
• Con la función reduce para computar la suma de los primeros 20 números enteros:
my $suma = reduce { $^a + $^b }, 1..20; # -> 210
• Con la función map para convertir la primera letra de una lista de ciudades en mayús-
cula (usando la función integrada tc):
> .say for map {.tc}, <londres parís roma washington madrid>;
Londres
París
Roma
Washington
Madrid
• Con la función grep para generar una lista de números pares al filtrar los números
impares:
my @pares = grep { $_ %% 2 }, 1..17; # -> [2 4 6 8 10 12 14 16]
288 Capítulo 14. Programación Funcional en Perl
Esta es otra forma de lambda donde el parámetro de la “función“ es definido por la variable
de bucle de un bloque puntiagudo.
El ejemplo de ordenamiento presentado en la Subsección (14.1.1) más arriba puede también
escribirse con un bloque de código anónimo (tomando así ventaja de la sintaxis de sort y
usando un bloque de código con un solo argumento descrito en la Sección 9.12):
Aquí, el bloque de código un poco extenso pasado como un argumento a la función sort
es una lambda.
14.1.3 Clausuras
En la programación de computadoras, una clausura (o clausura lexical) es una función que
puede acceder el contenido de las variables que están lexicalmente disponibles donde la
función es definida, aún si aquellas variables ya no se encuentran en el ámbito en la sección
de código donde la función es llamada.
Considera el siguiente ejemplo:
}
my &contar-desde-cinco = crear-contador(5);
say &contar-desde-cinco() for 1..6; # imprime los números del 5 al 10
Podríamos simplificar crear-contador aún más con el uso de sentencias return implícitas:
pero esto es menos claro porque la intención del código es menos explícita.
El último ejemplo crear-fifo en la solución al ejercicio sobre la cola FIFO (Subsec-
ción A.7.1.6) es otro ejemplo que utiliza el mismo mecanismo:
sub crear-fifo {
my @cola;
return (
sub {return shift @cola;},
sub ($elem) {push @cola, $elem;}
);
}
my ($fifo-consigue, $fifo-pone) = crear-fifo();
$fifo-pone($_) for 1..10;
print " ", $fifo-consigue() for 1..5; # -> 1 2 3 4 5
290 Capítulo 14. Programación Funcional en Perl
En Perl 6, todas las subrutinas son clausuras, lo cual significa que todas las subrutinas
tienen acceso a las variables lexicales que existían en el entorno al momento de la definición
de las subrutinas. Sin embargo, ellas no actúan necesariamente como clausuras.
De hecho, todos los objetos de código, incluyendo simples bloques de código anónimo,
pueden actuar como clausuras, lo cual significa que pueden hacer referencia a variables
lexicales desde el ámbito externo, y en efecto esto es lo que pasa con la variable de bucle de
un bloque puntiagudo o en el siguiente bloque map:
my $multiplicador = 7;
say map { $multiplicador * $_ }, 3..6; # -> (21 28 35 42)
En este ejemplo, el bloque pasado a map hace referencia a la variable $multiplicador desde
el ámbito externo, convirtiendo al bloque en una clausura.
Los lenguajes sin clausuras no pueden proveer fácilmente funciones de orden superior que
sean tan poderosas y fáciles de usar como map.
Aquí presentamos otro ejemplo de un bloque que actúa como una clausura para la imple-
mentación de un contador:
my &cuenta;
{
my $contador = 10;
&cuenta = { say $contador++ };
}
&cuenta() for 1..5;
Esta clausura guarda una referencia a la variable $contador cuando la clausura es creada.
La llamada al bloque de código &cuenta muestra y actualiza a $contador exitosamente.
Esto sucede aún cuando la variable ya no se encuentra en el ámbito lexical al momento que
el bloque es ejecutado.
Hemos discutido varios ejemplos donde estas funciones pueden usarse conjuntamente en
un tipo de tubería de datos en la cual los datos producidos en cada paso de la tubería
son suministrados al siguiente paso. Por ejemplo, anteriormente en este capítulo (Subsec-
ción 14.1.1), usamos esto:
14.2. Procesamiento de Listas y Programación de Tuberías 291
Como destacamos anteriormente, este tipo de código debería leerse de derecha a izquierda
(y de abajo hacia arriba si está escrito en varias líneas de código): $reg1 es suministrada
a split, la cual divide el dato en cuatro piezas; las piezas son después invertidas (con
reverse) y suministradas a join para crear un solo dato donde las piezas están ahora en
orden inverso.
Similarmente, podríamos producir una lista de mascotas que pertenecen a mujeres solteras
que viven en Kansas con el siguiente código que encadena varios métodos:
my @mascotas-de-mujeres-solteras-de-kansas =
map { .mascotas },
grep { !.está-casada },
grep { .género eq "Femenino" },
grep { .estado eq "Kansas" },
@ciudadanos;
Esta tubería debería leerse de abajo hacia arriba. Toma una lista de todos los ciudadanos,
filtra aquellos de Kansas que son mujeres, filtra aquellas que no están casadas, y finalmente
genera la lista de mascotas de dichas personas. Nota que .mascotas puede devolver un
animal, una lista de animales, o una lista vacía. La función map “aplana“ las listas hasta ese
entonces producidas, así que el resultado final que termina en el array es una lista plana de
animales (y no una lista anidada de listas).
Estas tuberías son muy poderosas y expresivas, y pueden hacer muchas cosas en pocas
líneas de código.
"id1;13-05-2015"
==> split(/<[;-]>/)
==> reverse()
==> join(",")
==> my @out; # @out es una array que contiene una artículo:
# una cadena de texto.
say @out.perl; # ["2015,05,13,id1"]
Por cierto, si estás usando tales operaciones de tubería con una entrada bien grande, de-
pendiendo de la arquitectura de tu plataforma, Perl 6 puede ser capaz de ejecutar estas
múltiples operaciones en paralelo en diferentes CPUs o núcleos (cores), mejorando signi-
ficativamente el rendimiento del proceso en su totalidad.
292 Capítulo 14. Programación Funcional en Perl
Existe también un operador backward feed (suministro inverso), <==, que posibilita la escrit-
ura de la tubería en orden inverso:
(Nota que esto produce el resultado correcto hasta para el caso del factorial de 1, el cual es
definido matemáticamente como 1.)
14.2.3 El Hiperoperador
Un hiperoperador aplica el operador especificado a cada artículo de una lista (o dos listas
en paralelo) y devuelve una lista modificada (similar a la función map). Dicho hiperoper-
ador usa las comillas francesas o alemanas, « » (Unicode codepoints U+00AB y U+00BB),
pero puedes usar las comillas angulares, << >>, si así lo deseas (o si no sabes como entrar
estos caracteres Unicode con tu editor).
Nuestro primer ejemplo multiplicará cada elemento de una lista por un número dado (5):
my @b = 6..10;
my @c = 5 <<*>> @b;
say @c; # imprime 30 35 ... 50
# resultado (5*6, 5*7, ...)
Podemos también combinar dos listas y, por ejemplo, agregar los valores respectivos:
my @a = 1..5;
my @b = 6..10;
my @d = @a >>+<< @b;
say @d; # -> [7 9 11 13 15]
14.2. Procesamiento de Listas y Programación de Tuberías 293
• Dado que la función integrada lcm devuelve el mínimo común múltiplo entre dos
números, escribe un programa que muestre el número positivo más pequeño divisi-
ble por todos los números entre 1 y 20.
• Escribe un programa que calcule la suma de todos los dígitos del factorial de 100.
Nuevamente, no prosigas con la lectura hasta que hayas tratado de resolver estos pequeños
problemas (y con suerte lo hayas logrado).
El operador de reducción facilita la aplicación de un operador a todos los elementos de una
lista. Así que si lo usamos con la función lcm nos dará el mínimo común múltiplo (MCM)
entre 1 y 20:
Para la suma de los dígitos del factorial de 100, usamos el metaoperador de reducción []
dos veces, una con el operador de multiplicación para calcular el factorial de 100, y otra
con el operador de adición para añadir los dígitos del resultado:
Para el cuadrado de la suma menos la suma de los cuadrados, es fácil calcular la suma de
los 100 primeros números enteros con el operador de reducción. El hiperoperador <<...>>
fácilmente suministra una lista de los cuadrados de estos enteros, y otra aplicación del
operador de reducción reduce esta lista a una suma:
• Prefijo (prefix): un operador unario colocado antes del operando, por ejemplo el signo
de menos en la expresión −1
• Sufijo (postfix): un operador unario colocado después del operando, por ejemplo el
signo de exclamación usado como un símbolo matemático para el factorial: 5!
• Circunfijo (circumfix): un operador compuesto de dos símbolos alrededor del/de los
operando(s), por ejemplo los paréntesis (...) o los paréntesis angulares < ... >
• Poscircunfijo (postcircumfix): un operador compuesto de dos símbolos colocados de-
spués de un operando y alrededor de otro operando, por ejemplo los corchetes en
@a[1]
Para declarar un nuevo operador, usualmente necesitas especificar los siguientes elementos
en el orden especificado:
Observa que el signo de exclamación usado como un operador prefijo (i.e., colocado al
frente de su operando) es el operador de negación, pero usualmente no es posible con-
fundir los dos operadores porque uno es un operador prefijo y nuestro operador es un
operador sufijo (aunque debes ser cuidadoso con la posición donde pones los espacios en
blanco si tu expresión es algo complicada). La palabra multi no es estrictamente requerida
en este ejemplo, pero es probablemente buena práctica colocarla, solo para cubrir los casos
cuando es necesaria.
Al igual que el otro ejemplo, podríamos definir el operador Σ (suma) de la siguiente man-
era:
296 Capítulo 14. Programación Funcional en Perl
El beneficio de usar el operador Σ sobre + puede no ser directamente obvio, pero es algunas
veces útil crear un “lenguaje de dominio específico“(DSL), i.e., un sublenguaje específica-
mente adaptado para un contexto o área de estudio específica (por ejemplo, matemáticas
o química), lo cual permite que un problema o solución en particular sea expresada más
claramente que lo que el lenguaje principal existente permitiría. En Perl 6, las gramáticas y
la facilidad de crear nuevos operadores hace la creación de un DSL una tarea sencilla.
El nuevo operador no tiene que ser declarado entre los paréntesis angulares. Las siguientes
declaraciones podrían usarse para definir un operador de adición:
infix:<+>
infix:<<+>>
infix:«+»
infix:('+')
infix:("+")
En el artículo de Knuth, esto es solo un atajo falso para discutir más fácilmente el algo-
ritmo de ordenamiento rápido (quicksort en inglés) (descrito en el ejercicio ), pero podemos
implementar fácilmente ese símbolo:
my ($c, $d) = 2, 5;
say $c :=: $d; # -> (5 2)
# usándolo para intercambiar dos elementos del array
my @e = 1, 2, 4, 3, 5;
@e[2] :=: @e[3];
say @e; # -> [1 2 3 4 5]
Ahora, el pseudocódigo de más arriba funciona tan bien como código real. Un algoritmo
de ordenamiento como el presentado más abajo (Sección 14.3.2) puede típicamente tener
líneas como estas para intercambiar dos elementos en un array:
if $alguna-condición {
my ($x, $y) = @array[$i], @array[$i + gap];
@array[$i], @array[$i + gap] = $y, $x;
}
Un punto final muy interesante. Supón que queremos usar el operador ⊕ para la unión
matemática de conjunto entre dos hashes. Esto podría escribirse fácilmente como sigue:
Hasta ahora, todo bien, nada realmente nuevo. Pero el nuevo operador infijo ⊕ se ha vuelto
casi similar a un operador integrado de Perl, y por lo tanto podemos usarlo conjuntamente
con el metaoperador de reducción:
Todo funciona como si este nuevo operador fuera parte de la gramática de Perl 6. Y eso
es lo que, en efecto, ha pasado aquí: hemos extendido el lenguaje con un nuevo operador.
Esta posibilidad de extender el lenguaje es una parte clave de la habilidad de Perl 6 para
afrontar futuras necesidades que ni siquiera tenemos en cuenta en el momento presente.
Primeramente, intentemos escribir en puro Perl la función map. Necesitamos tomar una
subrutina o un bloque de código como su primer argumento, aplicarlo a un array o una
lista, y devolver la lista modificada.
En el primer intento, esto funciona exactamente como se espera. (He intentado el mismo
experimento con otros lenguajes de programación en el pasado, incluyendo Perl 5; tomó
varios intentos antes de obtener la versión correcta del programa, especialmente lo con-
cerniente a la sintaxis de la llamada. Aquí, todo cae en su lugar naturalmente.) Para ser
honesto, las condiciones en este ejemplo son muy limitadas y podrían haber casos donde
mapa no funciona de la misma forma exactamente que map; la lección a tomar es que es
bien fácil construir una subrutina de orden superior que se comporta esencialmente en la
misma manera que map.
14.3.1.2 mi-grep
Para que funcione como sort, una función de ordenamiento puede recibir como parámet-
ros una función de comparación o bloque de código y un array para ser ordenado, y la
rutina de comparación debería usar parámetros marcadores ($^a y $^b en el código más
abajo). Esta es una posible implementación básica:
1 Merge sort se presenta en más detalles en la sección 14.7.1.
2 Quick sort se presenta en más detalles en 14.10
300 Capítulo 14. Programación Funcional en Perl
my @v;
my $max = 500;
@v[$_] = Int(20000.rand) for (0..$max);
El bucle interno compara los artículos que están distantes de cada uno por los valores
$brecha, y los intercambia si no están en el orden correcto. Al comienzo, $brecha es
grande, y se divide por una factor de retracción en cada iteración del bucle externo. El
rendimiento depende inmensamente en el valor del factor de retracción. Al final, la brecha
es 1 y comb sort es equivalente a un ordenamiento de burbuja (bubble sort en inglés). El
factor óptimo de retracción se encuentra entre 1.25 y 1.33; he usado un factor de retrac-
ción de 1.3, el cual es el valor sugerido por los autores de las publicaciones originales que
presentan el algoritmo.
con map, pero podría igualmente ser considerado como una forma de map con evaluación
retardada, la cual procesa solo los elementos de la lista de entrada que son necesarios para
el programa y nada más.
Para reflejar estas consideraciones, llamaremos nuestra subrutina iter-map. Dado que
podríamos también escribir una subrutina iter-grep y posiblemente otras, escribiremos
un iterador y un transformador de datos separadamente.
Ahora que el iterador devuelve un valor a la vez, podemos escribir la subrutina iter-map:
Dado que hemos llamado la función iter-map solo 5 veces, ha hecho el trabajo de multi-
plicar los valores por 2 solo 5 veces, en lugar de hacerlo 200 veces, de las cuales 195 serían
para nada. Por supuesto, multiplicar un número por 2 no es una operación costosa y el
array no es tan largo, pero esto muestra como la pereza puede prevenir computaciones
innecesarias. Regresaremos a esta idea, dado que Perl 6 ofrece soporte nativo de las listas
perezosas y el procesamiento perezoso.
Como ya notamos, una ventaja adicional de usar una función como iter-map es la posibil-
idad de usar listas infinitas virtualmente. Esta implementación que usa una lista infinita
funciona como antes:
my $iterador = crear-iter(1..*);
say iter-map { $_ * 2 }, $iterador() for 1..5;
# imprime 2, 4, 6, 8, 10
302 Capítulo 14. Programación Funcional en Perl
El @array ahora:
[1 2 3 4 5 (Any) (Any) 15 (Any) 17 20 21 22]
1
3
5
15
17
21
¡Array de entrada agotado!
Esto es lo que habría sido necesario para implementar iteradores perezosos en las versiones
anteriores de Perl (e.g., Perl 5), pero no se requiere todo esto con Perl 6, dado que el mismo
tiene soporte integrado para las listas perezosas y operadores perezosos, como veremos
más adelante.
Por ejemplo, el código siguiente devuelve una lista de números igual a los números pares
entre 1 y 10 multiplicados por 3:
my @lista = gather {
for 1..10 {
take 3 * $_ if $_ %% 2
}
};
say @lista; # -> [6 12 18 24 30]
Aquí, gather itera sobre los valores del rango y take “devuelve“ los valores deseados.
Si lo contemplas, el código de más arriba parece ser una forma de combinación de la fun-
ciones map y grep.
Dado que take también acepta una sintaxis de método, esto podría ser escrito así:
Estos ejemplos de código no aportan ninguna ventaja sobre map o grep, pero ilustran cómo
un bloque gather { take } puede ser considerado como una generalización de las fun-
ciones map y grep. Y, como ya lo mencionamos, el primer ejemplo en esta sección actual-
mente combina las acciones de una función map y una función grep.
Escribir una nueva versión de my-grep es bien fácil y se deja como un ejercicio al lector.
Llamar la función take solo hace sentido dentro del contenido de un bloque gather, pero
no tiene que ser dentro del bloque (o dentro del ámbito léxico del bloque gather); puede
ser dentro del ámbito dinámico del bloque gather.
Aunque no hemos tratado este concepto anteriormente, Perl tiene la noción de ámbito
dinámico: contrario al ámbito léxico, el ámbito dinámico no solo contiene el bloque actual,
sino que también las subrutinas llamadas desde dentro del bloque actual. Las variables
con ámbito dinámico usan el twigil “*“. Este es un ejemplo:
my @lista = gather {
computar-val($_) for 1..10;
}
sub computar-val( Numeric $x ) {
take $x * $x + 2 * $x - 6;
}
say @lista[0..5]; # -> (-3 2 9 18 29 42)
Como puedes ver, la función take no es llamada dentro del bloque gather, pero funciona
normalmente porque está dentro del bloque dinámico del bloque gather, i.e., dentro de la
subrutina computar-val, la cual es llamada en el bloque gather.
Un último ejemplo mostrará lo poderosa que la construcción gather { take } puede ser.
Consideremos este problema publicado en el sitio Rosetta Code (http://rosettacode.
org/wiki/Same_Fringe): escribe una subrutina que comparará las hojas de dos árboles
binarios para determinar si son las mismas listas de hojas cuando se visitan de izquierda a
306 Capítulo 14. Programación Funcional en Perl
derecha. La estructura o balance de los árboles no importa; solo el número, orden, y valor
de las hojas es importante.
La solución en Perl 6 usa un bloque gathe { take } y consiste de solo seis líneas de
código:
Perl 6 es el ganador indisputable en términos del código más conciso que soluciona el
problema.
Como una comparación, el ejemplo en Ada es casi 300 líneas en tamaño, los programas
en C y Java tienen alrededor de 100 líneas. Por cierto, las soluciones más concisas a parte
de Perl 6 (Clojure, Picolisp, Racket) tienen alrededor de 20 líneas y todos son lenguajes de
programación funcional, o (para Perl 5 por ejemplo) están escritas usando conceptos de
la programación funcional. Aunque el número de líneas de código es solamente uno de
muchos criterios para comparar programas y lenguajes, esto es mi opinión honesta un tes-
timonio en favor del paradigma de programación funcional y su expresividad intrínseca.
produce una lista perezosa de enteros sucesivos entre 0 y 200. El compilador de Perl 6
puede o no puede alocar algunos de los números (dependiendo en la implementación),
pero se requiere que produzca una lista completa inmediatamente. Los números que aún
no han sido generados pueden ser creados y suministrados más tarde, cuando el programa
intente usar dichos valores.
Como se explica más abajo, si quieres generar enteros consecutivos, puedes actualmente
simplificar la definición de la lista perezosa:
Si asignas una secuencia a un array, todos los valores de la secuencia se generarán inmedi-
atamente, dado que la asignación a un array es ansiosa (eager en inglés) (no perezosa). No
obstante, puedes forzar la pereza con la función integrada lazy al asignar a un array:
14.5. Listas Perezosas y el Operador de Secuencia 307
Puede ser que recuerdes que, en la Sección 9.1 del capítulo sobre arrays y listas, dijimos
que los paréntesis no son usualmente necesarios para construir una lista, a menos que sean
necesarios por razones de precedencia. El código más arriba es uno de tales ejemplos: in-
tenta ejecutar ese código sin los paréntesis y observa el contenido de las variables $impares
y $pares.
Cuando los tres números iniciales en una progresión geométrica son suministrados, el op-
erador de secuencia producirá una secuencia geométrica, como en este ejemplo que pro-
duce las potencias de dos:
El operador de secuencia puede también ser usado para producir números que no son
enteros, como mostramos en este ejemplo en la REPL:
El operando Inf se conoce como “Texas“ o su equivalente ASCII del símbolo de infinidad
∞.
El código anterior podría escribirse:
La manera más común de indicar una lista perezosa infinita es con el uso del argumento
Whatever *:
my $a;
my @fact = $a = 1, { $_ * $a++ } ... *;
say @fact[0..8]; # -> (1 1 2 6 24 120 720 5040 40320)
Esta estrategia es mucha más eficiente que aquellas que hemos visto antes para uso repeti-
tivo, dado que automáticamente almacena (caché) los valores previamente computados en
una array perezoso. Como podrás recordar de la Sección 10.9 (p. 188), caché es el proceso
por el cual un valor se almacena en la memoria para evitar tener que calcularlo nueva-
mente, con el objetivo de ahorrar tiempo y ciclos de CPU.
Y podemos de igual forma construir una lista infinita perezosa de los números Fibonacci:
14.5. Listas Perezosas y el Operador de Secuencia 309
Esto puede escribirse en una manera más concisa (aunque posiblemente menos explícita y
menos clara) usando el parámetro marcador Whatever *:
my @fibo = 0, 1, * + * ... *;
say @fibo[^10]; # -> (0 1 1 2 3 5 8 13 21 34)
Al igual que el factorial, esto es más eficiente que las implementaciones que hemos visto
previamente, porque los valores computados son almacenados en el array perezoso.
Similarmente la secuencia de números enteros impares al principio de esta sección podría
generarse en una forma un poco más concisa con el parámetro Whatever "*":
Esta sintaxis con un asterisco se llama la clausura Whatever; regresaremos a ella más ade-
lante.
Sin embargo, al usar el operador de secuencia, hay algo que se debe tener en cuenta: el
valor final (cota superior) tiene que ser uno de los números generados para que la lista pare
en ese valor final. Por lo contrario, el operador de secuencia construirá una lista infinita:
Como podemos observar en este ejemplo, el generador “salta sobre el valor final“ (va más
allá de 10), y la lista es de hecho infinita. Usualmente, esto no es un problema en términos
de los recursos de la computadora, dado que es una lista infinita perezosa, pero es prob-
ablemente un error si esperabas que la lista no sobrepasara el número 10. En este caso
específico, es muy fácil computar un punto final que coincidirá (e.g., 8 or 12), pero puede
ser más complicado encontrar un punto final válido. Por ejemplo, no es algo trivial deter-
minar el máximo número Fibonacci menor que 10,000 sin primero calcular la serie de tales
números hasta el primer número que sea mayor que 10,000.
En tales casos donde es difícil predecir cuál debería ser el punto final, podemos definir
otro bloque de código para determinar si la secuencia debería terminar o continuar. La
secuencia parará si el bloque devuelve un valor verdadero. Por ejemplo, para computar
los números Fibonacci hasta 100, podríamos usar esto en el REPL:
Esto es mejor, dado que termina la serie de números, pero no donde queríamos: queríamos
que la serie parara en el último número Fibonacci inmediatamente menor que 100, y es-
tamos obteniendo uno más. Sería bien fácil remover o filtrar el último número Fibonacci
generado, pero es aún mejor si no lo generamos. Un pequeño cambio en la sintaxis nos
ayuda a lograr el resultado esperado:
Hacer el cambio de ... a ...^ significa que la lista resultante no incluye el primer elemento
para el cual la prueba de terminación devolvió un valor verdadero.
En algunos lenguajes de programación puros, una función puede solo tomar un argumento
y devolver un resultado. La currificación es una técnica creada para afrontar tal limitación.
Perl no posee dicha limitación, pero la currificación puede todavía ser muy útil para reducir
y simplificar las listas de argumentos en las llamadas de una subrutina, notablemente en
casos de llamadas recursivas repetidas.
Una versión currificada de esto sería otra función añadir_y(x) que devuelve una función
que añade y a su argumento.
my &añadir_3= hacer-adición 3;
say &añadir_2(6); # -> 9
Esto no es nada nuevo: &añadir_2 y &añadir_3 son solo clausuras que recuerdan el valor
de incremento pasado a la subrutina hacer-adición. Esto puede ser útil cuando algunas
funciones se llaman muchas veces (o recursivamente) con muchos argumentos, algunos de
los cuales son los mismos: la currificación de las mismas permite simplificar las llamadas
de las subrutinas.
El método assuming devuelve un objeto callable (es decir, que puede llamarse) que im-
plementa el mismo comportamiento de la subrutina original, pero que tiene los valores
pasados a assuming ya atados a los parámetros correspondientes.
Es también posible currificar funciones integradas. Por ejemplo, la función integrada
substr toma normalmente tres argumentos: la cadena de texto sobre la cual opera, la posi-
ción inicial, y la longitud de la subcadena de texto a ser extraída. Podrías tener que realizar
un número de extracciones sobre la misma cadena de texto. Puedes crear una versión cur-
rificada de substr siempre trabajando sobre la misma cadena de texto:
Esto imprimirá:
Cogito
Cogito, ergo
Cogito, ergo sum
Nota que hemos “asumido“ dos parámetros aquí, así que la subrutina currificada “re-
cuerda“ los dos primeros argumentos y solo el tercer argumento necesita pasarse a
&cadena-inicio.
Incluso puedes currificar los operadores de Perl 6 (o aquellos que creas) si así lo deseas:
my &añadir_2 = &infix:<+>.assuming(2);
312 Capítulo 14. Programación Funcional en Perl
my &tercio = * / 3;
say tercio(126); # -> 42
El Whatever star (*) es un marcador para un argumento, así que la expresión devuelve una
clausura.
Puede usarse en una forma similar a la variable tópica $_ (excepto que no tiene que existir
cuando se hace la declaración):
Es también posible usar múltiple términos Whatever en la misma expresión. Por ejemplo,
la subrutina añadir podría escribirse como una expresión Whatever con dos parámetros:
my $añadir = * + *;
say $añadir(4, 5); # -> 9
o:
my &añadir = * + *;
say añadir(4, 5); # -> 9
my $mult = * * *;
say $mult(6, 7); # -> 42
o a esto:
Para ser honesto, el compilador no se confunde, pero el usuario podría, a menos que ella o
él haya sido previamente expuesta/o a algunos lenguajes de programación funcional que
comúnmente usan este tipo de construcción sintáctica.
Estas ideas son poderosas, pero se te aconseja que preste atención para que no caigas en la
trampa de la ofuscación de código.
Con eso dicho, el paradigma de programación funcional es extremadamente expresivo y
puede hacer tu código mucho más corto. Y, sobre todo, código más corto, provisto que
permanezca claro y fácil de entender, tendrá menos errores que código más largo.
14.7. Usando un Estilo de Programación Funcional 313
Este programa siempre funciona sobre el array completo (y su copia) y las sub-listas no son
extraídas; la extracción es simulada por el uso de los subíndices de rango.
Este código no es muy largo, sin embargo es un poco complicado. Si intentas ejecutarlo,
encontrarás que hay un error (bug): el último artículo del array original no es ordenado
apropiadamente. Por ejemplo, si intentas ejecutarlo sobre una lista de 10 números enteros
en orden inverso (e.g., ordenados del 10 al 1, @array) usado en la prueba al final del código
más arriba, obtendrás el siguiente array:
[2 3 4 5 6 7 8 9 10 1]
Como un ejercicio, intenta arreglar el error antes de continuar (el error será arreglado más
adelante).
Es probable que notarás que identificar y corregir el error es difícil, aunque el error es
relativamente simple (cuando escribí este código, encontré errores más complicados antes
de encontrar este). Es bastante difícil de usar los subíndices de array apropiadamente e
insertar los artículos en el lugar correcto, y así evitar ´´errores por uno´´ (off-by-one errors en
inglés) y otros errores.
Esta es la versión corregida:
[11 13 14 15 19 24 25 29 39 46 52 57 62 68 81 83 89 92 94 99]
El código es más corto que la implementación previa, pero ese no es el punto principal.
La subrutina ord-mezcla es algo similar a la implementación previa, excepto que crea las
sub-listas recursivamente y después mezcla dichas sub-listas.
La parte más simplificada de esta implementación ahora es la subrutina mezclar-listas
(la cual realiza la mayor parte del trabajo en ambas implementaciones): recibe dos sub-
listas y las mezcla. La mayor parte de este trabajo se realiza en la última línea de código;
las dos penúltimas líneas solo se encargan de devolver la lista mezclada cuando una de las
sub-listas de entrada no contiene elementos.
Esta versión funcional del programa captura la esencia del algoritmo de ordenamiento por
mezcla:
Espero que puedas ver lo limpia y concisa que es la implementación al estilo funcional.
Para darte una idea, escribir y depurar este último programa me tomó alrededor de 15 min-
utos, i.e., cerca de 10 minutos menos que la versión que no es funcional. Si no lo crees,
intenta implementar estas dos versiones por ti mismo. (Es un ejercicio excelente aún si me
crees.)
La sección de ejercicios de este capítulo (Sección 14.10) proveerá otro (y probablemente
más relevante) ejemplo sobre la simplicidad de la programación funcional comparada a
estrategias más imperativas o procedimentales.
use v6;
use Test; # un módulo estándar incluido con Rakudo
use lib 'lib';
# ...
plan $num-tests;
# .... pruebas
• Regex:
like($valor, $regex-esperado, $descripción?)
unlike($valor, $regex-esperado, $descripción?)
La función like marca una prueba como aprobada si el $valor coincide con el
$regex-esperado. Dado que estamos hablando de regexes, “coincide“, en la oración
anterior, realmente se refiere a “coincidencias inteligentes“. La función unlike marca
una prueba como aprobada si el $valor no coincide con $regex-esperado.
Por ejemplo:
like 'foo', /fo/, 'foo se parece a fo';
unlike 'foo', /bar/, 'foo no se parace a bar';
• Y un sin número de otras funciones que puedes estudiar en la siguiente docu-
mentación: https://docs.perl6.org/language/testing.html.
No obstante, es mejor (donde sea posible) usar una de las funciones especializadas de
comparaciones, porque pueden imprimir diagnósticos más útiles en casos en los cuales
la comparación falla.
Si una prueba falla (aunque parezca ser exitosa), y no entiendes la razón por la cual falla,
podrías querer usar la función diag para obtener información adicional. Por ejemplo,
asume que la prueba:
está fallando y que no posees información suficiente para entender el por qué; podrías
intentar esto:
# archivo es-palindroma.p6
use v6;
Prueba este ejemplo, juega con el mismo, cambia algunas líneas, agrega nuevas pruebas, y
observa lo que pasa.
Escribir tales unidades de pruebas puede ser una tarea tediosa. La verdad es que solo las
pruebas manuales son algo tediosas, y si solo intentas, encontrarás que la escritura y uso
de tales pruebas hace que la tarea de pruebas sea menos complicada. Usualmente escribes
las pruebas una sola vez, y las ejecutas un sin número de veces. ¡Y te sorprenderás cuantos
errores encontrarás aún si estás seguro que tu código está correcto! También, una vez que
escribes un conjunto de pruebas para algo, podrías usarlo años más tarde, por ejemplo para
pruebas no regresivas después de un cambio de software. Esto no es solo ahorra tiempo
sino que también garantiza que estás suministrando software de buena calidad.
Muchas organizaciones actualmente escriben sus pruebas antes de escribir los programas.
Este proceso se conoce como desarrollo guiado por pruebas y hay muchas áreas donde es muy
exitoso. De hecho, el compilador de Rakudo/Perl 6 tenía un inmenso conjunto de pruebas
(más de 40,000 pruebas) mucho antes que el compilador estuviera listo. En cierta manera,
el conjunto de pruebas hasta se convirtió en la especificación verdadera del proyecto, para
que así cualquier persona pudiera usar el mismo conjunto de pruebas para verificar otra
implementación de Perl 6.
320 Capítulo 14. Programación Funcional en Perl
Una ventaja adicional de tal estrategia es que medir la proporción de pruebas que pasan
puede ser una mejor medida de la realización de software que los estimados fortuitos,
tales como, por ejemplo, una proporción del número de líneas de código escrito versus el
estimado del número final de líneas de código.
14.9 Glosario
Objeto de primera clase Un objeto que puede pasarse como un argumento a una sub-
rutina o devolverse como un valor de retorno de la misma. En Perl, las subrutinas
son objetos de primera clase (también llamados ciudadanos de primera clase).
Función de retrollamada Una función o subrutina que se pasa como un argumento a otra
función.
Función de orden superior Una función o subrutina que toma otra subrutina (o un simple
bloque de código) como un argumento. Las funciones integradas map, grep, reduce,
y sort son ejemplos de funciones de orden superior.
Subrutina anónima Una subrutina que carece de nombre. También llamada comúnmente
una lambda. Aunque no son exactamente lo mismo, los bloques puntiagudos pueden
también ser asimilados en subrutinas anónimas.
Clausura Una función que puede acceder variables que están lexicalmente disponible
donde la función es definida, incluso aquellas variables que ya no se encuentran en
el ámbito donde la función es llamada.
Programación de tubería Un modelo de programación en el cual piezas de datos (usual-
mente listas) experimentan transformaciones sucesivas como en una tubería o línea
de ensamblaje.
Reducción Un proceso a través del cual una lista de valores es reducida a un valor único.
Por ejemplo, una lista de números puede reducirse para encontrar el promedio, un
valor máximo, o una mediana. Algunos lenguajes conocen este proceso como folding.
Metaoperador Un operador que actúa sobre otro operador para proveer nuevas funcional-
idades.
Complexidad algorítmica Una medida aproximada del número de operaciones (y tiempo)
necesarias para realizar una computación sobre un conjunto de datos relativamente
grande, y, más precisamente, una medida de cómo un algoritmo escala cuando el
conjunto de datos crece.
Evaluación peresoza Un proceso de evaluación aplazada donde, por ejemplo, se puebla
una lista o procesa los artículos de una lista solamente a petición, cuando es re-
querido, para evitar el procesamiento innecesario.
Iterador Una pieza de código que devuelve valores a petición y mantiene un récord de
donde ha llegado, para de este modo ser capaz de saber cuál valor debería ser
proveído en la siguiente iteración.
Cache El proceso de caché se refiere al almacenamiento de un valor en la memoria (en una
variable, un array, un hash, etc.) para de esta forma evitar la necesidad de calcularlo
nuevamente, y como resultado ahorrar algún tiempo de computación.
14.10. Ejercicio: Ordenamiento Rápido 321
Currificación Esta técnica hace referencia al hecho de tomar una función que acepta varios
argumentos y transformarla en una serie de funciones que toman un menor número
de argumentos.
Desarrollo guiado por pruebas Una metodología de desarrollo donde las pruebas de soft-
ware se escriben de las especificaciones antes que el programa actual. Esto se hace
para que sea más fácil chequear que el programa cumple con las especificaciones.
ordrapido @array;
say @array;
El array es modificado en lugar (lo cual tiene la ventaja de requerir memoria limitada), lo cual
significa que el array original es modificado.
Para la programación funcional, los datos internos son inmutables, y por lo tanto copias fragmentos
de datos en nuevas listas, en lugar de modificarlos “directamente“.
Con el mismo espíritu que lo hicimos en la sección14.7 para el algoritmo de ordenamiento de mezcla,
trata de escribir una implementación del algoritmo de ordenamiento rápido. Pista: esto se puede
hacer en la mitad de una docena de líneas de código.
Solución: A.11.1.
15
Algunos Consejos Finales
Trata de hacer tus programas tan claros como sea posible, y tan simples como puedas.
Usa conceptos más avanzados solo si es realmente necesario, pero no lo hagas solo con el
propósito de presumir. No trates de ser astuto o, por lo menos, no tan astuto.
Recuerda que el código no solo es usado por el compilador, sino que también por humanos.
Piensa en ellos.
Contempla sobre la persona que mantendrá tu código. Como algunas personas le gusta
decir: “Siempre escribe código como si la persona que termina manteniendo tu código es
un psicópata violento que sabe donde vives.“ Y, si esto no te convence, recuerda que la
persona que mantenga tu código dentro de un año podría set tú. Para ese entonces, puede
ser que no recuerdes cómo ese truco astuto que usaste realmente funciona.
Una cita final de Edsger Dijkstra sobre el tema: “La simplicidad es un prerequisito para la
fiabilidad.“
Remueve código muerto y código de andamiaje Al escribir código nuevo, puedes crear
variables que no usas en la versión final de tu código. Si esto sucede, remuévelas; no
las dejes distraer la atención de tu lector. Si modificas un programa existente, limpia
15.2. Qué Hacer y Qué No Hacer 325
Prueba agresivamente Nadie puede escribir una pieza de código significante sin tener un
número inicial de errores. Se cita a Edsger Dijkstra diciendo: “Si la depuración es el
proceso de remover errores de software, entonces la programación debe ser el pro-
ceso de crearlos.“ Lamentablemente es cierto. Aunque Dijkstra también dijo que “las
pruebas de software muestran la presencia, no la ausencia de errores,“ las pruebas
de software son una parte esencial del desarrollo de software. Escribe conjuntos de
pruebas extensos, úsalos a menudo, y actualízalos a medida que la funcionalidad
evoluciona. Ve la Sección 14.8 (p. 316) para algunas herramientas de pruebas autom-
atizadas.
Evita la optimización prematura En las palabras de Donald Knuth: “La optimización pre-
matura es la fuente de todo lo malvado (o por lo menos la mayoría) en la progra-
mación.“
my $tiempo-restante = 31536000;
¿Qué es este número 31,536,000 que aparece de la nada? No hay manera de saber con
solo mirar esta línea de código. Compáralo con esto:
my $segundosEnUnAño = 365 * 24 * 60 * 60;
# ...
my $tiempo-restante = $segundosEnUnAño;
¿No es la segunda versión más clara?1 Bueno, para ser sincero, sería mejor usar una
constante en tal caso:
Evita valores literales Los valores literales son malos. Si tienes que usar algunos, defíne-
los como variables o constantes al comienzo de tu programa, y usa esas variables o
constantes en vez. Los rutas literales de archivos son especialmente malas. Si tienes
que usar algunos, usa algunas variables con rutas relativas:
my $dir-base = '/path/a/datos/aplicacion';
my $dir-entrada = "$dir-base/ENTRADA";
my $dir-result = "$dir-base/RESULT";
my $dir-temp = "$dir-base/TEMP";
my $dir-log = "$dir-base/LOG";
Por lo menos, si la ruta debe cambiar, solo tienes que cambiar la línea de código
superior.
No ignore los errores devueltos por las subrutinas o funciones integradas No todos los
valores de retorno son útiles; por ejemplo, usualmente no chequeamos el valor de
retorno de una sentencia de impresión, pero eso es usualmente bueno porque no es-
tamos interesados en el efecto secundario, el hecho de imprimir algo a la pantalla o
a un archivo, en lugar de un valor de retorno. En la mayoría de los casos, necesitas
1 Esta calculación se refiere al año común (i.e. un calendario que no es bisiesto o año civil). No es la misma
definición de otros anos astronómicos, los cuales más de un cuarto de día más largo.
326 Capítulo 15. Algunos Consejos Finales
saber si algo malo pasó para tomar los pasos necesarios para recuperarse de la condi-
ción de error si es posible o abortar el programa con gracia (e.g., con un mensaje de
error informativo) si el error es muy serio para que el programa continúe.
Formatea tu código clara y consistentemente El compilador puede no importarle la in-
dentación de código, pero eso es importante para los lectores humanos. El formato de
tu código debería ayudar a clarificar la estructura y flujo de control de tus programas.
Sé bueno y diviértete.
Crear un hash de una lista de llaves y una lista de valores: Usando rebanadas (slices)
my %hash; %hash{@llaves} = @valores;
Usando el operador zip y un metaoperador con el constructor pareja:
my %hash = @llaves Z=> @valores;
Para las pruebas existentes, los valores de los hashes solo necesitan ser verdaderos.
Esta es una buena manera de crear un hash de una lista de llaves:
my %existentes = @llaves X=> True;
O, mejor aún, usa un set:
my $existentes = @llaves.Set;
say "existe" if $existentes{$objeto};
Hacer los atributos mandatorios (o parámetros de la subrutina): Esto es una forma
buena de hacer un atributo mandatorio en una clase:
has $.atr = die "El atributo 'atr' es mandatorio";
Este código usa el mecanismo de valor por defecto: si un valor es provisto, entonces
el código para el valor por defecto no se ejecuta. Si no valor es provisto, entonces el
código muere con el mensaje de error apropiado. El mismo mecanismo puede usarse
para los parámetros de las subrutinas.
O puedes usar el rasgo is required:
2 Cuando se sugiere dos soluciones, la segunda es usualmente la más idiomática.
15.3. Usa Expresiones Idiomáticas 327
Returnar los elementos únicos de una lista El método unique remueve los duplicados de
la lista de entrada:
return @array.unique;
O, si sabes que la lista está ordenada, puedes usar la función squish (la cual remueve
los duplicados adyacentes).
Añadir los elementos de una lista Usa la función reduce o el metaoperador de reducción:
Intercambiar dos variables Usa la invocación mutante del método .= con la función
reverse:
$z = 2 + Int(5.rand);
# o, mejor:
$z = (2..6).pick;
Recorrer un rango de valores, ignorando los límites del rango: Usa el operador de rango
exclusivo:
for ($comienzo+1) .. ($final-1) -> $i {...}
# o, mejor:
for $comienzo ^..^ $final -> $i {...}
Lee libros sobre algoritmos. Muchos libros excelentes tratan el tema, pero especialmente
recomiendo los siguientes dos (aunque deberías saber que no son fáciles):
15.4. ¿Qué sigue? 329
• Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein, Intro-
duction to Algorithms, The MIT Press
• Donald Knuth, The Art of Computer Programming, Addison Wesley (many volumes,
many editions).
Lee otros libros sobre programación, aún sus objetivos sean otros lenguajes de progra-
mación o no lenguaje en específico. Es probable que tengan una perspectiva diferente sobre
varios temas; esto te ofrecerá una perspectiva diferente y quizás una mejor entendimiento,
y complementará lo que leíste aquí. Lee tutoriales, artículos, blogs, y foros sobre pro-
gramación. Participa cuando puedas. Lee la Introduction to Perl 6 la cual existe en ocho
lenguajes diferentes al momento de escribir este libro (http://perl6intro.com/). Lee la
documentación oficial de Perl 6 (https://docs.perl6.org).
Este libro tiene más de mil ejemplos de código, lo cual es mucho, pero podría ser no su-
ficiente si quieres aprender mucho más. Deberías leer fragmentos de código escritas por
otros. Mira las librerías o módulos de código abierto e intenta entender lo que hace y cómo
lo hacen. Intenta usarlas.
Habiendo dicho eso, debería enfatizar que puedes leer tantos libros como quieras sobre la
teoría de la natación, pero nunca aprenderás a nadar hasta que lo intentes. Lo mismo es
cierto sobre aprender a programar y aprender un lenguaje de programación. Escribe nuevo
código. Modifica los ejemplos existentes y observa lo que pasa. Intenta cosas nuevas.
Continúa, sé valiente, sumérgete en la piscina y nada. El resultado es: aprenderás al hacer.
¡Aprender el arte de la programación es divertido. Disfrútalo!
330 Capítulo 15. Algunos Consejos Finales
A
Soluciones a los Ejercicios
Este (largo) capítulo provee soluciones a los ejercicios sugeridos en los capítulos principales
del libro. No obstante, también contiene muchísimo más.
Primero, en muchos casos, provee varias soluciones diferentes, ilustrando así estrategias
distintas para solucionar un problema, discutiendo sus méritos respectivos o desventajas y
usualmente mostrando soluciones que pueden ser más eficientes que otras.
Segundo, usualmente provee muchísima información adicional o ejemplos complementar-
ios.
Solo el alto volumen de ejemplos de código de este capítulo podría enseñarte muchísimo
sobre la programación en general y acerca del lenguaje Perl 6 en particular.
Finalmente, este capítulo algunas veces introduce (con ejemplos) conceptos nuevos serán
discutidos en capítulos posteriores de este libro. Haber visto tales ejemplos puede ayudarte
a tener una mayor comprensión de estas nuevas ideas al leer estos capítulos posterior-
mente. En algunos casos, este capítulo discute o introduce nociones que no será discutidas
en otra parte del libro.
Cuando resuelvas un ejercicio, aún si crees que lo hiciste maravillosamente, por favor
asegúrate de consultar las soluciones en este capítulo e intentarlas: es altamente proba-
ble que aprenderás algo de ellas.
use v6;
sub alinear-a-derecha( $cadena-entrada ) {
my $long_cadena = chars $cadena-entrada;
my $long_restante = 70 - $long_cadena;
my $espacios_delanteros = " " x $long_restante;
say $espacios_delanteros, $cadena-entrada;
}
alinear-a-derecha("Larry Wall");
alinear-a-derecha("The quick brown fox jumps over the lazy dog");
Esta subrutina:
Larry Wall
The quick brown fox jumps over the lazy dog
Sin embargo, podemos hacer el código más conciso al componer algunas de las sentencias
y expresiones:
Podríamos hasta reducirla a una subrutina más corta con una sola línea:
Esto funciona bien, pero podría argumentarse que esta última versión es menos clara. De
hecho, la variable temporaria $espacios_delanteros usada en la versión previa tenía un
nombre que documentaba lo que la subrutina estaba haciendo. Puedes hacer el código
muy conciso en el último ejemplo, pero algunas veces se puede volver un poco terso, así
que hay una compensación entre lo conciso y la claridad.
Nota que hay dos funciones integradas, printf y sprintf, que pueden realizar tareas sim-
ilares. También está el método .fmt para producir salida formateada.
A.1. Ejercicios del Capítulo 3: Funciones y Subrutinas 333
Esto muestra:
¡Hello World!
¡Hello World!
use v6;
Obviamente hay maneras mejores de hacer algo cuatro veces que solo repetir say $valor;
cuatro veces con en la subrutina decir-cuatro-veces más arriba, pero esto será discutido
en el Capítulo4 (ver Sección 4.10).
Para dibujar una cuadrícula similar con cuatro filas y cuatro columnas, primero necesita-
mos modificar las cadenas usadas para imprimir las líneas:
A.1. Ejercicios del Capítulo 3: Funciones y Subrutinas 335
Además de eso, podríamos modificar a imprimir-cuadr para que solo imprima cada línea
el número requerido de veces. Pero eso involucra un poco de repetición de código, y el
propósito de este ejercicio es usar las subrutinas para permitir la reutilización de código.
Hay dos cosas que necesitamos hacer cuatro veces repetidamente. Hace sentido
escribir una subrutina hacer-cuatro-veces que se usará para crear la subrutina
decir-cuatro-veces y para llamar filas completas cuatro veces. A esta subrutina se le
pasará la referencia de código para las acciones específicas requeridas:
Además, en lugar de declarar las variables globales para las cadenas de texto que repre-
sentan las líneas, es mejor práctica declararlas y definirlas dentro de las subrutinas donde
son usadas. Ya no necesitamos la subrutina decir-cuatro-veces; podemos solo pasar los
argumentos relevantes a la subrutina hacer-cuatro-veces para obtener el mismo efecto.
Esto resulta en el programa siguiente:
hacer-n-veces $subref, 4;
This prints:
Carpe diem
Carpe diem
Carpe diem
Carpe diem
días-HMS(240_000);
Las primeras dos líneas realizan la división entera y la operación de módulo separada-
mente. Para los dos casos siguientes, hacemos las dos operaciones en una sola línea, us-
ando una sintaxis de lista.
Las variables $minutos, $horas, y $días se computan todas en una manera similar. El
código podría hacerse más modular al usar una subrutina para calcular los $minutos, las
$horas, y las $días. Aunque las subrutinas fructuosas serán realmente usadas en el capí-
tulo siguiente, hemos ya visto varios ejemplos de ellas y pueden proveer un resumen acerca
de cómo podrían ser utilizadas:
En la vida real, sería bueno verificar que la entrada proveída por el usuario es un entero
positivo y preguntar nuevamente si no lo es. Como un ejercicio adicional, podrías insertar
el código anterior en una subrutina recursiva que imprime un mensaje de error y se llama
a sí misma otra vez si la entrada del usuario no es válida. LA solución al ejercicio sigu-
iente (Sección A.2.3) provee un ejemplo de una subrutina recursiva diseñada para incitar
al usuario a suministrar la entrada nuevamente; esto podría ayudarte a cómo hacerlo si
encuentras alguna dificultada.
con esta:
an + bn = cn
sub obtener-entrada {
say "Tu misión, Jim, si la aceptas, es ";
say "proveer los valores A, B, C y n que satisfacen la ecuación de Fermat:";
say " A ** n + B ** n = C * *n";
my $a = prompt "Por favor proveer un valor para A: ";
my $b = prompt "Por favor proveer un valor para B: ";
my $c = prompt "Por favor proveer un valor para C: ";
my $n = prompt "Por favor proveer un valor para el exponente: ";
if chequear-fermat($a.Int, $b.Int, $c.Int, $n.Int) {
say "La ecuación se satisface para tus valores";
} else {
say "Nope. La ecuación no está correcta."
}
my $intentar-nuevamente = prompt "¿Quieres intentar nuevamente (S/N)?";
obtener-entrada if $intentar-nuevamente eq 'Y';
}
Otra manera de hacer esto sería comenzar a encontrar la longitud más larga y probar con
esa solamente, pero eso no hace el algoritmo significativamente más simple.
Incitar el usuario a entrar tres longitudes se ha mostrado en los ejercicios previos; nada
nuevo aquí. No obstante, esta es una forma nueva de hacerlo:
my ($fib1, $fib2) = 1, 1;
for 3..$n {
my $nuevo_fib = $fib1 + $fib2;
($fib1, $fib2) = $fib2, $nuevo_fib;
}
return $fib2;
}
Esto imprimiría:
Args : n = 3, s = 0
Args : n = 2, s = 3
Args : n = 1, s = 5
Args : n = 0, s = 6
6
Para evitar que los argumentos conduzcan a una recursión infinita, podemos agregar re-
stricciones de tipo entero a la signatura de la subrutina y algún código para cesar la recur-
sión si el primer argumento es negativo, por ejemplo:
recurse $n - 1, $n + $s;
}
}
Ahora, si llamamos a recurse con un valor negativo para $n, obtenemos un error:
Args : n = -1, s = 0
¡PARA! $n es negativo, nos damos por vencidos
in sub recurse at recurse2.pl6 line 6
in block <unit> at recurse2.pl6 line 12
Otra posibilidad sería usar una característica de Perl 6 que no hemos discutido todavía,
subrutinas multi, las cuales son descritas en la Sección ?? (p. ??). La idea es declarar dos
versiones de la subrutina recurse, las cuales tienen el mismo nombre pero signaturas difer-
entes. El compilador averiguará cual versión de recurse debe llamar dependiendo en cual
signatura aplica a los argumentos pasados a la subrutina:
multi recurse( $n , $s ) {
say "Args : n = $n, s = $s";
# haz algo diferente para tal caso, por ejemplo:
# recurse (abs $n.Int), $s; # llamando la primera versión de recurse
# or simply:
say '¡PARA! $n es negativo, nos damos por vencidos';
}
$ perl6 recurse.pl6
Args : n = 6.1, s = 0
¡PARA! $n es inválido, nos damos por vencidos
$ perl6 recurse.pl6
Args : n = -1, s = 0
¡PARA! $n es negativo, nos damos por vencidos
342 Apéndice A. Soluciones a los Ejercicios
multi recurse( $n , $s ) {
say "Args : n = $n, s = $s";
recurse (abs $n.Int), $s;
}
say comparar 4, 7; # -1
say comparar 7, 4; # 1
say comparar 5, 5; # 0
Nota: esto ejemplifica una función comparar de tres sentidos comúnmente usado para
ordenar un número en los lenguajes de programación, incluyendo versiones más antiguas
de Perl (tal como Perl 5). En Perl 6, los operadores que implementan esta funcionalidad
(los comparadores de tres sentidos cmp, leg y <=>) devuelven tipos especiales de valores:
Order::More, Order::Less, y Order::Same. (ver Sección 9.11 acerca de ordenamiento en
el capítulo sobre arrays y listas para más detalles.)
Esto imprime:
hypotenuse = 5
0
Nota que las pruebas proveídas aquí son solo un número limitado de ejemplos, con
propósitos de ilustración. Un conjunto de pruebas más rigurosa podría ser necesario (pro-
bar por ejemplo con números negativos y positivos). Más tarde veremos mejores maneras
de construir conjuntos de pruebas más robustos (ver por ejemplo la Sección 14.8 y la solu-
ción al ejercicio en la Sección A.9.2).
344 Apéndice A. Soluciones a los Ejercicios
n + 1
if m = 0
A(m, n) = A(m − 1, 1) if m > 0 and n = 0
A(m − 1, A(m, n − 1)) if m > 0 and n > 0.
Hemos usados paréntesis para mostrar mejor la estructura, pero funciona igualmente sin
ellos. Aún en la última línea de código con dos llamadas a la subrutina, la signatura de la
subrutina con dos números enteros es suficiente para que el compilador de Perl 6 entienda
cual argumento está asociado con una de la llamada:
La función Ackermann está definida para números enteros que no son negativos. Como
un ejercicio adicional, modifica la subrutina ack para prevenir que argumentos negativos
sean suministrados. Discutimos dos maneras diferentes de hacer esto en la Sección 5.9.
Result:
True
True
True
False
True
Al agregar una sentencia say para visualizar las llamadas recursivas, obtenemos:
1024 2
512 2
256 2
128 2
64 2
32 2
16 2
8 2
4 2
True
Nota que hay un método más simple para encontrar el MCD de dos números sin usar la
función módulo. Se conoce como el algoritmo de Euclides y es considerado el algoritmo
más antiguo (ver https://es.wikipedia.org/wiki/Algoritmo_de_Euclides). El algo-
ritmo de Euclides se basa en la observación de que el MCD de dos números no cambia si
el número mayor es reemplazado con la diferencia de tal número y el número menor.
Esto podría implementarse en Perl con la siguiente subrutina recursiva:
Este código funciona perfectamente bien en casi todos los casos, por lo menos para todos
los valores de entrada estrictamente positivos, pero intenta seguir el flujo de ejecución si
uno de los argumentos pasados a la subrutina, digamos $b, es cero. En este caso, mcd entra
en una recursión infinita. Esto usualmente se conoce como un edge case o un corner case,
i.e., un valor de entrada especial para el cual un programa aparentemente funcional cesa
de funcionar apropiadamente.
Tenemos un problema similar para los valores de entrada negativos.
Una solución podría ser agregar una restricción de signatura (o usar un subconjunto de
tipo):
pero esto no es realmente satisfactorio porque el MCD de cualquier entero que no es cero
y 0 está matemáticamente bien definido y es igual al primer número.
Dejando al lado por un momento el caso de los números negativos, podrías escribir la
subrutina nuevamente en la siguiente manera:
Concerniente a los números negativos, hay un teorema que dice que el MCD de a y b es el
mismo que el MCD de a y −b:
mcd(a,b) = mcd(−a,b) = mcd(a,−b) = mcd(−a,−b)
Podemos modificar la subrutina mcd aún más:
$b = -$b if $b < 0;
return $a if $b == 0;
return $b if $a == 0;
return mcd($b, $a - $b) if $a > $b;
return mcd($a, $b - $a) if $b > $a;
return $a;
}
Esto funciona bien ahora, pero recuerda que una subrutina recursiva puede ser llamada
muchas veces y, por cada llamada, las primeras cuatro líneas de código en el programa más
arriba son ejecutadas, aunque son realmente útiles solo en la primera llamada: una vez que
estas condiciones han sido chequeadas durante la primera llamada de la subrutina, sabe-
mos que los argumentos deben ser válidos y permanecer válidos en la cadena de llamadas
recursivas, así que estos chequeos después de la primera llamada no son necesarios. Esto
es un despilfarro y puede conducir a problemas innecesarios de rendimiento.
Idealmente, podría ser mejor separar estas cuatro líneas que chequea las precondiciones de
las cascada de llamadas recursivas. Por ejemplo, podríamos escribir dos subrutinas:
Ahora, mcd está haciendo todos los chequeos necesarios de los argumentos iniciales y llama
subrutina recursica mcd1 con los argumentos que han sido higienizados y que no con-
ducirán a una recursión infinita. Nota que hemos nombrados los parámetros dentro de
mcd1 diferentemente para mejor claridad, pero esto no era necesario; funcionaría igual si
hubiésemos mantenido a $a y $b.
El código anterior trabaja perfectamente bien.
Aunque podría haber un último problema. Alguien descuidado (o queriendo ser muy
astuto) podría tratar de llamar a mcd1 directamente, y así destruyendo los beneficios de
los chequeos realizados por mcd. Para prevenir eso, podemos hacer buen uso del hecho de
que las subrutinas tiene ámbito lexical en Perl 6 y pueden ser locales a otras subrutinas:
podemos declarar y definir mcd1 dentro del cuerpo de la subrutina mcd, para que mcd1
pueda ser llamada solo desde el interior de la subrutina mcd:
return $c;
}
$a = -$a if $a < 0;
$b = -$b if $b < 0;
return $a if $b == 0;
return $b if $a == 0;
return mcd1 $a, $b;
}
Nota que podría tener más sentido declarar este estimado inicial dentro de la subrutina
mi-raíz-cuad, en lugar de tener que pasarlo como un argumento. La justificación para
hacer esto es que, en algunos casos, la subrutina que hace la llamada tiene información
sobre el rango del valor de entrada y podría por lo tanto ser capaz de proveer un mejor
estimado inicial, lo cual conduce el algoritmo a converger hacia la solución mucho más
rápido.
Esta es una implementación del método de Newton para computar la raíz cuadrada de un
número:
$estimate = $y;
}
return $estimado;
}
sub probar-raíz-cuadrada {
say "a mi-raíz-cuad(a)\t sqrt(a)\t difer";
for 1..9 -> $a {
my $estimado-ini = $a/2;
my $result = mi-raíz-cuad $a, $estimado-ini;
my $sqrt = sqrt $a;
my $difer = abs($result - $sqrt);
imprimir-result($a, $result, $sqrt, $diff);
}
}
probar-raíz-cuadrada;
sub pi-estimado {
#`{ ======================================
Algoritmo por Srinivasa Ramanujan
(ver http://en.wikipedia.org/wiki/Pi)
====================================== }
my $factor = 2 * 2.sqrt / 9801;
my $k = 0;
my $suma = 0;
while True {
my $num = factorial(4*$k) * (1103 + 26390*$k);
A.5. Ejercicios del Capítulo 7: Cadenas de Texto 351
say pi-estimado;
# say pi - pi-estimado;
my $fruta = "banana";
my $indice = $fruta.chars;
while $indice > 0 {
$indice--;
my $letra = substr $fruta, $indice, 1;
say $letra;
}
my $fruta = "banana";
my $indice = $fruta.chars;
while $indice > 0 {
say substr $fruta, --$indice, 1;
}
352 Apéndice A. Soluciones a los Ejercicios
Aquí, imprimimos el valor devuelto por substr directamente, sin usar una variable tem-
poraria, y disminuimos la variable $indice dentro de la expresión usando una substr.
Necesitamos usar la forma prefija del operador de decremento porque necesitamos que
$indice sea reducido antes de ser utilizado por substr.
El bucle sería aún más conciso si usáramos un while con un modificador de sentencia (o la
sintaxis sufija de while):
my $fruta = "banana";
my $indice = $fruta.chars;
say substr $fruta, --$indice, 1 while $indice;
Esta es la misma idea, usando la función flip para invertir la cadena de texto:
El objetivo de este ejercicio es entrenarte a usar bucles para recorrer la cadena de texto.
Combinar las funciones o métodos flip y comb haría nuestra solución mucho más simple
(y probablemente más rápida):
Pero esto no funciona apropiadamente porque crea una lista de cuatro elementos en la cual
el primer elemento es una sub-lista desde “J” hasta “N”:
Regresaremos a esto más tarde en el libro, pero digamos que necesitamos aplanar esta
combinación de listas en una sola lista iterable, lo cual puede hacerse con el método o la
función flat o el operador “|“:
my $sufijo = 'ack';
for ('J' .. 'N', 'Ou', 'P', 'Qu').flat -> $letra {
say $letra ~ $sufijo;
}
A.5. Ejercicios del Capítulo 7: Cadenas de Texto 353
Aquí nuevamente, podríamos hacer el código un poco más conciso con la sintaxis sufija de
for y la variable tópica $_:
my $sufijo = 'ack';
say "$_$sufijo" for flat 'J' .. 'N', 'Ou', 'P', 'Qu';
Aquí introducimos otra manera simple y común de concatenar dos cadenas de texto: sim-
plemente inserta las dos variables una después de la otra dentro de comillas dobles y per-
mite que la interpolación de variables haga su trabajo.
La solución al Ejercicio 7.1 (p. 358) más abajo usa las funciones index y substr para realizar
la misma cuenta.
Como puedes ver, esto es algo un poco complicado porque los casos especiales que necesi-
tamos manejar. Compara esto con un regex de una sola línea que hace lo mismo:
}
return; # no encontrada!
}
s b: u
l w: o
l o: l
s m: Nil
y l: e
a z: Nil
Esto es mucho más simple que el atento anterior, pero sería aún un poco difícil de cambiar
algo, por ejemplo agregar una nueva condición: la estructura del código necesitaría ser
modificado un poco.
Aún comparada con esta solución más simple, la solución de regex realmente brillas en
orden de magnitud.
A.5.5.7 Primera palabra que comienza con una vocal (minúscula o mayúscula)
Podemos simplemente ignorar la distinción entre mayúscula y minúscula para la palabra
completa:
my $cadena = " Ask not what your country can do for you — " ~
" ask what you can do for your country. (JFK)";
say ~$0 if $cadena ~~ /:i << (<[aeiouy]> \w*)/; # -> Ask
while $j > 0 {
El índice $j debería ser capaz de repetir hasta llegar a 0 (inclusivo) si queremos comparar
la primera letra de $palabra2 con la última letra de $palabra1.
La versión corregida de la subrutina es-inversa podría ser:
my $i = 0;
my $j = $palabra2.chars - 1;
while $j >= 0 {
return False if substr($palabra1, $i, 1) ne substr($palabra2, $j, 1);
$i++; $j--;
}
return True;
}
sub contar_a {
my $palabra = "banana";
my $cuenta = 0;
my $idx = 0;
while True {
$idx = index $palabra, 'a', $idx;
last unless $idx.defined;
$idx++;
$cuenta++;
}
return $cuenta;
}
say contar_a(); # -> 3
Si la adaptamos para cualquier cadena de texto y cualquier letra es solo una cuestión de
pasar los argumentos correctos a la subrutina y usar dentro de la subrutina sus parámetros
en lugar de valores literales:
A.5. Ejercicios del Capítulo 7: Cadenas de Texto 359
Contar una letra dada en una palabra dada con la función substr es algo directo: solo
necesitamos pasar a través de cada letra de la palabra e incrementar un contador cuando
sea necesario:
Las otras subrutinas tienen los errores siguientes (algunas tienen varios errores; vamos a
mencionar por lo menos uno de ellos):
solucionaría el problema.
El siguiente es un ejemplo del bucle que podrías escribir para probar cada subrutina, cada
una con tres cadenas de texto de entrada:
Sería posible reemplazar las nueve sentencias de impresión con un bucle simple, pero re-
quiere usar características que no hemos estudiado todavía.
En otros capítulos verás maneras de organizar pruebas de casos, por ejemplo en la Sec-
ción 14.8 (p. 316).
Sin embargo, este es un caso típico donde los regexes pueden resultar más eficiente que un
bucle (en términos de eficiencia de código, i.e., como un aproximación, cuántas líneas de
código son necesarias para realizar una tarea dada).
Discutimos en Capítulo 7 que las capturas de regex poblan las variables especiales $0, $1,
$2, etc. Un patrón de regex que coincide con cualquier letra repetida podría por lo tanto
ser /(.) $0/, donde el carácter encontrado por $0 es el mismo carácter encontrado por el
punto.
Similarmente, un patrón de regex que coincide con tres pares de letras dobles en una fila
podría implementarse así:
Con esto, el programa para encontrar las palabras con tres pares de letras dobles en el
archivo words.txt puede escribirse en tres líneas de código:
Nota del traductor: Aunque el archivo lemario.txt tiene alrededor de 88, 000 palabras, fui
incapaz de encontrar una palabra (en español) que contenga tres pares de letras con-
secutivas. No obstante, hay palabras que contienen dos pares de letras consecutivas
como toffee, hemorro, etc.
A.6. Ejercicios del Capítulo 8: Juego de Palabras 363
Si usamos el archivo words.txt, ambos programas encuentran cuatro palabras, las cuales
son variaciones de “bookkeeper” y “bookkeeping”.
La versión de regex es tan simple que puedes escribirla directamente en la línea de co-
mando del sistema operativo como un one-liner: (see Section 2.5):
Otra forma de hacerlo sería con el uso de regexes para encontrar si tenemos palíndromos:
https://raw.githubusercontent.com/LaurentRosenfeld/thinkperl6/master/Supplementary/words.txt
364 Apéndice A. Soluciones a los Ejercicios
$num + 3 ~~ /^(.)(.)(.)$2$1$0/;
}
Este código es más corto, pero también más lento: toma casi el doble para ejecutarse en mi
computadora. Así que hay un compromiso: La primera manera, la cual es más rápida, es
probablemente mejor si necesitas ejecutar tu programa muchas veces o usualmente, pero
puedes preferir la segunda versión si esto es una computación que haces raramente. La
decisión es tuya.
sub chequear_dif() {
# enumera todas las posibles diferencias de edade
for 15..75 -> $dif {
A.7. Ejercicios del Capítulo 9: Arrays y Listas 365
Pero eso no funciona porque esencialmente eso le dice a Perl que el parámetro @cola es un
array de arrays. Lo que necesitamos es la siguiente sintaxis de signatura:
Hay varias maneras de insertar varios elementos a la cola, pero la más simple es probable-
mente usar una signatura con el parámetro slurpy (o parámetro variádico): un parámetro
array o hash se marca como variádico con un asterisco al frente del mismo, lo cual significa
que puede atarse a un número arbitrario de argumentos (cero o más). Estos parámetros se
llaman "slurpy" porque sorbe los parámetros restantes a una función, al igual que un per-
sona sorbiendo un helado. Esto también significa que un parámetro variádico de posición
puede ser solamente el último en la signatura:
Esto mostrará:
[1 2 3 4 5 6 7 8]
8
7
6
A.7. Ejercicios del Capítulo 9: Arrays y Listas 367
Ver también la sección 11.3.2 para más detalles sobre los parámetros variádicos.
Nota que, para una subrutina encolar, no podemos simplemente escribir:
porque, cuando se provee un array como un segundo argumento, unshift inserta los
nuevos artículos como una sub-lista. Esta pequeña dificultad se puede solucionar con el
uso de un bucle for o con el uso del operador “|“ el cual aplana la lista.
Otra posibilidad es usar la función integrada prepend en lugar de unshift, dado que añade
los elementos aplanados de una array al principio de la cola:
my @linea = 1, 2, 3, 4;
encolar @linea, 6, 7, 8;
say @linea;
say desencolar @linea for 1..3;
Esto muestra:
[1 2 3 4 6 7 8]
1
2
3
368 Apéndice A. Soluciones a los Ejercicios
A.7.1.5 Excepciones
Finalmente, una debilidad adicional necesita arreglarse: ¿Qué pasa si la cola está vacía
cuando intentamos desencolar un artículo? Levantar una excepción o abortar el programa
podría ser lo que necesitamos. Podríamos también decidir devolver un valor indefinido y
dejar que la subrutina que hace la llamada haga lo necesario con el valor devuelto:
my @linea;
encolar @line, 1, 2, 3;
say @linea;
for 1..4 -> $cuenta {
my $artic = desencolar @linea;
if defined $artic {
say $artic;
} else {
say "ERROR: La cola está vacía!";
}
}
[1 2 3]
1
2
3
ERROR: La cola está vacía!
La subrutina desencolar podría simplificarse con el uso del operador ternario (ver Sec-
ción 11.1) y devolver el valor Nil si la lista está vacía:
Como un ejercicio adicional, podrías aplicar los cambios que hemos hecho más arriba para
el manejo de colas al ejemplo de código para las pilas (visto en la Sección 9.4 en la p. 155).
Otro problema con nuestra implementación de las colas es que la cola @file es totalmente
accesible al desarrollador, quien podría ser tentado a escudriñar el array directamente o
A.7. Ejercicios del Capítulo 9: Arrays y Listas 369
aún peor, modificarlo, sin usar las subrutinas encolar y desencolar diseñadas para man-
tener la cola consistente.
Podríamos querer prevenir eso y hacer imposible que el usuario altere la cola o por lo
contrario la acceda por otros medios que no son las subrutinas adecuadas. Ocultar la in-
formación sobre la implementación o al contrario hacerla inaccesible por otros medios más
que aquellos diseñados para ese propósito es usualmente conocido como la encapsulación
de datos. Una manera común de lograr la encapsulación de datos es a través de la progra-
mación orientada a objetos, la cual discutimos en el Capítulo 12.
Podemos, sin embargo, obtener un resultado similar al combinar el ámbito de variables y
material que se discute en la Sección 3.14 acerca de las subrutinas como objetos de primera
clase.
Considera la siguiente implementación de una cola:
sub crear-fifo {
my @cola;
return (
sub {return shift @cola;},
sub ($artic) {push @cola, $artic;}
) ;
}
my ($fifo-obtener, $fifo-poner) = crear-fifo();
$fifo-poner($_) for 1..10;
print " ", $fifo-obtener() for 1..5; # -> 1 2 3 4 5
La pieza central aquí es la subrutina crear-fifo. El array @cola que almacena los datos
tiene ámbito lexical en este subrutina y no puede accederse directamente desde otra parte
en el programa. crear-fifo devuelve dos subrutinas anónimas, una para desencolar
artículos y otra para encolarlos. Estas subrutinas son clausuras lexicales, lo cual significa
que pueden acceder @cola, porque han sido definidas dentro de su ámbito, aún si son lla-
madas desde otro lugar. Aún cuando crear-fifo ha finalizado, esas subrutinas pueden
todavía accederlo porque ellas le ceden vida extra al array mientras las subrutinas son ac-
cesibles.
El resto del código debería ser claro: cuando crear-fifo es llamada, crean las dos subruti-
nas anónimas que son almacenadas en las variable $fifo-obtener y $fifo-poner. Una
subrutina tal como crear-fifo es algunas veces llamada una función factoría porque genera
otras subrutinas al tiempo de ejecución. Finalmente, $fifo-poner es llamada diez veces
para poblar la cola con los enteros del 1 al 10, y $fifo-obtener es llamada cinco veces para
obtener los primeros cinco artículos de la la cola. La cola está encapsulada: aparte de usar
las dos subrutinas anónimas, no hay manera de acceder a sus datos
Encolar una lista de artículos (en lugar de uno solo) y manejar las excepciones (tal como
intentar obtener un artículo de una cola vacía) se deja como un ejercicio al lector el .
Las técnicas usadas aquí toman prestado de un paradigma de programación llamado pro-
gramación funcional, un modelo de programación usado por lenguajes tales como Lisp,
Caml, Clojure, y Haskell. Este paradigma es bien diferente a todo lo que hemos visto
hasta ahora, tal como la programación orientada a objetos es otro paradigma de progra-
mación diferente. A medida que ganas experiencia como un programador, deberías hacer
370 Apéndice A. Soluciones a los Ejercicios
Aquí, la expresión @array.end devuelve el índice del último elemento del array. También
es posible contar el número de artículos en el array usando dicho índice y acceder al último
y los penúltimos artículos de una lista o un array usando la siguiente sintaxis:
No tienes que especificar el número de elementos con splice si quieres el resto. Podemos
evitar el uso del array inmediato @result. Así que podríamos simplificar my-pop:
La única dificultada en este ejercicio es manejar una signatura con una lista “variádica“ de
parámetros (o parámetros slurpy). Esto se explicó en la Subsección A.7.1.2: (p. 366).
A.7. Ejercicios del Capítulo 9: Arrays y Listas 371
Recuerda que el adverbio delete remueve le valor, pero deja el espacio indefinido den-
tro del array. La función splice también removería el espacio, que es algo distinto a lo
que queremos si buscamos simular el comportamiento delete (aunque, en alguna man-
era, podría considerarse una mejora remover el espacio también). Para realmente simular
la función delete, es probablemente mejor si solo dejamos el valor “indefinido“:
Para mantener los elementos de una lista que son cuadrados perfectos, una manera de
hacerlo es chequear si la raíz cuadrada de cada número es un número entero. Por ejemplo:
Esto funciona bien con la muestra de datos del ejemplo, pero este programa fallará si in-
tentamos con un número negativo. Queremos evitar esa excepción y solo considerar que
un número negativo nunca puede ser un cuadrado perfecto.
Debido a que el bloque de código aquí se complicaría, podríamos mejor usar una referencia
de una función:
say sort &my_comp, <22ac 34bd 56aa3 12c; 4abc( 1ca 45bc>;
# -> (56aa3 4abc( 22ac 45bc 34bd 12c; 1ca)
my @desordenados = <22ac 34bd 56aa3 12c; 42acd 12cd; 4abc( 1ca 45bc 3dab!>;
my @ordenados = sort {/\d+ (\w+)/; $0 // Nil}, @desordenados;
say @ordenados;
# -> [56aa3 4abc( 22ac 42acd 45bc 34bd 12c; 1ca 12cd; 3dab!]
La única dificultad sintáctica aquí es que tenemos que aplanar las sub-listas $elem para
recorrerla. Esto podría también hacerse con el operado “|“:
Esta es otra manera de hacerlo, que usa un bucle for para recorrer el array externo y el
operador de reducción para agregar los elementos de las listas anidadas:
Usar la función map para aplanar las listas anidadas y el operador de reducción puede hacer
este código considerablemente más corto:
Al comparar esta solución que necesita una sola línea de código con la primera muestra lo
expresivo que el estilo de programación funcional puede ser para manejar arrays y listas y
posiblemente entiendes la razón por la cual he recomendado este estilo de programación
en este capítulo.
Estas soluciones funcionan bien porque se sabe que solo hay listas y listas anidadas (listas
de listas). ¿Qué pasa si el nivel de anidamiento no se conoce con antelación? Una solución
para esto sería el uso de una subrutina recursiva para explorar el árbol de listas:
374 Apéndice A. Soluciones a los Ejercicios
Recuerda que una técnica recursiva es usualmente una herramienta eficiente para manejar
datos encadenados o anidados.
Pero adivina qué? El código puede ser mucho más corto con la programación funcional.
Recuerda que el metaoperador de reducción puede darte una lista de resultados parciales:
Podrías pensar que he diseñado estos ejercicios para probar mi punto sobre el poder ex-
presivo de la programación funcional. ¡No del todo! Ambos ejercicios, este ejercicio y el
anterior, son tomados directamente del libro Think Python de Allen Downey en el cual este
libro está basado. No he escrito estos dos ejercicios pero solamente las soluciones presen-
tadas aquí.
A.7. Ejercicios del Capítulo 9: Arrays y Listas 375
Nota que *-1 se refiere al índice del último elemento de una array. Para desechar el último
elemento, podemos limitar el rango *-2.
my @nums = 5..10;
corta(@nums);
say @nums; # -> [6 7 8 9]
Usar una rebanada es más simple; solo asegúrate de asignar la rebanada el array para
modificar el array en lugar:
$previo = $actual;
}
return True;
}
say está-ordenada < 2 4 5 7 7 8 9>; # -> True
say está-ordenada < 2 4 5 7 6 8 9>; # -> False
Otra técnica podría simplemente comparar la lista de entrada con una versión de la misma:
sub está-ordenada(@array) {
return @array eqv @array.sort;
}
say is-sorted < 2 4 5 7 7 8 9>; # -> True
say is-sorted < 2 4 5 7 6 8 9>; # -> False
Mientras esto conduce a código más corto y simple, esta no es una solución óptima, porque
requiere que el programa ordene el array de entrada, lo cual es significativamente más
costoso que solamente recorrer el array, por lo menos cuando el array a ser chequeado es
largo.
Una vez más, la programación funcional y especialmente el hiperoperador de reducción
puede conducir a código mucho más corto que la primera solución, sin incurrir en el costo
de un ordenamiento adicional:s
Por cierto, esta última versión de está-ordenada hará “corto circuito“ y devolver False
tan pronto haya encontrado el valor que no se encuentra en el orden correcto, sin iterar
sobre el resto de la lista.
$ perl6 anagramas.pl6
ban bane: False
post stop: True
pots stop: True
post pots: True
pots taps: False
saco cosa: True
En este ejemplo, el bucle comienza con el índice 1 (y no 0) por cada artículo es comparado
con el artículo previo.
Otra forma es iterar sobre los elementos de una array ordenado y mantener un registro de
los artículos previos para habilitar la comparación:
378 Apéndice A. Soluciones a los Ejercicios
Otra posibilidad es usa la función unique de Perl 6, la cual devuelve una secuencia de
valores únicos desde la lista o array de entrada. Comparar el número de elementos de
la salida de output con el número de elementos de la lista original nos dirá si algunos
duplicados fueron removidos por unique:
Nota que Perl también tiene una función integrada repeated, la cual es la contraparte de
nique y devuelve los elementos duplicados de una lista:
u
Otra forma eficiente de encontrar o remover los duplicados de una lista o un array es
usar hashes, una estructura de datos integrada que discutimos en el Capítulo 10 (ver Ejerci-
cio 10.3).
my $cuenta-dupl = 0;
my $num-pruebas = 1000;
for 1..$num-pruebas{
$dupl-count++ if tiene-cumpleaños-duplicados 23; # 23 estudiantes
}
say "En $num-pruebas pruebas, $cuenta-dupl tenían "~
"por lo menos un cumpleaño duplicado";
$ perl6 birthdays.pl6
En 1000 pruebas, 498 tenían por lo menos un cumpleaño duplicado
$ perl6 birthdays.pl6
En 1000 pruebas, 505 tenían por lo menos un cumpleaño duplicado
$ perl6 birthdays.pl6
En 1000 pruebas, 527 tenían por lo menos un cumpleaño duplicado
$ perl6 birthdays.pl6
En 1000 pruebas, 491 tenían por lo menos un cumpleaño duplicado
Esta simulación confirma que con una muestra de 23 personas, hay una posibilidad aprox-
imada de 50% que al menos dos personas tendrán el mismo cumpleaño.
Nota que Perl tiene una función integrada roll que devuelve elementos se-
leccionados aleatoriamente desde una lista. Esto puede hacer la subrutina
tiene-cumpleaños-duplicados significativamente más concisa:
my $inicio_push = now;
my @push_array;
for 'words.txt'.IO.lines -> $linea {
push @push_array, $linea;
}
say "push tomó " ~ now - $inicio_push ~ " segundos.";
@push_array = ();
my $inicio_unsh = now;
my @unsh_array;
for 'words.txt'.IO.lines -> $linea {
unshift @unsh_array, $linea;
}
say "unshift tomó " ~ now - $inicio_unsh ~ " segundos.";
@unsh_array = ();
Inténtalo tu mismo y ejecútalo varias veces. Deberías notar que push es consistentemente
más rápido que unshift, aunque la diferencia no es tan abismal.
Presumiblemente, esto se debe a que unshift cuando está insertando elementos al
comienzo del array, Perl tiene que mover los datos muchas veces para reorganizar el ar-
ray completo. Por lo contrario, usar push para insertar elementos al final del array implica
menos mantenimiento interno de los datos.
Como un ejercicio adicional, puedes tratar de explorar otras maneras de poblar un array,
tal como append o splice.
Si vas a insertar cada línea del archivo de entrada en un array sin hacer ningún cambio a
esas líneas, sorber (slurp) todos los datos en el array sin un bucle será más simple y mucho
más rápido:
my $sorbido_inicio = now;
my @array_sorbido = 'words.txt'.IO.lines;
say "slurp tomó " ~ now - $sorbido_inicio ~ " segundos.";
Nota que realmente no necesitas llamar la función now al inicio del programa: puedes usar
INIT now para extraer el tiempo cuando el programa comenzó a ejecutarse:
my @array_sorbido = 'words.txt'.IO.lines;
say "slurp tomó " ~ (now - INIT now) ~ " segundos.";
'a' encontrada
'f' encontrada
'w' no encontrada
'e' encontrada
'q' no encontrada
'ab' no encontrada
'ce' no encontrada
Una opción involucra tener solamente una copia del array original, digamos una variable
global, y pasar rangos de índices. Pero variables globales son usualmente desaprobadas
porque tienden a ir en contra de los postulados de la programación estructurada y pueden
ser peligrosa (aunque este sería un caso donde el uso de variables globales hace sentido).
Podemos actualmente trabajar mejor sin el uso de variables globales y aún tener el ben-
eficio de no pasar el array completo una y otra vez gracias al hecho que, en Perl 6, las
subrutinas son clausuras, lo cual significa que pueden acceder variables que existen en el
entorno donde fueron creadas.
En el código siguiente, bisectar no es un subrutina recursiva; ahora, es una subrutina
muy simple que solo configura el entorno para bisectar2, la cual es la subrutina recursiva
y está definida dentro del cuerpo de bisectar. Dado que el array y la palabra a ser buscada
existen dentro de bisectar, bisectar2 será capaz de tener acceso a ellos. Los parámetros
de bisectar2 son solamente dos subíndices que representan el rango en el cual tendrá que
buscar la palabra:
Como un ejercicio adicional, adapta el programa anterior para buscar palabras en inglés en
words.txt y palabras en español en lemario.txt. Nota lo rápido que es. Por favor ten presente
que esto funciona porque las palabras en este archivo están ordenadas alfabéticamente.
Intenta cambiar el código para contar y mostrar el número de pasos necesarios para encon-
trar una palabra dada. Compara esto con el número de pasos que tomaría una búsqueda
lineal en promedio (i.e., recorrer el array linealmente hasta encontrar la palabra o pueda la
palabra ser declarad ausente). Puedes adivinar por qué este algoritmo es también llamado
búsqueda logarítmica?
A.7. Ejercicios del Capítulo 9: Arrays y Listas 383
my @array = 'words.txt'.IO.lines;
Con el algoritmo usado aquí, cada pareja de inversas se encuentra dos veces (una vez
por cada palabra de la pareja). Al examinar cualquier palabra de la lista, realmente no
necesitamos buscar hacia atrás en la parte de la lista anterior a esa palabra dado que si la
palabra forma una pareja con otra palabra que viene antes en el orden alfabético, ya hemos
encontrado la pareja cuando procesamos esa otra palabra. Por lo tanto sería más eficiente
y más rápido si hacemos la búsqueda solamente hacia adelante, i.e., buscar la palabra en
la parte de la lista que viene después de la palabra que está siendo examinada. Como un
ejercicio adicional, modifica el bucle for para buscar palabras hacia adelante.
La búsqueda binaria es bastante rápida, pero la búsqueda en una hash es mucho más ráp-
ida. Aunque aún no hemos discutido los hashes, intenta el código siguiente:
my $inversa = $palabra.flip;
say "$palabra y $inversa forman una pareja de inversas"
if %hash{$reverse}:exists;
}
say now - INIT now;
No te preocupes en entender el código por el tiempo presente, pero nota lo corto que es.
Y lo rápido que se ejecute: en la misma computadora portátil, el tiempo de ejecución es
alrededor de 16 segundos (menos de 0.15 milisegundos por búsqueda). Espero que esto
estimulará tu apetito para el Capítulo 10.
Nota que la salida de este ejemplo no está ordenada porque un hash no mantiene el orden
de los datos de entrada, como veremos en el Capítulo 10. Sería bien fácil mantener el
ordenamiento, por ejemplo al usar un array en adición al hash, pero ese no es realmente el
tema aquí.
Regresando a la subrutina bisectar, copiar y pegar esta subrutina desde el programa del
ejercicio anterior en el código de este ejercicio no es la mejor manera de reutilizar código.
Supón que un error es encontrado en esa subrutina; ahora necesita ser arreglado en dos
programas diferentes; la probabilidad que ese error sea corregido en un programa y olvi-
dado en el otro es bien significante. Aún si no se olvida, esto doblemente arduo, y esto
es incrementa la probabilidad de cometer otro error en el proceso. Aún si no hay ningún
error, podríamos necesitar un mejora y otra vez esto debe hacerse dos veces.
Una buena manera de reutilizar software mientras se mantiene una sola copia de la sub-
rutina reutilizada es insertarla en un módulo de Perl, i.e, en un archivo separado que será
cargado en nuestros programas para usarlo.
El módulo podría nombrarse BusquedaBinaria.pm y contener el código siguiente:
Nota que el nombre del módulo dado en la parte superior del código y el nombre del
archivo tienen que corresponder:
• BusquedaBinaria.pm
El otro cambio que se hizo al código de la subrutina fue agregar el rasgo is export a la
signatura de la subrutina.
Ahora un programa de Perl será capaz de cargar este módulo y usar las subrutinas
bisectar y mostrar-resultado. Por ejemplo:
use lib "."; # le dice a Perl que busque módulos en el directorio actual
use BusquedaBinaria;
my @array = 'a'..'m';
for < a f w e q ab ce g > -> $busqueda {
my $result = bisectar $busqueda, @array;
mostrar-resultado $result, $busqueda;
}
Perl tiene una lista de directorios que busca por módulos, los cuales pueden variar de
una instalación de Perl a otra. La primera línea use lib "."; le dice a Perl que también
busque por módulos en el directorio actual (i.e., el directorio donde se ejecuta el programa
que utiliza el módulo). Esto es solo un ejemplo; podrías preferir usar un directorio para
tus módulos. La segunda línea use BusquedaBinaria; le dice a Perl que cargue el módulo
e importe las subrutinas marcadas para exportación (por ejemplo, con is export) en el
módulo. Ahora, el programa puede usar las subrutinas bisectar y mostrar-resultado
como si hubiesen sido definidas dentro del programa.
¡Eso es todo, amigos! ¿Bien simple, cierto? ¡Solo inténtalo! Aunque existen otras cosas más
que saber sobre módulos, ya sabes lo suficiente para crear y usar módulos.
Podrías revisar algunas de las otras subrutinas que hemos creado hasta ahora y poner
aquellas que podrían ser útiles en un módulo. Pista: algunas de las subrutinas relacionadas
con cadenas de texto y arrays que hemos visto hasta ahora son buenas candidatas.
386 Apéndice A. Soluciones a los Ejercicios
my @array = 'words.txt'.IO.lines;
for @array -> $palabra {
my ($palabra1, $palabra2) = entrelazar($palabra);
say "$palabra: $palabra1, $palabra2" if
bisectar($palabra1, @array) >= 0 and
bisectar($palabra2, @array) >= 0;
}
La subrutina entrelazar no es óptima en el sentido que recorre las letras del array @letras
dos veces cada vez que es llamada. Podemos mejorarla usando un bloque puntiagudo que
toma dos parámetros ( una letra impar y otra par):
@pares.join, @impares.join;
}
Como un ejercicio adicional, puedes encontrar palabras que están entrelazadas en tres for-
mas, es decir, cada tercer letra forma una palabra, comenzando con la primera, segunda
o tercera? Pista: será probablemente más fácil si comienzas con la versión revisada de
entrelazar más arriba.
No es posible memoizar los casos donde $m o $n es cero, porque los otros valores son
desconocidos. Solo el código correspondiente a la última línea de código puede ser mem-
oizado, pero eso está bien porque hace casi todo el trabajo de cualquier manera.
El problema siguiente es que los hashes vistos hasta ahora tenían solamente una clave,
pero la función Ackermann toma dos parámetros. La solución simple es crear un clave
compuesta, i.e., concatenar los dos parámetros con un separador para crear las claves del
hash. Esto conduce a esta posible solución:
my %ack-memo;
sub mem-ack( Int $m, Int $n ) {
return $n + 1 if $m == 0;
return mem-ack($m - 1, 1) if $n == 0;
%ack-memo{"$m;$n"} = mem-ack($m - 1, mem-ack($m, $n-1))
unless %ack-memo{"$m;$n"}:exists;
return %ack-memo{"$m;$n"};
}
say mem-ack 3, 4;
my %ack-memo;
sub mem-ack (Int $m, Int $n) {
return $n + 1 if $m == 0;
return mem-ack($m - 1, 1) if $n == 0;
%ack-memo{"$m;$n"} = mem-ack($m - 1, mem-ack($m, $n-1))
unless %ack-memo{"$m;$n"}:exists;
return %ack-memo{"$m;$n"};
}
my $inicio = now;
say mem-ack 3, 4;
say "mem-ack runtime: ", now - $inicio;
dd %ack-memo;
Hemos usado claves compuestas para %ack-memo, pero podemos tener hashes multidimen-
sionales al igual que tenemos arrays multidimensionales (ver Sección 9.10. Solo necesita-
mos tener dos claves, cada una de dentro de un par de llaves:
my %h;
%h{'a'}{'b'}= 'ab';
%h{'a'}{'c'}= 'ac';
%h{'a'}{'d'}= 'ad';
%h{'b'}{'c'}= 'bc';
dd %h;
# -> Hash %h = {:a(${:b("ab"), :c("ac"), :d("ad")}), :b(${:c("bc")})}
my %i;
%i{'a';'b'} = 'ab';
%i{'a';'c'} = 'ac';
%i{'b';'c'} = 'bc';
dd %i; # -> Hash %i = {:a(${:b("ab"), :c("ac")}), :b(${:c("bc")})}
Como un ejercicio adicional, genera una lista de 50,000 enteros aleatorios entre 0 y
1,000,000,000, y después, usando los varios métodos que hemos demostrado, chequea si
esta lista contiene elementos duplicados y mide el tiempo de ejecución de estos varios
métodos. Si encuentras dificultades haciendo esto, estudia las soluciones a los ejercicios
“tiene duplicados“ (ver Subsección A.7.11) y la “paradoja del cumpleaños“ (ver Subsec-
ción A.7.12)) para obtener algunas pistas. Un ejemplo de un benchmarking simple es pre-
sentado en el ejercicio más arriba.
Una vez que tus subrutinas funcionan apropiadamente, ejecuta el proceso por lo menos 10
veces para ver si las diferencias son significantes.
390 Apéndice A. Soluciones a los Ejercicios
Rotar cada palabra de una lista con alrededor de 88,000 palabras por cada desplaza-
miento entre 1 y 13 es algo bien largo. Ejecutar el programa tomará algún tiempo, prob-
ablemente de 10 a 15 minutos. Usar la función integrada .trans (see documentation in
https://docs.perl6.org/routine/trans) podría acelerar el proceso. Inténtalo y juzga
por ti mismo.
Esta es una solución que usa la lista de palabras words.txt usada anteriormente y el dic-
cionario fonético CMU2 :
my %fonético;
cargar-fonético('cmu_dict.txt');
my %palabras = map { $_ => 1}, 'words.txt'.IO.lines;
Pero esto es algo ineficiente porque actualmente no necesitamos la lista de palabras, dado
que el diccionario CMU es otra lista de palabras que podemos usar (y no podemos usar pal-
abras que aparecería en la lista de palabras y no en el diccionario CMU, porque el programa
no sería capaz de averiguar cómo suenan). El programa siguiente usa solo el diccionario
CMU y ahorra el tiempo de cargar la lista de palabras y hacer chequeos en la misma:
my %fonético;
cargar-fonético('cmu_dict.txt');
5: Un dígito
Esto funciona perfectamente ahora, pero la salida para 42 no está ya en el mismo orden.
Si queremos mantener el orden original, necesitamos agregar una sentencia when con un
bloque vacío:
Sería igualmente posible anidar la sub-expresión when dentro de la expresión when 10..99:
La signatura asegura que el operando es un entero (lo cual levanta un error cuando falla).
Podemos protegernos de un número negativo, lo cual podemos hacer al levantar un error
si un $n es negativo. Además, podemos usar el módulo estándar Test para automatizar
nuestras pruebas:
La línea plan 5; dice que el plan de prueba contiene cinco pruebas individuales. Después
las dos primeras pruebas chequean el operador factorial falla para valores de entrada in-
válidos. Y chequea la salida por algunos valores de entrada.
1..5
ok 1 - Factorial falla para -1
ok 2 - Factorial falla para 2.5
ok 3 - Factorial 0
ok 4 - Factorial 1
ok 5 - Factorial de un entero grande
En este ejemplo, hemos colocado las pruebas en el mismo archivo que la definición de la
subrutina por la simplicidad del ejemplo. Normalmente, las pruebas estarían en un archivo
separado usualmente en un directorio llamado “t“ y con una extensión .t en el archivo.
Pruebas y el módulo Test serán discutidos en la Sección 14.8 (p. 316). Más información
sobre pruebas pueden encontrarse aquí: https://doc.perl6.org/language/testing.
Esto funciona bien, pero una vez que hemos hecho eso, podemos también deshacernos de
la estructura de datos $set-libro y solamente filtrar directamente las palabras extraídas
desde el libro:
La prueba de tal programa puede tomar algún tiempo, porque tiene que procesar el libro
completo cada vez. Para el propósito inicial de prueba, un consejo es reducir el monto
de datos de entrada para acelerar las pruebas. Podrías lograe eso al prepara un archivo
pequeño con un número limitado de líneas del archivo original quijote.txt. Otra forma
simple de hacerlo es leer solamente algunas líneas desde el libro, lo cual puedes hacer con
una rebanada en la línea de código que leer el archivo. Por ejemplo, para leer solo las
primeras 2,000 líneas del libro:
Esto puede también usarse para eliminar la cabecera. Dado que el texto actual del libro
comienza en la línea 37, podemos tener algo como esto:
y remover de procesar-línea el código para saltar la cabecera. Sin embargo, aún necesi-
tamos una bandera que indique el final del libro:
my $final = False;
sub procesar-línea( Str $línea is copy ) {
return if $final;
$línea ~~ s:g/<['-]>/ /;
$línea ~~ s:g/<[;:,¡!¿?«».()"_`]>//;
$línea = $línea.lc;
$final = True if $línea ~~ /^fin$/;
return $línea.words;
}
No obstante, hay otra forma de hacerlo, la subrutina MAIN que discutimos brevemente en
la Sección 4.15(p. 69). Si hay una subrutina llamada MAIN en el programa, entonces el
programa comenzará a ejecutar esta subrutina, cuyos parámetros serán los argumentos
pasados al programa en la línea de comando. Esto significa que la signatura de MAIN hará
posible la extracción de parámetros y chequear su validez.
En nuestra solución más abajo, la subrutina MAIN está declarada como sigue:
El programa chequeará que los argumentos pasados al mismo coincide con la signatura de
la subrutina MAIN. En el ejemplo, el primer parámetro tiene que ser una cadena de texto y
el segundo un entero; el tercero y el cuarto son opcionales y tendrán valores por defecto de
2 y 0 respectivamente si los argumentos correspondientes no son proveídos.
Si los argumentos pasados al programa no coinciden con la signatura de MAIN, el programa
terminará después de imprimir un mensaje de uso generado automáticamente:
El parámetro $linea-inicio tiene que ser un número entero. Dado que el argumento
correspondiente (“foo“) no es un entero, el programa muestra un mensaje que detalla el
uso del programa.
Validar los argumentos de la línea de comando pasados a un programa puede ser algunas
veces una tarea tediosa. Pero, con el mecanismo de signatura de la subrutina MAIN, puede
usualmente reducirse a una sola línea de código, la signatura MAIN.
Esta es una manera posible de realizar una análisis de Markov de un archivo de texto:
my %prefijos;
my $final = False;
sub procesar-línea( $orden, Str $linea is copy ) {
return if $final;
$linea ~~ s:g/<[-']>/ /;
$linea ~~ s:g/<[;:,!?.()"_`]>//; # removiendo signos de puntuación
$linea = $linea.lc; # convirtiendo a minúsculas
return unless $linea ~~ /\w/;
procesar-palabras($orden, $linea.words);
$final = True if $linea ~~ /^fin$/;
}
Este programa puede ser llamado en el archivo quijote.txt con la siguiente sintaxis:
o usando emma.txt:
Hemos ya visto problemas similares a este. Esta es una solución posible usando el modelo
de programación de tuberías que describimos en la Sección 11.7 (page 204):
my %frecuencias;
%frecuencias{$_}++ for grep {/<[a..z]>/}, map {.lc},
"goldbug.txt".IO.lines.comb;
my $cuenta_total = [+] values %frecuencias;
say "$_ :\t%frecuencias{$_} \t",
sprintf "%5.2f", %frecuencias{$_}*100/$cuenta_total
for reverse sort {%frecuencias{$_}}, %frecuencias.keys;
Esto muestra:
e : 7625 13.10
t : 5485 9.42
a : 4477 7.69
o : 4208 7.23
i : 4183 7.18
n : 3912 6.72
s : 3516 6.04
h : 3372 5.79
r : 3278 5.63
d : 2533 4.35
l : 2324 3.99
u : 1893 3.25
c : 1523 2.62
m : 1499 2.57
f : 1392 2.39
w : 1303 2.24
p : 1169 2.01
y : 1146 1.97
g : 1143 1.96
b : 1031 1.77
v : 525 0.90
k : 351 0.60
x : 120 0.21
j : 111 0.19
q : 60 0.10
z : 44 0.08
Recuerda que el personaje de Edgar Allan Poe declaró que la sucesión de letras más común-
mente utilizadas en inglés es como sigue:
e a o i d h n r s t u y c f g l m w b k p q x z
Al parecer el personaje de Poe estaba aproximadamente correcto, pero no del todo cierto,
en su estimados de las frecuencias de las letras en un texto en inglés. Aparentemente él
A.9. Ejercicio del Capítulo 11: Estructuras de Datos 401
loop {
my $menor1 = shift @pares;
my $menor2 = shift @pares;
my $par-nuevo = $menor1.key ~ $menor2.key => $menor1.value + $menor2.value;
insertar-par @pares, $par-nuevo;
%código{$menor1.key} = $menor1.key ~ $menor2.key ~ "|.";
%código{$menor2.key} = $menor1.key ~ $menor2.key ~ "|-";
last if @pares <= 2;
}
%código{@pares[0].key} = ".";
%código{@pares[1].key} = "-";
y el hash %código contienes los códigos parciales para letra o letra ficticia:
{a => ga|-, c => ., g => ga|., ga => tga|-, t => tga|., tga => -}
Después usamos otro bucle para sustituir las letras falsas y deshacernos de ellas, hasta que
quedemos con las letras actuales de la cadena de texto original:
loop {
my $hecho = True;
for %código.keys -> $letra {
next if $letra.chars > 1;
my ($val, $código) = split '|', %código{$letra};
next unless defined $val and defined $código;
$hecho = False;
my $result = %código{$val} ~ $código;
%código{$letra} = $result;
}
last if $hecho;
}
my %codificar;
%codificar{$_} = %código{$_} for grep {$_.chars < 2 }, %código.keys;
c => .
t => -.
g => --.
a => ---
A.9.6.3 Código Huffman para una Cadena de Texto Más Compleja (Sección 11.9)
Para esta pregunta, necesitaremos un párrafo pequeño escrito especialmente con un sola-
mente algunas letras del alfabeto:
A.9. Ejercicio del Capítulo 11: Estructuras de Datos 403
Eastern Tennessee anteaters ensnare and eat red ants, detest ant antennae (a
tart taste) and dread Antarean anteater-eaters. Rare Andean deer eat tender sea
reeds, aster seeds and rats’ ears. Dessert? Rats’ asses.
my $cadena = "Eastern Tennessee anteaters ensnare and eat red ants, detest ant
antennae (a tart taste) and dread Antarean anteater-eaters. Rare
Andean deer eat tender sea reeds, aster seeds and rats’ ears. Dessert?
Rats’ asses.";
my %frecuencias;
%frecuencias{$_}++ for grep { /\w/ }, $cadena.lc.comb;
Este tratado muy elocuente sobre el hábito alimenticio de varios animales produce la tabla
de frecuencias siguiente:
e : 40 23.53
a : 32 18.82
t : 24 14.12
s : 22 12.94
n : 20 11.76
r : 19 11.18
d : 13 7.65
Usando el mismo código al igual que en la pregunta anterior genera la tabla de Huffman
siguiente:
a => ..
e => .-
s => -.-
n => -..
t => --.
d => ---.
r => ----
No solo queremos codificar una cadena de texto de entrada con el código Huffman sino que
también queremos ser capaces de decodificarla y reconocer la entrada original. Debido
a eso, ya no queremos filtrar la puntuación de la tabla de traducción, la cual será más
grande que antes. Los espacios ( espacios horizontales y retornos de línea) serán manejados
diferentemente: no los modificaremos en la cadena de texto codificada de pseudo-Morse,
dado que esto hará más fácil de chequear y mostrar el resultado.
La tabla de frecuencias ahora incluye signos de puntuación que existen en la cadena de
texto de entrada:
e : 40 22.10
a : 32 17.68
t : 24 13.26
s : 22 12.15
n : 20 11.05
r : 19 10.50
d : 13 7.18
. : 3 1.66
, : 2 1.10
’ : 2 1.10
) : 1 0.55
- : 1 0.55
? : 1 0.55
( : 1 0.55
e => .-
a => ---
s => -..
n => ..-
t => --.
r => ...
d => -.--
. => -.-.-.
( => -.-..-.
’ => -.-.--.
? => -.-..--
) => -.-...-
- => -.-....
, => -.-.---
Cada letra de la entrada es convertida a letras minúsculas (dado que hemos limitado nues-
tra tabla a minúsculas), traducida a su código equivalente de pseudo-Morse , y concate-
nada a la cadena de texto de salida. Si la letra no se encuentra en el hash %codificar,
entonces se almacena en la salida tal como es: esto posibilita la inserción de espacios y
caracteres de final de línea en la cadena de texto de salida.
Aquí presentamos el resultado (formateado para adaptarlo al libro):
A.9. Ejercicio del Capítulo 11: Estructuras de Datos 405
.-----..--..-.....-
el primer punto no es una entrada pero la combinación ".-" es una "e". La siguiente raya
no es una entrada y tampoco lo es "--", pero "---" es una "a".
406 Apéndice A. Soluciones a los Ejercicios
La siguiente raya no es una entrada y tampoco lo es "-.", pero "-.." es una "s". Simi-
larmente, los tres caracteres siguientes, "--.", forman una "t", y podemos decodificar la
letra "eastern".
Podríamos implementar esto con dos bucles anidados: uno itera a través de la cadena de
texto y el segundo consume el número necesario de puntos y rayas hasta el final de una
letra:
sub decodificación( Str $entrada, %decodificar ) {
my @códigos = $entrada.comb;
my $salida;
loop {
last unless @códigos;
my $actual = shift @códigos;
$salida ~= $actual and next if $actual ~~ /\s/;
$salida ~= %decodificar{$actual} and
next if %decodificar{$actual}:exists;
Aquí, la variable $actual acumula los puntos y las rayas de la cadena de entrada hasta se
determina que es una entrada en la tabla de traducción, en cuyo momento se reinicia a una
cadena de texto vacía para prepararla para la letra siguiente.
La solución presentada más arriba para encontrar el código Huffman usa la subrutina
insertar-par para mantener el array de pares @pares ordenado. Esto hace fácil encontrar
las letras restantes menos comunes o pseudo-letras. Podrías recordar de la Sección 11.15.3
que los montículos binarios son estructuras de datos grandiosas cuyo objetivo es acceder
rápidamente los artículos más pequeños de una colección. Como un ejercicio adicional, po-
drías escribir la solución nuevamente usando un montículo binario. La solución original
de David Huffman actualmente usó un árbol (llamado el árbol de Huffman) muy similar a
un montículo binario.
Para comenzar, excluyamos las fechas de febrero que son mayores que 29. Esto puede
hacerse al expandir la aserción de código mostrada en el código para reconocer las fechas:
if $cadena ~~ /<fecha>/ {
say ~$/; # 2016-02-29
say "Día\t= " , ~$/<fecha><día>; # 29
say "Mes\t= " , ~$/<fecha><mes>; # 02
say "Year\t= " , ~$/<fecha><año>; # 2016
}
Esto funciona. Febrero tiene 29 días dado que 2016 es un año bisiesto. Pero este código
validaría Feb. 29 para 2015 o 2017, lo cual es incorrecto dado que no son años bisiestos.
408 Apéndice A. Soluciones a los Ejercicios
En el calendario Juliano antiguo (en honor a Julio César), los años bisiestos son años que
son divisibles por 4. Resultó que que el calendario Juliano tenía muchos años bisiestos
para reflejar la realidad astronómica, así que el calendario retrocedió alrededor de 3 días
por cada período de cuatro siglos.
El calendario Gregoriano, introducido por el Papa Gregorio XIII en 1582, corrigió el calen-
dario Juliano con la siguiente regla adicional: años divisibles por 100 deberían considerarse
años bisiestos solo si son divisibles por 400. Así que, para el calendario Gregoriano, 1700,
1800, 1900, y 2100 no son bisiestos pero 2000 y 2400 si lo son.
Dependiendo del tipo de fechas que tu programa encontrará, puedes simplificar las re-
glas. Si está escribiendo un módulo que está supuesto a ser preciso con cualquier fecha del
pasado o futuro, probablemente quieres implementar la regla Gregoriana exacta. Pero si
sabes que vas a encontrar fechas del período actual, podrías elegir una regla mucho más
simple.
En particular, dado que 2000 es una excepción a la excepción y es bisiesto, cualquier año
entre 1901 y 2099 es bisiesto si es divisible por 4 y no bisiesto por el contrario. Esta reglas
es probablemente suficiente para cualquier aplicación de negocios escrita en 2017. Proba-
blemente no hay razón para hacerlo más complicado de lo que necesita ser (aunque podría
argumentarse que es el tipo de razonamiento que condujo al gran terror del error “Y2K“).
Con esta simplificación en mente, una subrutina para encontrar si un año es bisiesto de-
bería simplemente devolver verdadero si es divisible por 4 y podría lucir así:
sub es-bisiesto( $año ) { # funciona para los años entre 1901 y 2099
return True if $año %% 4;
return False;
}
O más simple:
sub es-bisiesto( $año ) { # funciona para los años entre 1901 y 2099
return $año %% 4;
}
El código más arriba es dado como un ejemplo acerca de cómo computar si un año es
bisiesto, dado que es un problema interesante y clásico, pero Perl actualmente provee un
método para eso en el rol Dateish. Por ejemplo:
A.10. Ejercicios del Capítulo 13: Regexes y Gramáticas 409
Puedes agregar las reglas para Feb. 29 en el ejemplo de código más arriba, pero sugerimos
que esto se está complicando para una aserción de código dentro de la regla fecha: agre-
gar una condición Booleano en una aserción de código dentro de una regla está bien, pero
cuando la condición se vuelve más complicada, esto causa que la regla sea difícil de enten-
der. Piensa sobre la persona que tendrá que mantener tu código dentro de una año (y esa
persona podría ser tú).
Preferimos mover el código que realiza la validación fuera de la regla fecha a una sub-
rutina dedicada a chequear todas las fechas para febrero:
Una vez que hemos introducido esta mínima subrutina, podríamos ir un paso más allá
y mover el resto de la aserción de código dentro de la subrutina, para que la aserción de
código final contenga solo una llamada a la nueva versión de la subrutina. Esto se deja
como un ejercicio adicional al lector.
A.10.2 Ejercicio 13.12 (p. 284): Una Gramática para una Calculador Arit-
mética
Aquí presentamos una forma posible de implementar una calculadora aritmética.
410 Apéndice A. Soluciones a los Ejercicios
A.10.2.1 La Gramática
my grammar Calculadora {
rule TOP { <expr> }
rule expr { <término> + % <op-suma-sust> }
token op-suma-sust { [< + - >] }
rule término { <átomo> + % <op-mult-div> }
token op-mult-div { [< * / >] }
rule átomo {
| <número> { make +$<número> }
| <expr-paren> { make $<expr-paren>.made }
}
rule número { <signo> ? [\d+ | \d+\.\d+ | \.\d+ ] }
rule expr-paren { '(' <expr> ')' }
token signo { [< + - >] }
}
Nota que hemos incluido dos acciones en la gramática (en la regla átomo). Una razón para
hacer eso es por conveniencia: dado que la regla átomo cubre dos sub-reglas nombradas
diferentes, es un poco más fácil incluir la acción solo en el contexto de las sub-reglas. Si una
acción se había adjuntado a la regla átomo, la regla hubiese requerido encontrar cual sub-
regla había coincidido para saber cual acción debía realizar. Nada difícil, pero hacer esto
habría hecho el código mucho más complejo. La otra razón para hacerlo es por propósito
pedagógicos: aunque algunas veces hace sentido crear una clase de acciones, es igualmente
útil saber que las acciones pueden insertarse en la parte de la gramática. Para un gramática
bien simple, podría ser innecesario crear una clase de acciones con solo una o dos acciones.
class AccionesCalc {
method TOP ($/) {
make $<expr>.made
A.10. Ejercicios del Capítulo 13: Regexes y Gramáticas 411
}
method expr ($/) {
$.calcular($/, $<término>, $<op-suma-sust>)
}
method término ($/) {
$.calcular($/, $<átomo>, $<op-mult-div>)
}
method expr-paren ($/) {
make $<expr>.made;
}
method calcular ($/, $operandos, $operadores) {
my $result = (shift $operandos).made;
while my $op = shift $operadores {
my $número = (shift $operandos).made;
given $op {
when '+' { $result += $número; }
when '-' { $result -= $número; }
when '*' { $result *= $número; }
when '/' { $result /= $número; }
default { die "operador desconocido"}
}
}
make $result;
}
}
El método calcular computa las expresiones (términos separados por los operadores de
adición o sustracción) y términos (átomos separados por los operadores de multiplicación
o división) desde la izquierda hacia la derecha dado que los operadores son asociativos por
la izquierda.
Esta gramática para una calculadora y su asociada clase de acciones puede probarse con el
código siguiente:
for |< 3*4 5/6 3+5 74-32 5+7/3 5*3*2 (4*5) (3*2)+5 4+3-1/5 4+(3-1)/4 >,
"12 + 6 * 5", " 7 + 12 + 23", " 2 + (10 * 4) ", "3 * (7 + 7)" {
my $result = Calculadora.parse($_, :actions(AccionesCalc));
# say $result;
printf "%-15s %.3f\n", $/, $result.made if $result;
}
3*4 12.000
5/6 0.833
3+5 8.000
74-32 42.000
5+7/3 7.333
5*3*2 30.000
(4*5) 20.000
(3*2)+5 11.000
412 Apéndice A. Soluciones a los Ejercicios
4+3-1/5 6.800
4+(3-1)/4 4.500
12 + 6 * 5 42.000
7 + 12 + 23 42.000
2 + (10 * 4) 42.000
3 * (7 + 7) 42.000
Puede preguntarte si este código funciona correctamente con expresiones entre paréntesis
anidadas. Originalmente pensé, cuando escribí este código, que podría tener un fallo y que
tal vez necesitaba cambiar o añadir algo para conseguir que las expresiones entre parénte-
sis funcionaran correctamente. Por ejemplo, considera la siguiente prueba de código con
expresiones entre paréntesis relativamente anidadas:
El resultado es correcto:
(((2+3)*(5-2))-1)*3 42.000
2 * ((4-1)*((3*7) - (5+2))) 84.000
Esta versión funcional del programa refleja directamente la estrategia del algoritmo de
ordenamiento rápido:
A.11. Ejercicios del Capítulo 14: Programación Funcional 413
• Si el array tiene menos de dos artículos, ya está ordenado, y por lo tanto devuélvelo
inmediatamente (este es el caso base que parará la recursión).
• Por el contrario, elige un artículo como un pivote (aquí, nosotros elegimos el ele-
mento del medio o uno inmediatamente cerca del medio).
• Divide el array en tres sub-listas que contienen artículos respectivamente más pe-
queños, mayores que e iguales al pivote.
Como se mencionó previamente, el pivote ideal sería la mediana de los valores, pero el
costo de encontrar la mediana sería exorbitante.
En principio, podrías elegir cualquier artículo como el pivote, incluyendo el primer o úl-
timo elemento del array. Pero para entradas específicas (tales como arrays que ya están
casi ordenados, de forma ascendente o descendente), esto puede incrementar significativa-
mente el tiempo de ejecución por las particiones (o divisiones) se vuelven desequilibradas,
y por lo tanto perdiendo la ventaja de la estrategia de dividir y conquistar. Elegir un el-
emento en el medio, como hicimos aquí, reduce la probabilidad de tal comportamiento
patológico. Otra posible forma posible de prevenir tal riesgo es seleccionar el pivote aleato-
riamente entre los elementos del array.
Índice
for, 64, 65, 67, 101, 113, 158, 159, 236, base, 67, 72, 85, 91, 316, 413
288, 355, 383, 387 corner, 147, 347
infinito, 96, 100, 104, 161, 327, 358, 365, edge, 147, 347
389 especial, 146, 147
palabra clave, 160 kebab, 20
recorrido, 113 mayúscula, 19
sentencia, 160 minúscula, 19
until, 97 título, 19
while, 95, 113, 351 categoría
byte, 224 de caracteres, 118, 263, 355–357
de caracteres negada, 119
cálculo lambda, 288 de clases, 127, 133
código cero, índice comienza con, 108
andamiaje, 325 Cervantes, Miguel de, 248
de fuente abierta, 49 Chekhov, Anton, 248
de longitud variable, 221 chequeo
morse, 221, 222 de consistencia, 192
muerto, 76, 89, 325 de error, 85
reuso de, 246 Church, Alonzo, 288, 310
cabecera, 37, 50 cifrado, 222
cache, 171, 188, 189, 308, 309, 320 cifrado César, 135, 194, 260, 360
cadena de texto, 9, 14, 107, 213, 351 ciudadano de primera clase, 285, 320
concatenación, 25, 111 clase, 229–231, 246, 247, 261
longitud, 108 atributo, 234, 261
operación, 25 de acciones, 269, 410
operador de relación, 57 definición, 231
operadores, 108, 110 herencia, 239
recorrido inverso, 351, 353 hija, 240, 242, 261
vacía, 108, 132 padre, 240, 242, 243, 261
Caesar, Julius, 408 Pixel, 240, 241
caja negra, 6, 49, 231, 251 Punto2D, 231, 232, 234
calculadora, 15, 29, 284, 409 Punto3D, 250
aritmética, 272, 273, 284, 409 PuntoMovible, 242
gramática, 284 Rectángulo, 237
calendario subclase, 240, 242
Gregoriano, 408 clausura, 99, 288, 290, 301, 320, 369, 382
Juliano, 408 whatever, 309, 310
canino, 244 clave, 177, 193
captura, 264, 283, 357, 362 coerción, 25, 26, 33, 57, 151, 153, 169, 208,
regex, 264 377, 378
capturas, 115, 123, 129, 265 tipo, 45
enumeradas, 123 coincidencia
nombradas, 265, 267, 279 de cadena de texto, 116
numeradas, 265 inteligente, 197
Car Talk, 147, 148, 194, 362–364 literal, 117
carácter, 107 cola, 214
comodín, 118 coma, 11
de nueva línea, 137 coma flotante, 9, 103
carnívoro, 244 comentario, 26, 28, 324, 351
caso multilínea, 351
Índice 417
triángulo, 72, 338 YAGNI: you aren’t gonna need it, 323
tronco (árbol), 215
True
valor especial, 56, 130
Turing
lenguaje completo, 81
tesis, 81
Turing, Alan, 81
twigil, 160, 199, 233, 237, 288
++ --
** METAOPERADORES DECLARADORES DE ÁMBITO
unary + - ~ ! ? ^ [op] reducir listop to A op B op C... my ámbito lexical
* / % %% div op= A = A op B our ámbito de paquete
+ - !op !(A op B) has ámbito de instancia
x xx »op« hyper/vectorizar anon no ámbito
~ Zop zip con operador op state lexical persistente
& Xop cross con operador op augment parasítico benigno
| ^ Rop invertir argumentos
supersede parasítico maligno
sleep abs sin temp let Sop crear secuencia
<=> leg cmp .. but SINTAXIS DE CONTROL
~~ > == gt eq === eqv !op for LIST { } # $_ argumento implícito
&& for LIST -> $a, $b { } # argumentos explícitos
|| ^^ // min max while/until EXPR { } # mientra TRUE/hasta True
??!! ff repeat while/until EXPR { } # hacer por lo menos una vez
= := op= => loop { } loop (a;b;c) { } # paréntesis requeridos!
so not if EXPR { } elsif EXPR { } else { } # estado de verdad
, : with EXPR { } orwith EXPR { } else { } # estado de definición
X Xop Z Zop ... given EXPR { when EXPR { } default { } } # sentencia switch
say die map etc. EXPR if EXPR for LIST; # comprensiones de listas
and next, last, redo # controles de bucle
loosest
Q con cualquier delimitador puede usarse para citar cadenas verbatim sin interpolación o escape.
No obstante, su comportamiento puede cambiarse a través del uso de adverbios:
Características Básicas
corta larga significado ejemplo
:q :single Interpola \\, \qq y escape de delim. con \ Q:q[\s] == Q:q[\\s]
:qq :double Interpola con :s, :a, :h, :f, :c, :b Q:qq!$a, {2**3}, @let[]!
:s :scalar Interpola variables $ Q:s^$c^
ADVERBIOS
Nota: Por defecto, solo las variables con el sigilo $ son interpoladas. Aun si usas Q:a{...} o
Q:h{...}, debes añadir [] al array o {} al hash para realizar la interpolación. Por ejemplo,
Q:a{@let[]} y Q:h!%bin{}! serán interpolado, pero no Q:a{@let} y Q:h!%bin!. Esto evita la
interpolación indeseada en casos como "[email protected]" o "es.wikipedia.org/wiki/%C3%91".
Características Avanzadas
corta larga significado ejemplo
:x :exec Ejecutar comando y devolver resultado Q:x{date}
:w :words Dividir en palabras (no protección de quote) .say for Q:w{'a b' c}
ADVERBIOS
:ww :quotewords Dividir palabras(con protección de quote) .say for Q:ww{'a b' c}
:v :val Convertir en alomorfo si es posible Q:v{42}.^name
:to :heredoc Analizar como terminador heredoc Q:to{END};
Content starts in
Algunos Atajos next line
forma adverbial atajo forma usual significado END
Q:q[...] q[...] '...'
Q:qq[...] qq[...] "..."
Q:q:w[...] qw[...] <...> word quoting
Q:q:ww[...] qww[...] word quoting con protección de quote
Q:qq:w[...] qqw[...] word quoting con interpolación
Q:qq:ww[...] qqww[...] <<...>> or « » word quoting con interpolación y protección de quote
Q:q:x[...] qx[...] shell quoting
Q:qq:x[...] qqx[...] shell quoting con interpolación
RECURSOS
ENLACES IRC -- irc.freenode.net SOCIAL
perl6.org #perl6 /r/perl6
rakudo.org #perl6-dev @perl6org
#moarvm
v2 2018, 07/28th