Il problema
Immagina di avere una funzione in un'API pubblica che deve restituire una specie di raccolta. Il suo tipo è qualcosa come
foo :: a -> b
dove b
è una sorta di collezione.
Per quanto posso vedere, ci sono tre principali tipi di collezioni - almeno quelle utilizzate più spesso:
- Sequenze (l'ordine conta, potrebbero esserci duplicati)
- Set (l'ordine non ha importanza, nessun duplicato)
- Mappe (gli elementi sono coppie chiave-valore, l'ordine non ha importanza, nessuna chiave duplicata)
Che cosa dovresti utilizzare per b
in questi tre casi ?. Ho preso in considerazione tre alternative, ma potrebbero essercene di più.
A. Basta restituire una lista
In questa alternativa, si restituiscono solo gli elenchi per tutto:
-
foo :: a -> [c]
-
foo :: a -> [c]
(e documenta che non ci sono duplicati) -
foo :: a -> [(k, c)]
(e documenta che non ci sono chiavi duplicate)
Il vantaggio di questo è che il client può costruire qualsiasi raccolta desiderata dall'elenco restituito.
Lo svantaggio per 2 e 3 è che
- il tipo non ha la garanzia "no duplicati" e il client deve fare più lavoro per ottenere un set / mappa da esso
- la costruzione della lista in 2 e 3 potrebbe essere meno efficiente, perché qualcosa come un
union
per insiemi è ovviamente più veloce di quella per le liste - potresti costruire una lista intermedia (dipende dal problema in questione).
B. Restituisce un tipo concreto che corrisponde alle garanzie
In questa alternativa, si restituisce un tipo che corrisponde esattamente alle garanzie che si stanno dando:
-
foo :: a -> [c]
(o forsefoo :: a -> Data.Sequence.Seq c
) -
foo :: a -> Data.Set.Set c
(pigro o severo?) -
foo :: a -> Data.Map.Map k c
(pigro o severo?)
Il vantaggio è che il tipo descrive accuratamente le garanzie che vuoi esprimere e che il cliente non deve fare alcun lavoro extra.
Lo svantaggio è che sei bloccato in una particolare implementazione. Se si desidera passare a un'implementazione di un set diverso, si interrompe l'API pubblica. Potrebbe essere una buona cosa in caso di passaggio tra pigro e severo, perché questi possono avere importanza per la correttezza del codice chiamante. Ma in generale vuoi essere in grado di scambiare l'implementazione senza che i tuoi clienti se ne accorgano.
C. Lascia che il cliente decida
In questa alternativa, devi solo limitare il tipo restituito con una classe di tipo e lasciare che il client decida sul tipo concreto:
1. 'foo :: Sequence b c => a -> b'
2. 'foo :: Set b c => a -> b'
3. 'foo :: Map b c k => a -> b'
(I nomi che ho usato provengono da collections-api , ma non importa.)
Il vantaggio di questo è che il client può scegliere il tipo di raccolta da utilizzare.
Lo svantaggio è che non hai più il controllo sulla collezione che costruisci più. Ciò significa, ad esempio, che non puoi assumere la pigrizia o la complessità degli inserti. Un altro problema è che il client può ottenere tipi ambigui, ad es. Quando piegano immediatamente la raccolta restituita:
Data.Foldable.fold (foo whatever)
Ora b
è ambiguo.
Ci sono altre alternative? Cosa consiglieresti?
I non vuole foo
per poter restituire un tipo diverso di raccolta per ogni chiamata.
I tre casi considerati sono completamente separati.
In ogni caso la domanda è "se ho una funzione che deve restituire una X, che tipo dovrebbe avere?" dove X può essere una sequenza, un set o una mappa (o qualsiasi altro tipo di collezione a cui puoi pensare)