Considera la Functor
classe 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 f
da a
a b
, ma rimane f
così com'era. Quindi, se usi fmap
su 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' Functor
astrazione 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 map
metodo 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 map
metodo 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
map
metodo restituisce sempre la stessa Functor
sottoclasse del ricevitore.
- Pertanto, non esiste un modo staticamente indipendente dai tipi per invocare un non
Functor
metodo 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 Functor
che 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 Functor
dal map
metodo, ma poiché non c'è limite al numero di Functor
implementazioni 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 Functor
vincolo 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 FB
ad avere lo stesso F
di FA
— in modo che quando dichiari un tipo List<A> implements Functor<List<A>, A>
, il map
metodo 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 F
verrà creata un'istanza per tipi non parametrizzati come just List
or 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 T
e 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 f
o myList |> Seq.map f |> Seq.toList
superiore ti permetta di scrivere semplicemente myList |> map f
e 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 Seq
comunque e poi posso convertire in quello che voglio in seguito.
Esistono molti linguaggi che generalizzano l'idea della map
funzione 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 Functor
classe è più generale di così; non è legato alla nozione di sequenze. Puoi implementarlo fmap
per i tipi che non hanno una buona mappatura con sequenze, come IO
azioni, 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 fmap
preservare il tipo: non appena si ottengono map
operazioni che producono un tipo di risultato diverso, diventa molto, molto più difficile fornire garanzie come questa.