Perché ci sono così tante funzioni `map` per diversi tipi in F #


9

Sto imparando F #. Ho iniziato FP con Haskell e sono curioso di questo.

Dato che F # è un linguaggio .NET, mi sembra più ragionevole dichiarare un'interfaccia simile Mappable, proprio come la Functorclasse di tipo haskell .

inserisci qui la descrizione dell'immagine

Ma come nella figura sopra, le funzioni F # sono separate e implementate da sole. Qual è lo scopo progettuale di tale design? Per me, introdurre Mappable.mape implementare questo per ciascun tipo di dati sarebbe più conveniente.


Questa domanda non appartiene a SO. Non è un problema di programmazione. Ti suggerisco di chiedere in F # Slack o in qualche altro forum di discussione.
Bent Tranberg,

5
@BentTranberg Lettura generosa, The community is here to help you with specific coding, algorithm, or language problems.comprenderebbe anche domande sulla progettazione del linguaggio, purché siano soddisfatti gli altri criteri.
kaefer

3
Per farla breve, F # non ha classi di tipi e pertanto deve reimplementare mape altre funzioni di ordine superiore comuni per ciascun tipo di raccolta. Un'interfaccia potrebbe aiutare poco poiché richiede ancora che ogni tipo di raccolta fornisca un'implementazione separata.
dumetrulo

Risposte:


19

Sì, una domanda molto semplice in superficie. Ma se prendi il tempo per pensarci fino in fondo, ti immergi nelle profondità della teoria dei tipi. E la teoria dei tipi ti fissa anche.

Innanzitutto, ovviamente, hai già correttamente capito che F # non ha classi di tipi, ed è per questo. Ma tu proponi un'interfaccia Mappable. Ok, diamo un'occhiata a quello.

Diciamo che possiamo dichiarare tale interfaccia. Riesci a immaginare come sarebbe la sua firma?

type Mappable =
    abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>

Dov'è fil tipo che implementa l'interfaccia. Oh aspetta! F # non ha nemmeno quello! Ecco funa variabile di tipo di tipo superiore e F # non ha affatto una gentilezza superiore. Non c'è modo di dichiarare una funzione f : 'm<'a> -> 'm<'b>o qualcosa del genere.

Ma ok, supponiamo di aver superato anche questo ostacolo. E ora abbiamo un'interfaccia Mappableche può essere implementato da List, Array, Seq, e il lavello della cucina. Ma aspetta! Ora abbiamo un metodo anziché una funzione e i metodi non compongono bene! Diamo un'occhiata all'aggiunta di 42 ad ogni elemento di un elenco nidificato:

// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))

// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))

Guarda: ora dobbiamo usare un'espressione lambda! Non è possibile passare questa .mapimplementazione a un'altra funzione come valore. In effetti la fine delle "funzioni come valori" (e sì, lo so, usare un lambda non sembra molto male in questo esempio, ma credetemi, diventa molto brutto)

Ma aspetta, non abbiamo ancora finito. Ora che è una chiamata di metodo, l'inferenza di tipo non funziona! Poiché la firma del tipo di un metodo .NET dipende dal tipo di oggetto, non è possibile dedurre entrambi dal compilatore. Questo è in realtà un problema molto comune riscontrato dai neofiti quando interagiscono con le librerie .NET. E l'unica cura è fornire una firma di tipo:

add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))

Oh, ma questo non è ancora abbastanza! Anche se ho fornito una firma per nestedListse stesso, non ho fornito una firma per il parametro lambda l. Quale dovrebbe essere tale firma? Diresti che dovrebbe essere fun (l: #Mappable) -> ...? Oh, e ora siamo finalmente riusciti a classificare i tipi N, come vedi, #Mappableè una scorciatoia per "qualsiasi tipo 'atale 'a :> Mappable" - cioè un'espressione lambda che è essa stessa generica.

Oppure, in alternativa, potremmo tornare alla gentilezza superiore e dichiarare il tipo di nestedListpiù precisamente:

add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...

Ma ok, mettiamo da parte l'inferenza di tipo per ora e torniamo all'espressione lambda e come ora non possiamo passare mapcome valore a un'altra funzione. Diciamo che estendiamo un po 'la sintassi per consentire qualcosa di simile a ciò che Elm fa con i campi record:

add42 nestedList = nestedList.map (.map ((+) 42))

Quale sarebbe il tipo di .mapessere? Dovrebbe essere un tipo vincolato , proprio come in Haskell!

.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>

Wow, ok. Mettendo da parte il fatto che .NET non consente nemmeno l'esistenza di tali tipi, in effetti siamo appena tornati alle classi di tipi!

Ma c'è una ragione per cui F # non ha classi di tipi in primo luogo. Molti aspetti di quel motivo sono descritti sopra, ma un modo più conciso per dirlo è: semplicità .

Come vedi, questo è un gomitolo di lana. Una volta che hai classi di tipo, devi avere vincoli, gentilezza superiore, rango N (o almeno rango 2) e prima di conoscerlo, stai chiedendo tipi impredicativi, funzioni di tipo, GADT e tutti i il resto.

Ma Haskell paga un prezzo per tutte le chicche. Si scopre che non esiste un buon modo per dedurre tutta quella roba. I tipi di tipo superiore funzionano, ma i vincoli già non lo fanno. Rango-N: non ci sognare nemmeno. E anche quando funziona, ricevi errori di tipo che devi avere un dottorato di ricerca per capire. Ed è per questo che in Haskell sei delicatamente incoraggiato a mettere firme di tipo su tutto. Bene, non tutto , tutto , ma quasi tutto. E dove non metti le firme dei tipi (ad esempio dentro lete where) - sorpresa-sorpresa, quei posti sono in realtà monomorfizzati, quindi sei essenzialmente tornato nella semplicistica terra F #.

In F #, d'altra parte, le firme dei tipi sono rare, principalmente solo per la documentazione o per l'interoperabilità di .NET. Al di fuori di questi due casi, puoi scrivere un programma molto complesso in F # e non usare una firma di tipo una volta. L'inferenza del tipo funziona bene, perché non c'è nulla di troppo complesso o ambiguo da gestire.

E questo è il grande vantaggio di F # rispetto a Haskell. Sì, Haskell ti consente di esprimere cose super complesse in modo molto preciso, va bene. Ma F # ti permette di essere molto appetitoso, quasi come Python o Ruby, e avere ancora il compilatore che ti cattura se inciampi.

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.