Tratar con no saber los nombres de los parámetros de una función cuando la está llamando

13

Aquí hay un problema de programación / lenguaje en el que me gustaría escuchar sus pensamientos.

Hemos desarrollado convenciones que la mayoría de los programadores (deberían) seguir que no son parte de la sintaxis de los lenguajes pero sirven para hacer que el código sea más legible. Por supuesto, estos son siempre un tema de debate, pero hay al menos algunos conceptos básicos que la mayoría de los programadores encuentran agradables. Nombra tus variables apropiadamente, nombra en general, hace que tus líneas no sean exageradamente largas, evitando funciones largas, encapsulaciones, esas cosas.

Sin embargo, hay un problema que todavía no he encontrado a nadie comentando y que podría ser el más grande del grupo. Es el problema de que los argumentos sean anónimos cuando se llama a una función.

Las funciones provienen de las matemáticas donde f (x) tiene un significado claro porque una función tiene una definición mucho más rigurosa que la que suele tener en la programación. Las funciones puras en matemáticas pueden hacer mucho menos de lo que pueden hacer en programación y son una herramienta mucho más elegante, generalmente solo toman un argumento (que generalmente es un número) y siempre devuelven un valor (también generalmente un número). Si una función toma múltiples argumentos, casi siempre son solo dimensiones adicionales del dominio de la función. En otras palabras, un argumento no es más importante que los otros. Están ordenados explícitamente, claro, pero aparte de eso, no tienen un orden semántico.

Sin embargo, en programación, tenemos más funciones que definen la libertad, y en este caso diría que no es algo bueno. Una situación común, tienes una función definida como esta

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Mirando la definición, si la función está escrita correctamente, está perfectamente claro qué es qué. Al llamar a la función, incluso podría tener algo de magia de finalización de código / intellisense en su IDE / editor que le dirá cuál debería ser el siguiente argumento. Pero espera. Si necesito eso cuando estoy escribiendo la llamada, ¿no hay algo que nos falta aquí? La persona que lee el código no tiene el beneficio de un IDE y, a menos que salte a la definición, no tiene idea de cuál de los dos rectángulos pasados ​​como argumentos se utiliza para qué.

El problema va más allá que eso. Si nuestros argumentos provienen de alguna variable local, puede haber situaciones en las que ni siquiera sabemos cuál es el segundo argumento, ya que solo vemos el nombre de la variable. Tome por ejemplo esta línea de código

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Esto se alivia en diversos grados en diferentes idiomas, pero incluso en lenguajes estrictamente tipados e incluso si nombra sus variables con sensatez, ni siquiera menciona el tipo que es la variable cuando la pasa a la función.

Como suele ocurrir con la programación, hay muchas posibles soluciones a este problema. Muchos ya están implementados en idiomas populares. Parámetros con nombre en C #, por ejemplo. Sin embargo, todo lo que sé tiene inconvenientes significativos. Nombrar cada parámetro en cada llamada de función no puede conducir a un código legible. Casi parece que tal vez estamos superando las posibilidades que nos brinda la programación de texto sin formato. Nos hemos movido de SOLO texto en casi todas las áreas, pero todavía codificamos lo mismo. ¿Se necesita más información para que se muestre en el código? Agrega más texto. De todos modos, esto se está volviendo un poco tangencial, así que me detendré aquí.

Una respuesta que obtuve al segundo fragmento de código es que probablemente primero desempaquetarás la matriz en algunas variables con nombre y luego las usarás, pero el nombre de la variable puede significar muchas cosas y la forma en que se llama no necesariamente te dice la forma en que se supone que debe hacerlo. ser interpretado en el contexto de la función llamada. En el ámbito local, es posible que tenga dos rectángulos llamados leftRectangle y rightRectangle porque eso es lo que representan semánticamente, pero no necesita extenderse a lo que representan cuando se asigna a una función.

De hecho, si sus variables se nombran en el contexto de la función llamada, entonces está introduciendo menos información de la que potencialmente podría con esa llamada de función y, en algún nivel, si conduce a un código peor. Si tiene un procedimiento que da como resultado un rectángulo que almacena en rectForClipping y luego otro procedimiento que proporciona rectForDrawing, entonces la llamada real a DrawRectangleClipped es solo una ceremonia. Una línea que no significa nada nuevo y está ahí solo para que la computadora sepa exactamente lo que quieres, aunque ya lo hayas explicado con tu nombre. Esto no es algo bueno.

Realmente me encantaría escuchar nuevas perspectivas sobre esto. Estoy seguro de que no soy el primero en considerar esto un problema, entonces, ¿cómo se resuelve?

Darwin
fuente
2
Estoy confundido acerca de cuál es el problema exacto ... parece que hay varias ideas aquí, no estoy seguro de cuál es su punto principal.
FrustratedWithFormsDesigner
1
La documentación de la función debe decirle qué hacen los argumentos. Puede objetar que alguien que lee el código puede no tener la documentación, pero realmente no sabe lo que hace el código y cualquier significado que extraiga de la lectura es una suposición educada. En cualquier contexto donde el lector necesite saber que el código es correcto, necesitará la documentación.
Doval
3
@Darwin En la programación funcional, todas las funciones solo tienen 1 argumento. Si necesita pasar "argumentos múltiples", el parámetro suele ser una tupla (si desea que se ordenen) o un registro (si no desea que lo sean). Además, es trivial formar versiones especializadas de funciones en cualquier momento, por lo que puede reducir la cantidad de argumentos necesarios. Dado que casi todos los lenguajes funcionales proporcionan sintaxis para tuplas y registros, agrupar valores es indoloro y obtienes la composición de forma gratuita (puedes encadenar funciones que devuelven tuplas con las que toman tuplas).
Doval
1
@Bergi La gente tiende a generalizar mucho más en FP pura, así que creo que las funciones en sí mismas suelen ser más pequeñas y numerosas. Aunque podría estar muy lejos. No tengo mucha experiencia trabajando en proyectos reales con Haskell y la pandilla.
Darwin
44
Creo que la respuesta es "¿No nombra sus variables 'deserializedArray'"?
cuál es

Respuestas:

10

Estoy de acuerdo en que la forma en que las funciones se usan a menudo puede ser una parte confusa de escribir código, y especialmente leer código.

La respuesta a este problema depende en parte del idioma. Como mencionó, C # ha nombrado parámetros. La solución de Objective-C a este problema involucra nombres de métodos más descriptivos. Por ejemplo, stringByReplacingOccurrencesOfString:withString:es un método con parámetros claros.

En Groovy, algunas funciones toman mapas, lo que permite una sintaxis como la siguiente:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

En general, puede resolver este problema limitando el número de parámetros que pasa a una función. Creo que 2-3 es un buen límite. Si parece que una función necesita más parámetros, me hace repensar el diseño. Pero, esto puede ser más difícil de responder en general. A veces intentas hacer demasiado en una función. A veces tiene sentido considerar una clase para almacenar sus parámetros. Además, en la práctica, a menudo encuentro que las funciones que toman una gran cantidad de parámetros normalmente tienen muchas de ellas como opcionales.

Incluso en un lenguaje como Objective-C tiene sentido limitar el número de parámetros. Una razón es que muchos parámetros son opcionales. Para ver un ejemplo, vea rangeOfString: y sus variaciones en NSString .

Un patrón que uso a menudo en Java es usar una clase de estilo fluido como parámetro. Por ejemplo:

something.draw(new Box().withHeight(5).withWidth(20))

Esto utiliza una clase como parámetro, y con una clase de estilo fluido, hace que el código sea fácil de leer.

El fragmento de Java anterior también ayuda cuando el orden de los parámetros puede no ser tan obvio. Normalmente asumimos con coordenadas que X viene antes que Y. Y normalmente veo la altura antes que el ancho como una convención, pero eso aún no está muy claro ( something.draw(5, 20)).

También he visto algunas funciones como, drawWithHeightAndWidth(5, 20)pero incluso estas no pueden tomar demasiados parámetros, o comenzaría a perder legibilidad.

David V
fuente
2
El orden, si continúa con el ejemplo de Java, puede ser muy complicado. Por ejemplo, compare los siguientes constructores de awt: Dimension(int width, int height)y GridLayout(int rows, int cols)(el número de filas es la altura, lo que significa que GridLayouttiene la altura primero y Dimensionel ancho).
Pierre Arlaud
1
Tales inconsistencias también han sido muy criticadas con PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), por ejemplo: array_filter($input, $callback)versus array_map($callback, $input), strpos($haystack, $needle)versusarray_search($needle, $haystack)
Pierre Arlaud
12

Principalmente se resuelve con un buen nombre de funciones, parámetros y argumentos. Sin embargo, ya exploró eso y descubrió que tenía deficiencias. La mayoría de esas deficiencias se mitigan manteniendo funciones pequeñas, con un pequeño número de parámetros, tanto en el contexto de llamada como en el contexto de llamada. Su ejemplo particular es problemático porque la función que está llamando está tratando de hacer varias cosas a la vez: especificar un rectángulo base, especificar una región de recorte, dibujarla y llenarla con un color específico.

Esto es como intentar escribir una oración usando solo los adjetivos. Ponga más verbos (llamadas a funciones) allí, cree un sujeto (objeto) para su oración, y es más fácil de leer:

rect.clip(clipRect).fill(color)

Incluso si clipRecty colortienen nombres terribles (y no debería), todavía se puede discernir sus tipos a partir del contexto.

Su ejemplo deserializado es problemático porque el contexto de llamada está tratando de hacer demasiado a la vez: deserializar y dibujar algo. Debe asignar nombres que tengan sentido y separen claramente las dos responsabilidades. Como mínimo, algo como esto:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Muchos problemas de legibilidad son causados ​​por tratar de ser demasiado conciso, omitiendo las etapas intermedias que los humanos requieren para discernir el contexto y la semántica.

Karl Bielefeldt
fuente
1
Me gusta la idea de encadenar múltiples llamadas de función para aclarar el significado, pero ¿no es eso simplemente bailar alrededor del tema? Básicamente es "Quiero escribir una oración, pero el lenguaje que estoy demandando no me deja, así que solo puedo usar el equivalente más cercano"
Darwin
@Darwin En mi humilde opinión, no es como si esto pudiera mejorarse haciendo que el lenguaje de programación sea más parecido al lenguaje natural. Los lenguajes naturales son muy ambiguos y solo podemos entenderlos en contexto y en realidad nunca podemos estar seguros. El encadenamiento de llamadas a funciones es mucho mejor ya que cada término (idealmente) tiene documentación y fuentes disponibles y tenemos paréntesis y puntos que aclaran la estructura.
maaartinus
3

En la práctica, se resuelve con un mejor diseño. Es excepcionalmente raro que las funciones bien escritas tomen más de 2 entradas, y cuando ocurre, es raro que esas muchas entradas no puedan agregarse en algún paquete cohesivo. Esto hace que sea bastante fácil dividir funciones o agregar parámetros para que no haga que una función haga demasiado. Una tiene dos entradas, se vuelve fácil de nombrar y mucho más claro acerca de qué entrada es cuál.

Mi lenguaje de juguete tenía el concepto de frases para lidiar con esto, y otros lenguajes de programación centrados en el lenguaje más natural han tenido otros enfoques para abordarlo, pero todos tienden a tener otras desventajas. Además, incluso las frases son poco más que una buena sintaxis para hacer que las funciones tengan mejores nombres. Siempre será difícil crear un buen nombre de función cuando se necesitan muchas entradas.

Telastyn
fuente
Las frases realmente parecen un paso adelante. Sé que algunos idiomas tienen capacidades similares, pero está lejos de ser generalizado. Sin mencionar que, con todo el odio macro proveniente de los puristas de C (++) que nunca usaron macros bien hechas, es posible que nunca tengamos características como estas en lenguajes populares.
Darwin
Bienvenido al tema general de los idiomas específicos de dominio , algo que realmente deseo que más personas entiendan la ventaja de ... (+1)
Izkata
2

En Javascript (o ECMAScript ), por ejemplo, muchos programadores se acostumbraron a

pasar parámetros como un conjunto de propiedades de objeto con nombre en un único objeto anónimo.

Y como práctica de programación, pasó de los programadores a sus bibliotecas y de allí a otros programadores a quienes les gustó y lo usaron y escribieron algunas bibliotecas más, etc.

Ejemplo

En lugar de llamar

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

Me gusta esto:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, que es un estilo válido y correcto, se llama

function drawRectangleClipped (params)

Me gusta esto:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, que es válido y correcto y agradable con respecto a su pregunta.

Por supuesto, tiene que haber condiciones adecuadas para esto: en Javascript, esto es mucho más viable que en, por ejemplo, C. En JavaScript, esto incluso dio origen a la notación estructural ahora ampliamente utilizada que se hizo popular como una contraparte más ligera de XML. Se llama JSON (puede que ya hayas oído hablar de él).

Pavel
fuente
No sé lo suficiente sobre ese idioma para verificar la sintaxis, pero, en general, me gusta esta publicación. Parece bastante elegante. +1
IT Alex
Muy a menudo, esto se combina con argumentos normales, es decir, hay 1-3 argumentos seguidos de params(a menudo que contienen argumentos opcionales y, a menudo, opcional) como, por ejemplo, esta función . Esto hace que las funciones con muchos argumentos sean bastante fáciles de entender (en mi ejemplo hay 2 argumentos obligatorios y 6 opciones).
maaartinus
0

Debería usar el objetivo C, entonces, aquí hay una definición de función:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

Y aquí se usa:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Creo que ruby ​​tiene construcciones similares y puedes simularlas en otros idiomas con listas de valores-clave.

Para funciones complejas en Java, me gusta definir variables ficticias en la redacción de las funciones. Para su ejemplo izquierda-derecha:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Parece más codificación, pero puede, por ejemplo, usar leftRectangle y luego refactorizar el código más adelante con "Extract local variable" si cree que no será comprensible para un futuro mantenedor del código, que podría ser o no ser usted.

asombro
fuente
Sobre ese ejemplo de Java, escribí en la pregunta por qué creo que no es una buena solución. ¿Qué piensas sobre eso?
Darwin
0

Mi enfoque es crear variables locales temporales, pero no solo llamarlas LeftRectangey RightRectangle. Más bien, uso nombres algo más largos para transmitir más significado. A menudo trato de diferenciar los nombres tanto como sea posible, por ejemplo, no llamarlos a ambos something_rectangle, si su rol no es muy simétrico.

Ejemplo (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

e incluso podría escribir una función o plantilla de envoltura de una línea:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(ignore los signos de unión si no conoce C ++).

Si la función hace varias cosas raras sin una buena descripción, ¡entonces probablemente sea necesario refactorizarla!

einpoklum
fuente