Considere a Functor
classe 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 f
de a
para b
, mas deixa f
como estava. Portanto, se você usar fmap
sobre 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 Functor
abstraçã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 map
mé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 map
mé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
map
método sempre retorna a mesma Functor
subclasse que o receptor.
- Portanto, não há uma maneira estaticamente segura de invocar um
Functor
mé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 Functor
que 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 Functor
do map
método, mas como não há limite para quantas Functor
implementaçõ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 Functor
restriçã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 FB
a ter o mesmo F
que FA
- então, quando você declara um tipo List<A> implements Functor<List<A>, A>
, o map
mé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 F
será instanciado para tipos não parametrizados como apenas List
ou 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 T
for uma variável de tipo, você pode escrever T
e 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 f
ou myList |> Seq.map f |> Seq.toList
tipos de tipo superior permitem que você simplesmente escreva myList |> map f
e 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 Seq
qualquer maneira e, em seguida, posso converter para o que quiser.
Existem muitas linguagens que generalizam a ideia da map
funçã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 Functor
classe é mais geral do que isso; não está vinculado à noção de sequências. Você pode implementar fmap
para tipos que não têm um bom mapeamento para sequências, como IO
açõ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 fmap
preservar o tipo - porque assim que você obtém map
operações que produzem um tipo de resultado diferente, se torna muito, muito mais difícil fazer garantias como essa.