Considere a Functorclasse de tipo em Haskell, onde fé uma variável de tipo de tipo superior:
class Functor f where
fmap :: (a -> b) -> f a -> f b
O que essa assinatura de tipo diz é que fmap muda o parâmetro de tipo de fde apara b, mas deixa fcomo estava. Portanto, se você usar fmapsobre uma lista, obterá uma lista; se usá-la sobre um analisador, obterá um analisador, e assim por diante. E essas são garantias estáticas em tempo de compilação.
Não sei F #, mas vamos considerar o que acontece se tentarmos expressar a Functorabstração em uma linguagem como Java ou C #, com herança e genéricos, mas sem genéricos de tipo superior. Primeira tentativa:
interface Functor<A> {
Functor<B> map(Function<A, B> f);
}
O problema com essa primeira tentativa é que uma implementação da interface pode retornar qualquer classe que implemente Functor. Alguém poderia escrever um FunnyList<A> implements Functor<A>cujo mapmétodo retorne um tipo diferente de coleção, ou mesmo algo que não seja uma coleção, mas ainda seja um Functor. Além disso, ao usar o mapmétodo, você não pode invocar nenhum método específico de subtipo no resultado, a menos que seja reduzido ao tipo que você realmente espera. Portanto, temos dois problemas:
- O sistema de tipo não nos permite expressar a invariante de que o
mapmétodo sempre retorna a mesma Functorsubclasse que o receptor.
- Portanto, não há uma maneira estaticamente segura de invocar um
Functormétodo não relacionado ao resultado de map.
Existem outras maneiras mais complicadas que você pode tentar, mas nenhuma delas realmente funciona. Por exemplo, você pode tentar aumentar na primeira tentativa, definindo subtipos Functorque restringem o tipo de resultado:
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);
}
// …
Isso ajuda a impedir que os implementadores dessas interfaces mais estreitas retornem o tipo errado de Functordo mapmétodo, mas como não há limite para quantas Functorimplementações você pode ter, não há limite para quantas interfaces mais estreitas você precisará.
( EDITAR: E observe que isso só funciona porque Functor<B>aparece como o tipo de resultado e, portanto, as interfaces filhas podem restringi-lo. Portanto, não podemos restringir os dois usos de Monad<B>na seguinte interface: AFAIK :
interface Monad<A> {
<B> Monad<B> flatMap(Function<? super A, ? extends Monad<? extends B>> f);
}
Em Haskell, com variáveis do tipo de classificação superior, isso é (>>=) :: Monad m => m a -> (a -> m b) -> m b.)
Outra tentativa é usar genéricos recursivos para tentar fazer com que a interface restrinja o tipo de resultado do subtipo ao próprio subtipo. Exemplo de brinquedo:
/**
* A semigroup is a type with a binary associative operation. Law:
*
* > x.append(y).append(z) = x.append(y.append(z))
*/
interface Semigroup<T extends Semigroup<T>> {
T append(T arg);
}
class Foo implements Semigroup<Foo> {
// Since this implements Semigroup<Foo>, now this method must accept
// a Foo argument and return a Foo result.
Foo append(Foo arg);
}
class Bar implements Semigroup<Bar> {
// Any of these is a compilation error:
Semigroup<Bar> append(Semigroup<Bar> arg);
Semigroup<Foo> append(Bar arg);
Semigroup append(Bar arg);
Foo append(Bar arg);
}
Mas esse tipo de técnica (que é bastante misteriosa para o seu desenvolvedor OOP comum, mas para o seu desenvolvedor funcional comum também) ainda não consegue expressar a Functorrestrição desejada :
interface Functor<FA extends Functor<FA, A>, A> {
<FB extends Functor<FB, B>, B> FB map(Function<A, B> f);
}
O problema aqui é que isso não se restringe FBa ter o mesmo Fque FA- então, quando você declara um tipo List<A> implements Functor<List<A>, A>, o mapmétodo ainda pode retornar a NotAList<B> implements Functor<NotAList<B>, B>.
Tentativa final, em Java, usando tipos brutos (contêineres não parametrizados):
interface FunctorStrategy<F> {
F map(Function f, F arg);
}
Aqui Fserá instanciado para tipos não parametrizados como apenas Listou Map. Isso garante que a FunctorStrategy<List>só possa retornar a List- mas você abandonou o uso de variáveis de tipo para rastrear os tipos de elemento das listas.
O cerne do problema aqui é que linguagens como Java e C # não permitem que parâmetros de tipo tenham parâmetros. Em Java, se Tfor uma variável de tipo, você pode escrever Te List<T>, mas não T<String>. Os tipos de tipo superior removem essa restrição, de modo que você poderia ter algo assim (não totalmente pensado):
interface Functor<F, A> {
<B> F<B> map(Function<A, B> f);
}
class List<A> implements Functor<List, A> {
// Since F := List, F<B> := List<B>
<B> List<B> map(Function<A, B> f) {
// ...
}
}
E abordando este bit em particular:
(Eu acho) Eu entendo que em vez de myList |> List.map fou myList |> Seq.map f |> Seq.toListtipos de tipo superior permitem que você simplesmente escreva myList |> map fe ele retornará um List. Isso é ótimo (presumindo que esteja correto), mas parece meio mesquinho? (E isso não poderia ser feito simplesmente permitindo a sobrecarga de funções?) Normalmente, eu converto para de Seqqualquer maneira e, em seguida, posso converter para o que quiser.
Existem muitas linguagens que generalizam a ideia da mapfunção dessa forma, modelando-a como se, no fundo, o mapeamento fosse sobre sequências. Esta sua observação segue esse espírito: se você tem um tipo que suporta a conversão de e para Seq, você obtém a operação do mapa "gratuitamente" ao reutilizar Seq.map.
Em Haskell, entretanto, a Functorclasse é mais geral do que isso; não está vinculado à noção de sequências. Você pode implementar fmappara tipos que não têm um bom mapeamento para sequências, como IOações, combinadores de analisador, funções, etc .:
instance Functor IO where
fmap f action =
do x <- action
return (f x)
-- This declaration is just to make things easier to read for non-Haskellers
newtype Function a b = Function (a -> b)
instance Functor (Function a) where
fmap f (Function g) = Function (f . g) -- `.` is function composition
O conceito de "mapeamento" realmente não está vinculado a sequências. É melhor entender as leis do functor:
(1) fmap id xs == xs
(2) fmap f (fmap g xs) = fmap (f . g) xs
Muito informalmente:
- A primeira lei diz que mapear com uma função identidade / noop é o mesmo que não fazer nada.
- A segunda lei diz que qualquer resultado que você pode produzir mapeando duas vezes, você também pode produzir mapeando uma vez.
É por isso que você deseja fmappreservar o tipo - porque assim que você obtém mapoperações que produzem um tipo de resultado diferente, se torna muito, muito mais difícil fazer garantias como essa.