¿Por qué no anotar parámetros de función?

28

Para responder a esta pregunta, supongamos que el costo de la ambigüedad en la mente de un programador es mucho más costoso que unas pocas pulsaciones de teclas adicionales.

Dado eso, ¿por qué permitiría que mis compañeros de equipo se salgan sin anotar sus parámetros de función? Tome el siguiente código como ejemplo de lo que podría ser un código mucho más complejo:

let foo x y = x + y

Ahora, un examen rápido de la información sobre herramientas le mostrará que F # ha determinado que usted quiere que x e y sean ints. Si eso es lo que pretendías, entonces todo está bien. Pero yo no sé si eso es lo que pretende. ¿Qué pasaría si hubiera creado este código para concatenar dos cadenas juntas? ¿O qué pasa si creo que probablemente quisiste agregar dobles? ¿O qué pasa si simplemente no quiero tener que pasar el mouse sobre cada parámetro de función para determinar su tipo?

Ahora tome esto como un ejemplo:

let foo x y = "result: " + x + y

F # ahora supone que probablemente haya tenido la intención de concatenar cadenas, por lo que x e y se definen como cadenas. Sin embargo, como el pobre imbécil que mantiene su código, podría mirar esto y preguntarme si tal vez tenía la intención de agregar x e y (ints) y luego agregar el resultado a una cadena para fines de UI.

Ciertamente, para ejemplos tan simples, uno podría dejarlo ir, pero ¿por qué no aplicar una política de anotación de tipo explícito?

let foo (x:string) (y:string) = "result: " + x + y

¿Qué daño hay en ser inequívoco? Claro, un programador podría elegir los tipos incorrectos para lo que está tratando de hacer, pero al menos sé que lo intentaron, que no era solo un descuido.

Esta es una pregunta seria ... Todavía soy muy nuevo en F # y estoy abriendo el camino para mi compañía. Los estándares que adopte probablemente serán la base para toda la futura codificación F #, incrustada en el interminable pegado de copias que estoy seguro impregnará la cultura en los años venideros.

Entonces ... ¿hay algo especial en la inferencia de tipos de F # que lo convierte en una característica valiosa para mantener, anotando solo cuando sea necesario? ¿O los expertos F # -ers tienen la costumbre de anotar sus parámetros para aplicaciones no triviales?

JDB
fuente
La versión anotada es menos general. Es por la misma razón que preferiblemente usa IEnumerable <> en las firmas y no SortedSet <>.
Patrick
2
@Patrick: no, no lo es, porque F # está infiriendo el tipo de cadena. No he cambiado la firma de la función, solo la hice explícita.
JDB
1
@Patrick - E incluso si fuera cierto, ¿puedes explicar por qué es algo malo? Quizás la firma debería ser menos general, si eso es lo que el programador pretendía. Quizás la firma más general en realidad está causando problemas, pero no estoy seguro de cuán específico pretendía ser el programador sin una gran cantidad de investigación.
JDB
1
Creo que es justo decir que, en lenguajes funcionales, prefieres generalidad y anotar en el caso más específico; por ejemplo, cuando desea un mejor rendimiento y puede obtenerlo mediante sugerencias de tipo. El uso de funciones anotadas en todas partes requeriría que escribas sobrecargas para cada tipo específico, lo que puede no estar garantizado en el caso general.
Robert Harvey

Respuestas:

30

No uso F #, pero en Haskell se considera buena forma de anotar (al menos) definiciones de nivel superior, y a veces definiciones locales, a pesar de que el lenguaje tiene una inferencia de tipo generalizada. Esto es por algunas razones:

  • Lectura
    Cuando desee saber cómo usar una función, es increíblemente útil tener disponible la firma de tipo. Simplemente puede leerlo, en lugar de tratar de inferirlo usted mismo o confiar en las herramientas para hacerlo por usted.

  • Refactorización
    Cuando desee alterar una función, tener una firma explícita le garantiza que sus transformaciones conservan la intención del código original. En un lenguaje de tipo inferido, es posible que el código altamente polimórfico teclee pero no haga lo que pretendías. La firma de tipo es una "barrera" que concreta la información de tipo en una interfaz.

  • Rendimiento
    En Haskell, el tipo inferido de una función puede sobrecargarse (por medio de las clases de tipos), lo que puede implicar un despacho en tiempo de ejecución. Para los tipos numéricos, el tipo predeterminado es un entero de precisión arbitraria. Si no necesita la generalidad completa de estas características, puede mejorar el rendimiento especializando la función al tipo específico que necesita.

Para las definiciones locales, las letvariables enlazadas y los parámetros formales para lambdas, encuentro que las firmas de tipo generalmente cuestan más en código que el valor que agregarían. Entonces, en la revisión del código, sugeriría que insista en las firmas para las definiciones de nivel superior y simplemente pida anotaciones juiciosas en otro lugar.

Jon Purdy
fuente
3
Esto suena como una respuesta muy razonable y bien equilibrada.
JDB
1
Estoy de acuerdo en que tener la definición con los tipos definidos explícitamente puede ayudar al refactorizar, pero creo que las pruebas unitarias también pueden resolver este problema y porque creo que la prueba unitaria debería usarse con la programación funcional para garantizar que una función funcione como está diseñada antes de usar el función con otra función, esto me permite dejar los tipos fuera de la definición y tener la confianza de que si hago un cambio importante se detectará. Ejemplo
Guy Coder, el
44
En Haskell se considera correcto anotar sus funciones, pero creo que F # y OCaml tienden a omitir anotaciones a menos que sea necesario para eliminar la ambigüedad. Eso no quiere decir que sea dañino (aunque la sintaxis para anotar en F # es más fea que en Haskell).
KChaloux
44
@KChaloux En OCaml, ya hay anotaciones de tipo en el .mliarchivo de interfaz ( ) (si escribe una, lo cual se recomienda encarecidamente. Las anotaciones de tipo tienden a omitirse de las definiciones porque serían redundantes con la interfaz.
Gilles ' Así que deja de ser malvado '
1
@Gilles @KChaloux, de hecho, lo mismo en los .fsiarchivos de firma F # ( ), con una advertencia: F # no usa archivos de firma para la inferencia de tipos, por lo que si algo en la implementación es ambiguo, aún tendrá que anotarlo nuevamente. books.google.ca/…
Yawar Amin
1

Jon dio una respuesta razonable que no repetiré aquí. Sin embargo, le mostraré una opción que podría satisfacer sus necesidades y en el proceso verá un tipo diferente de respuesta que no sea sí / no.

Últimamente he estado trabajando con el análisis sintáctico utilizando combinadores de analizador sintáctico. Si conoce el análisis, sabe que normalmente usa un lexer en la primera fase y un analizador en la segunda fase. El lexer convierte el texto en tokens y el analizador convierte los tokens en un AST. Ahora que F # es un lenguaje funcional y los combinadores están diseñados para combinarse, los combinadores de analizador están diseñados para utilizar las mismas funciones tanto en el lexer como en el analizador, pero si configura el tipo para las funciones del combinador de analizador, solo puede usarlos para lex o analizar y no ambos.

Por ejemplo:

/// Parser that requires a specific item.

// a (tok : 'a) : ('a list -> 'a * 'a list)                     // generic
// a (tok : string) : (string list -> string * string list)     // string
// a (tok : token)  : (token list  -> token  * token list)      // token

o

/// Parses iterated left-associated binary operator.


// leftbin (prs : 'a -> 'b * 'c) (sep : 'c -> 'd * 'a) (cons : 'd -> 'b -> 'b -> 'b) (err : string) : ('a -> 'b * 'c)                                                                                    // generic
// leftbin (prs : string list -> string * string list) (sep : string list -> string * string list) (cons : string -> string -> string -> string) (err : string) : (string list -> string * string list)  // string
// leftbin (prs : token list  -> token  * token list)  (sep : token list  -> token  * token list)  (cons : token  -> token  -> token  -> token)  (err : string) : (token list  -> token  * token list)   // token

Como el código es copyright, no lo incluiré aquí, pero está disponible en Github . No lo copie aquí.

Para que las funciones funcionen, deben dejarse con los parámetros genéricos, pero incluyo comentarios que muestran los tipos inferidos dependiendo del uso de las funciones. Esto facilita la comprensión de la función de mantenimiento y deja la función genérica para su uso.

Guy Coder
fuente
1
¿Por qué no escribirías la versión genérica en la función? ¿No funcionaría eso?
svick
@svick No entiendo tu pregunta. ¿Quiere decir que dejé los tipos fuera de la definición de la función, pero podría haber agregado los tipos genéricos a las definiciones de funciones ya que los tipos genéricos no cambiarían el significado de la función? Si es así, la razón es más una preferencia personal. Cuando comencé con F #, los agregué. Cuando comencé a trabajar con usuarios más avanzados de F #, prefirieron que se dejaran como comentarios porque era más fácil modificar el código sin las firmas allí. ...
Guy Coder
A la larga, la forma en que los muestro como comentarios funcionó para todos una vez que el código estaba funcionando. Cuando comienzo a escribir una función pongo los tipos. Una vez que tengo la función funcionando, muevo la firma de tipo a un comentario y elimino tantos tipos como sea posible. Algunas veces con F # debe dejar el tipo para ayudar a la inferencia. Durante un tiempo estuve experimentando con la creación del comentario con let y = para que puedas descomentar la línea para la prueba, luego comentarlo nuevamente cuando haya terminado, pero parecían tontos y agregar let y = no es tan difícil.
Guy Coder
Si estos comentarios responden a lo que está preguntando, hágamelo saber para que pueda pasarlos a la respuesta.
Guy Coder
2
Entonces, cuando modifica el código, ¿no modifica los comentarios, lo que lleva a documentación obsoleta? Eso no me parece una ventaja. Y realmente no veo cómo es mejor un comentario desordenado que un código desordenado. Para mí, parece que este enfoque combina las desventajas de las dos opciones.
svick
-8

¡El número de errores es directamente proporcional al número de caracteres en un programa!

Esto generalmente se indica como el número de errores que son proporcionales a las líneas de código. Pero tener menos líneas con la misma cantidad de código da la misma cantidad de errores.

Entonces, aunque es bueno ser explícito, cualquier parámetro adicional que escriba puede provocar errores.

James Anderson
fuente
55
“¡El número de errores es directamente proporcional al número de caracteres en un programa!” ¿Entonces todos deberíamos cambiar a APL ? Ser demasiado conciso es un problema, al igual que ser demasiado detallado.
svick
55
" Para responder a esta pregunta, supongamos que el costo de la ambigüedad en la mente de un programador es mucho más costoso que unas pocas pulsaciones de teclas adicionales " .
JDB