Perché non annotare i parametri delle funzioni?


28

Per rendere rispondibile a questa domanda, supponiamo che il costo dell'ambiguità nella mente di un programmatore sia molto più costoso di alcuni tasti in più.

Detto questo, perché dovrei permettere ai miei compagni di squadra di evitare di annotare i loro parametri di funzione? Prendi il seguente codice come esempio di quello che potrebbe essere un pezzo di codice molto più complesso:

let foo x y = x + y

Ora, un rapido esame del suggerimento ti mostrerà che F # ha determinato che intendevi che xey fossero ints. Se è quello che intendevi, allora va tutto bene. Ma io non so se è quello desiderato. E se avessi creato questo codice per concatenare due stringhe insieme? O se penso che probabilmente intendevi aggiungere un doppio? O se non volessi semplicemente passare il mouse su ogni singolo parametro di funzione per determinarne il tipo?

Ora prendi questo come esempio:

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

F # ora presuppone che tu abbia probabilmente intenzione di concatenare le stringhe, quindi xey sono definite stringhe. Tuttavia, come il povero schmuck che sta mantenendo il tuo codice, potrei guardare questo e chiedermi se forse avevi intenzione di aggiungere insieme xey (ints) e quindi aggiungere il risultato a una stringa per scopi di interfaccia utente.

Certamente per esempi così semplici si potrebbe lasciar perdere, ma perché non applicare una politica di annotazione esplicita del tipo?

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

Che male c'è nell'essere inequivocabile? Certo, un programmatore potrebbe scegliere i tipi sbagliati per quello che stanno cercando di fare, ma almeno so che lo intendevano, che non era solo una svista.

Questa è una domanda seria ... Sono ancora molto nuovo con F # e sto aprendo la strada per la mia compagnia. Gli standard che adotterò saranno probabilmente la base di tutti i futuri codici F #, integrati nell'infinito copia-incolla che sono sicuro che permetteranno alla cultura per gli anni a venire.

Quindi ... c'è qualcosa di speciale nell'inferenza del tipo di F # che la rende una caratteristica preziosa a cui aggrapparsi, annotando solo quando necessario? O i F # esperti hanno l'abitudine di annotare i loro parametri per applicazioni non banali?


La versione annotata è meno generale. È per lo stesso motivo per cui preferibilmente usi IEnumerable <> nelle firme e non SortedSet <>.
Patrick,

2
@Patrick - No, non lo è, perché F # sta inferendo il tipo di stringa. Non ho cambiato la firma della funzione, l'ho solo resa esplicita.
JDB,

1
@Patrick - E anche se fosse vero, puoi spiegare perché è una brutta cosa? Forse la firma dovrebbe essere meno generale, se questo era ciò che il programmatore aveva inteso. Forse la firma più generale sta effettivamente causando problemi, ma non sono sicuro di quanto specifico intendesse essere il programmatore senza una grande quantità di ricerche.
JDB,

1
Penso che sia giusto dire che, nei linguaggi funzionali, preferisci la generalità e annoti nel caso più specifico; ad es. quando vuoi prestazioni migliori e puoi ottenerlo usando il suggerimento sul tipo. L'uso di funzioni con annotazioni ovunque richiederebbe di scrivere sovraccarichi per ogni tipo specifico, il che potrebbe non essere garantito nel caso generale.
Robert Harvey,

Risposte:


30

Non uso F #, ma in Haskell è considerato una buona forma per annotare (almeno) le definizioni di livello superiore e talvolta le definizioni locali, anche se il linguaggio ha un'inferenza di tipo pervasiva. Questo per alcuni motivi:

  • Lettura
    Quando vuoi sapere come usare una funzione, è incredibilmente utile avere la firma del tipo disponibile. Puoi semplicemente leggerlo, piuttosto che cercare di inferirlo da solo o fare affidamento su strumenti per farlo per te.

  • Refactoring
    Quando vuoi modificare una funzione, avere una firma esplicita ti dà la certezza che le tue trasformazioni mantengono l'intento del codice originale. In un linguaggio dedotto dal tipo, potresti scoprire che un codice altamente polimorfico controllerà i caratteri ma non farà ciò che volevi. La firma del tipo è una "barriera" che concretizza le informazioni sul tipo in un'interfaccia.

  • Prestazioni
    In Haskell, il tipo inferito di una funzione può essere sovraccaricato (per mezzo di typeclass), il che può comportare un invio di runtime. Per i tipi numerici, il tipo predefinito è un numero intero di precisione arbitraria. Se non hai bisogno della piena generalità di queste funzionalità, puoi migliorare le prestazioni specializzando la funzione sul tipo specifico di cui hai bisogno.

Per le definizioni locali, letle variabili -bound e i parametri formali per lambdas, trovo che le firme dei tipi di solito costano di più nel codice rispetto al valore che aggiungerebbero. Quindi, nella revisione del codice, suggerirei di insistere sulle firme per le definizioni di alto livello e semplicemente di chiedere annotazioni giudiziose altrove.


3
Sembra una risposta molto ragionevole ed equilibrata.
JDB il

1
Concordo sul fatto che avere la definizione con i tipi esplicitamente definiti possa aiutare durante il refactoring, ma trovo che anche i test unitari possano risolvere questo problema e poiché ritengo che il test unitario debba essere usato con la programmazione funzionale per garantire che una funzione funzioni come previsto prima di utilizzare il funzione con un'altra funzione, questo mi permette di lasciare i tipi fuori dalla definizione e avere la certezza che se faccio una modifica di rottura verrà catturato. Esempio
Guy Coder,

4
In Haskell è considerato corretto annotare le tue funzioni, ma credo che F # idiomatico e OCaml tendano a omettere le annotazioni a meno che non sia necessario per rimuovere l'ambiguità. Ciò non significa che sia dannoso (sebbene la sintassi per l'annotazione in F # sia più brutta che in Haskell).
KChaloux,

4
@KChaloux In OCaml, ci sono già le annotazioni di tipo nel .mlifile interface ( ) (se ne scrivi una, a cui sei fortemente incoraggiato. Le annotazioni di tipo tendono ad essere omesse dalle definizioni perché sarebbero ridondanti con l'interfaccia.
Gilles ' Quindi, smettila di essere malvagio, il

1
@Gilles @KChaloux infatti, lo stesso nei .fsifile di firma F # ( ), con un avvertimento: F # non usa i file di firma per l'inferenza di tipo, quindi se qualcosa nell'implementazione è ambiguo, dovrai comunque annotarlo di nuovo. books.google.ca/…
Yawar Amin,

1

Jon ha dato una risposta ragionevole che non ripeterò qui. Ti mostrerò comunque un'opzione che potrebbe soddisfare le tue esigenze e nel processo vedrai un diverso tipo di risposta diverso da un sì / no.

Ultimamente ho lavorato con l'analisi usando combinatori di parser. Se conosci l'analisi, sai che in genere usi un lexer nella prima fase e un parser nella seconda fase. Il lexer converte il testo in token e il parser converte i token in un AST. Ora che F # è un linguaggio funzionale e i combinatori progettati per essere combinati, i combinatori parser sono progettati per utilizzare le stesse funzioni sia nel lexer che nel parser, ma se si imposta il tipo per le funzioni del combinatore parser, è possibile utilizzarli solo per lex o per analizzare e non entrambi.

Per esempio:

/// 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

Poiché il codice è protetto da copyright, non lo includerò qui, ma è disponibile su Github . Non copiarlo qui.

Affinché le funzioni funzionino, devono essere lasciati con i parametri generici, ma includo commenti che mostrano i tipi dedotti a seconda dell'uso delle funzioni. Ciò semplifica la comprensione della funzione per la manutenzione lasciando la funzione generica per l'uso.


1
Perché non dovresti scrivere la versione generica nella funzione? Non funzionerebbe?
svick,

@svick Non capisco la tua domanda. Vuoi dire che ho lasciato i tipi fuori dalla definizione della funzione, ma avrei potuto aggiungere i tipi generici alle definizioni delle funzioni poiché i tipi generici non avrebbero cambiato il significato della funzione? In tal caso la ragione è più di una preferenza personale. Quando ho iniziato con F #, li ho aggiunti. Quando ho iniziato a lavorare con utenti più avanzati di F #, hanno preferito che fossero lasciati come commenti perché era più semplice modificare il codice senza le firme lì. ...
Guy Coder,

A lungo termine il modo in cui li mostro mentre i commenti funzionavano per tutti una volta che il codice funzionava. Quando inizio a scrivere una funzione inserisco i tipi. Una volta che la funzione è attiva, sposto la firma del tipo in un commento e rimuovo il maggior numero possibile di tipi. Alcune volte con F # è necessario lasciare il tipo per aiutare a dedurre. Per un po 'stavo sperimentando la creazione del commento con let e = in modo da poter decommentare la riga per il test, quindi commentarlo di nuovo quando fatto, ma sembravano sciocchi e aggiungendo un let e = non è così difficile.
Guy Coder,

Se questi commenti rispondono a ciò che stai chiedendo fammi sapere in modo che io possa spostarli nella risposta.
Guy Coder,

2
Quindi, quando si modifica il codice, non si modificano i commenti, portando a documentazione obsoleta? Non mi sembra un vantaggio. E non vedo davvero come il commento disordinato sia meglio del codice disordinato. A me sembra che questo approccio combini gli svantaggi delle due opzioni.
svick,

-8

Il numero di bug è direttamente proporzionale al numero di caratteri in un programma!

Questo di solito è indicato come il numero di bug è proporzionale alle righe di codice. Ma avere meno righe con la stessa quantità di codice fornisce la stessa quantità di bug.

Quindi, sebbene sia bello essere espliciti, qualsiasi parametro aggiuntivo che digiti può portare a bug.


5
"Il numero di bug è direttamente proporzionale al numero di caratteri in un programma!" Quindi dovremmo passare tutti all'APL ? Essere troppo concisi è un problema, proprio come essere troppo prolisso.
svick,

5
" Per rendere questa domanda rispondibile, supponiamo che il costo dell'ambiguità nella mente di un programmatore sia molto più costoso di qualche
sequenza di
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.