Considera la Functorclasse di tipo in Haskell, dove fè una variabile di tipo di livello superiore:
class Functor f where
fmap :: (a -> b) -> f a -> f b
Ciò che questa firma di tipo dice è che fmap cambia il parametro di tipo di un fda aa b, ma rimane fcosì com'era. Quindi, se usi fmapsu un elenco ottieni un elenco, se lo usi su un parser ottieni un parser e così via. E queste sono garanzie statiche in fase di compilazione.
Non conosco F #, ma consideriamo cosa succede se proviamo a esprimere l' Functorastrazione in un linguaggio come Java o C #, con ereditarietà e generici, ma senza generici di tipo superiore. Primo tentativo:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
Il problema con questo primo tentativo è che un'implementazione dell'interfaccia può restituire qualsiasi classe che implementa Functor. Qualcuno potrebbe scrivere un il FunnyList<A> implements Functor<A>cui mapmetodo restituisce un diverso tipo di raccolta, o anche qualcos'altro che non è affatto una raccolta ma è comunque un file Functor. Inoltre, quando si utilizza il mapmetodo non è possibile richiamare alcun metodo specifico del sottotipo sul risultato a meno che non lo si abbassi al tipo che ci si aspetta effettivamente. Quindi abbiamo due problemi:
- Il sistema dei tipi non ci permette di esprimere l'invariante secondo cui il
mapmetodo restituisce sempre la stessa Functorsottoclasse del ricevitore.
- Pertanto, non esiste un modo staticamente indipendente dai tipi per invocare un non
Functormetodo sul risultato di map.
Ci sono altri modi più complicati che puoi provare, ma nessuno di loro funziona davvero. Ad esempio, potresti provare ad aumentare il primo tentativo definendo dei sottotipi Functorche limitano il tipo di risultato:
interface Collection<A> extends Functor<A> {
Collection<B> map(Function<A, B> f);
}
interface List<A> extends Collection<A> {
List<B> map(Function<A, B> f);
}
interface Set<A> extends Collection<A> {
Set<B> map(Function<A, B> f);
}
interface Parser<A> extends Functor<A> {
Parser<B> map(Function<A, B> f);
}
Questo aiuta a impedire agli implementatori di quelle interfacce più strette di restituire il tipo sbagliato di Functordal mapmetodo, ma poiché non c'è limite al numero di Functorimplementazioni che puoi avere, non c'è limite al numero di interfacce più strette di cui avrai bisogno.
( EDIT: E nota che questo funziona solo perché Functor<B>appare come il tipo di risultato, e quindi le interfacce figlio possono restringerlo. Quindi, AFAIK non possiamo restringere entrambi gli usi Monad<B>nella seguente interfaccia:
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
In Haskell, con variabili di tipo di rango superiore, questo è (>>=) :: Monad m => m a -> (a -> m b) -> m b.)
Un altro tentativo consiste nell'usare generici ricorsivi per cercare di fare in modo che l'interfaccia restringa il tipo di risultato del sottotipo al sottotipo stesso. Esempio di giocattolo:
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Ma questo tipo di tecnica (che è piuttosto arcana per il tuo sviluppatore OOP run-of-the-mill, diamine anche per il tuo sviluppatore funzionale run-of-the-mill) non può ancora esprimere il Functorvincolo desiderato :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
Il problema qui è che questo non si limita FBad avere lo stesso Fdi FA— in modo che quando dichiari un tipo List<A> implements Functor<List<A>, A>, il mapmetodo può comunque restituire un NotAList<B> implements Functor<NotAList<B>, B>.
Prova finale, in Java, utilizzando tipi grezzi (contenitori non parametrizzati):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Qui Fverrà creata un'istanza per tipi non parametrizzati come just Listor Map. Ciò garantisce che a FunctorStrategy<List>può restituire solo un List— ma hai abbandonato l'uso delle variabili di tipo per tenere traccia dei tipi di elementi degli elenchi.
Il cuore del problema qui è che linguaggi come Java e C # non consentono ai parametri di tipo di avere parametri. In Java, se Tè una variabile di tipo, puoi scrivere Te List<T>, ma non T<String>. I tipi di livello superiore rimuovono questa restrizione, in modo che tu possa avere qualcosa di simile (non completamente pensato):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
<B> List<B> map(Function<A, B> f) {
}
}
E affrontando questo bit in particolare:
(Penso) Capisco che al posto di tipi di tipo superiore myList |> List.map fo myList |> Seq.map f |> Seq.toListsuperiore ti permetta di scrivere semplicemente myList |> map fe restituirà un List. È fantastico (supponendo che sia corretto), ma sembra un po 'meschino? (E non potrebbe essere fatto semplicemente consentendo il sovraccarico delle funzioni?) Di solito converto in Seqcomunque e poi posso convertire in quello che voglio in seguito.
Esistono molti linguaggi che generalizzano l'idea della mapfunzione in questo modo, modellandola come se, in fondo, la mappatura riguardasse le sequenze. Questa tua osservazione è in questo spirito: se hai un tipo che supporta la conversione da e verso Seq, ottieni l'operazione mappa "gratuitamente" riutilizzandola Seq.map.
In Haskell, tuttavia, la Functorclasse è più generale di così; non è legato alla nozione di sequenze. Puoi implementarlo fmapper i tipi che non hanno una buona mappatura con sequenze, come IOazioni, combinatori di parser, funzioni, ecc .:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g)
Il concetto di "mappatura" in realtà non è legato alle sequenze. È meglio capire le leggi del funtore:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
In modo molto informale:
- La prima legge dice che mappare con una funzione identity / noop è lo stesso che non fare nulla.
- La seconda legge dice che qualsiasi risultato che puoi produrre mappando due volte, puoi anche produrre mappando una volta.
Questo è il motivo per cui si desidera fmappreservare il tipo: non appena si ottengono mapoperazioni che producono un tipo di risultato diverso, diventa molto, molto più difficile fornire garanzie come questa.