Los parámetros con nombre hacen que el código sea más fácil de leer, más difícil de escribir
Cuando leo un fragmento de código, los parámetros con nombre pueden introducir un contexto que hace que el código sea más fácil de entender. Considere, por ejemplo, este constructor: Color(1, 102, 205, 170)
. ¿Qué demonios significa eso? De hecho, Color(alpha: 1, red: 102, green: 205, blue: 170)
sería mucho más fácil de leer. Pero, por desgracia, el compilador dice "no" , quiere Color(a: 1, r: 102, g: 205, b: 170)
. Al escribir código usando parámetros con nombre, pasa una cantidad innecesaria de tiempo buscando los nombres exactos; es más fácil olvidar los nombres exactos de algunos parámetros que olvidar su orden.
Esto una vez me mordió al usar una DateTime
API que tenía dos clases de hermanos para puntos y duraciones con interfaces casi idénticas. Si bien DateTime->new(...)
aceptó un second => 30
argumento, el DateTime::Duration->new(...)
deseado seconds => 30
, y similar para otras unidades. Sí, tiene mucho sentido, pero esto me mostró que los parámetros nombrados son fáciles de usar.
Los malos nombres ni siquiera hacen que sea más fácil de leer
Otro ejemplo de cómo los parámetros nombrados pueden ser malos es probablemente el lenguaje R. Este fragmento de código crea un diagrama de datos:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
Se ven dos argumentos posicionales para el X y Y las filas de datos, y luego una lista de parámetros con nombre. Hay muchas más opciones con valores predeterminados, y solo se enumeran aquellos cuyos valores predeterminados quería cambiar o especificar explícitamente. Una vez que ignoramos que este código usa números mágicos y podría beneficiarse del uso de enumeraciones (¡si R tuviera alguno!), El problema es que muchos de estos nombres de parámetros son bastante indescifrables.
pch
es en realidad el carácter de la trama, el glifo que se dibujará para cada punto de datos. 17
es un círculo vacío, o algo así.
lty
es el tipo de línea Aquí 1
hay una línea continua.
bty
es el tipo de caja Configurarlo para "n"
evitar que se dibuje un cuadro alrededor de la trama.
ann
controla la apariencia de las anotaciones de eje.
Para alguien que no sabe lo que significa cada abreviatura, estas opciones son bastante confusas. Esto también revela por qué R usa estas etiquetas: no como código autodocumentado, sino (siendo un lenguaje de tipo dinámico) como claves para asignar los valores a sus variables correctas.
Propiedades de parámetros y firmas
Las firmas de funciones pueden tener las siguientes propiedades:
- Los argumentos pueden ser ordenados o desordenados,
- con o sin nombre,
- requerido u opcional.
- Las firmas también se pueden sobrecargar por tamaño o tipo,
- y puede tener un tamaño no especificado con varargs.
Diferentes idiomas aterrizan en diferentes coordenadas de este sistema. En C, los argumentos están ordenados, sin nombre, siempre son obligatorios y pueden ser varargs. En Java, la situación es similar, excepto que las firmas pueden sobrecargarse. En el Objetivo C, las firmas se ordenan, nombran, requieren y no se pueden sobrecargar porque es solo azúcar sintáctico alrededor de C.
Los idiomas escritos dinámicamente con varargs (interfaces de línea de comandos, Perl, ...) pueden emular parámetros con nombre opcionales. Los idiomas con sobrecarga de tamaño de firma tienen algo así como parámetros opcionales posicionales.
Cómo no implementar parámetros con nombre
Cuando pensamos en parámetros con nombre, generalmente asumimos parámetros con nombre, opcionales y sin ordenar. Implementar estos es difícil.
Los parámetros opcionales pueden tener valores predeterminados. Estos deben ser especificados por la función llamada y no deben compilarse en el código de llamada. De lo contrario, los valores predeterminados no se pueden actualizar sin volver a compilar todo el código dependiente.
Ahora, una pregunta importante es cómo se pasan los argumentos a la función. Con parámetros ordenados, los argumentos se pueden pasar en un registro o en su orden inherente en la pila. Cuando excluimos los registros por un momento, el problema es cómo colocar argumentos opcionales desordenados en la pila.
Para eso, necesitamos un cierto orden sobre los argumentos opcionales. ¿Qué pasa si se cambia el código de declaración? Como el orden es irrelevante, un reordenamiento en la declaración de función no debería cambiar la posición de los valores en la pila. También debemos considerar si es posible agregar un nuevo parámetro opcional. Desde la perspectiva de los usuarios, esto parece ser así, porque el código que no utilizó ese parámetro anteriormente debería funcionar con el nuevo parámetro. Por lo tanto, esto excluye pedidos como el uso del orden en la declaración o el uso del orden alfabético.
Considere esto también a la luz del subtipo y el Principio de sustitución de Liskov: en la salida compilada, las mismas instrucciones deberían poder invocar el método en un subtipo con posiblemente nuevos parámetros con nombre y en un supertipo.
Posibles implementaciones
Si no podemos tener un orden definitivo, entonces necesitamos una estructura de datos desordenada.
La implementación más simple es simplemente pasar el nombre de los parámetros junto con los valores. Así se emulan los parámetros nombrados en Perl o con las herramientas de línea de comandos. Esto resuelve todos los problemas de extensión mencionados anteriormente, pero puede ser una gran pérdida de espacio, no una opción en el código de rendimiento crítico. Además, procesar estos parámetros con nombre ahora es mucho más complicado que simplemente extraer valores de una pila.
En realidad, los requisitos de espacio se pueden reducir mediante la agrupación de cadenas, que puede reducir las comparaciones de cadenas posteriores a las comparaciones de puntero (excepto cuando no se puede garantizar que las cadenas estáticas se agrupen realmente, en cuyo caso las dos cadenas tendrán que compararse en detalle).
En cambio, también podríamos pasar una estructura de datos inteligente que funcione como un diccionario de argumentos con nombre. Esto es barato en el lado de la persona que llama, porque el conjunto de claves se conoce estáticamente en la ubicación de la llamada. Esto permitiría crear una función hash perfecta o precalcular un trie. La persona que llama aún tendrá que probar la existencia de todos los posibles nombres de parámetros, lo cual es algo costoso. Python usa algo como esto.
Entonces es demasiado caro en la mayoría de los casos
Si una función con parámetros con nombre debe ser extensible correctamente, no se puede suponer un orden definitivo. Entonces solo hay dos soluciones:
- Haga que el orden de los parámetros nombrados sea parte de la firma y no permita cambios posteriores. Esto es útil para el código de autodocumentación, pero no ayuda con argumentos opcionales.
- Pase una estructura de datos de valor clave al destinatario, que luego debe extraer información útil. Esto es muy costoso en comparación, y generalmente solo se ve en lenguajes de script sin un énfasis en el rendimiento.
Otras trampas
Los nombres de variables en una declaración de función generalmente tienen algún significado interno y no son parte de la interfaz, incluso si muchas herramientas de documentación todavía los mostrarán. En muchos casos, desearía diferentes nombres para una variable interna y el argumento con nombre correspondiente. Los idiomas que no permiten elegir los nombres visibles externamente de un parámetro con nombre no ganan mucho si el nombre de la variable no se utiliza teniendo en cuenta el contexto de la llamada.
Un problema con las emulaciones de argumentos con nombre es la falta de comprobación estática en el lado de la persona que llama. Esto es especialmente fácil de olvidar al pasar un diccionario de argumentos (mirándote, Python). Esto es importante porque pasando un diccionario es una solución común, por ejemplo en JavaScript: foo({bar: "baz", qux: 42})
. Aquí, ni los tipos de valores ni la existencia o ausencia de ciertos nombres se pueden verificar estáticamente.
Emulación de parámetros con nombre (en lenguajes estáticamente escritos)
Simplemente usar cadenas como claves, y cualquier objeto como valor no es muy útil en presencia de un verificador de tipo estático. Sin embargo, los argumentos con nombre se pueden emular con estructuras o literales de objeto:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});