Tenho feito desenvolvimento em F # há um tempo e gosto disso. No entanto, um chavão que eu sei que não existe no F # é tipos de tipo superior. Eu li material sobre tipos de classe superior e acho que entendo sua definição. Só não sei por que eles são úteis. Alguém pode fornecer alguns exemplos de quais tipos de tipo superior facilitam em Scala ou Haskell, que requerem soluções alternativas em F #? Também para esses exemplos, quais seriam as soluções alternativas sem tipos de tipo superior (ou vice-versa em F #)? Talvez eu esteja tão acostumado a trabalhar com isso que não percebo a ausência desse recurso.
(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. Novamente, talvez eu esteja acostumado a trabalhar com isso. Mas há algum exemplo em que tipos de tipo superior realmente salvem você tanto no pressionamento de teclas quanto na segurança de digitação?
IMonad<T>
e então lançá-la de volta para o exemploIEnumerable<int>
ouIObservable<int>
quando terminar? Isso tudo é apenas para evitar o elenco?return
funcionaria, uma vez que isso realmente pertence ao tipo de mônada, não a uma instância particular, então você não gostaria de colocá-la naIMonad
interface.bind
akaSelectMany
etc. O que significa que alguém poderia usar a API parabind
umIObservable
para umIEnumerable
e presumir que funcionaria, o que sim, eca se for esse o caso e não há maneira de contornar isso. Só não tenho 100% de certeza de que não há maneira de contornar isso.Respostas:
Portanto, o tipo de um tipo é o seu tipo simples. Por exemplo,
Int
tem tipo, o*
que significa que é um tipo base e pode ser instanciado por valores. Por alguma definição vaga de tipo de tipo superior (e não tenho certeza de onde F # traça a linha, então vamos apenas incluí-lo) os contêineres polimórficos são um ótimo exemplo de tipo de tipo superior.O construtor de
List
tipo tem tipo, o* -> *
que significa que deve ser passado um tipo concreto para resultar em um tipo concreto:List Int
pode ter habitantes semelhantes,[1,2,3]
masList
ele próprio não pode.Vou supor que os benefícios dos recipientes polimórficos são óbvios, mas
* -> *
existem tipos de tipo mais úteis do que apenas os recipientes. Por exemplo, as relaçõesou analisadores
ambos também têm espécie
* -> *
.Podemos levar isso mais longe em Haskell, no entanto, tendo tipos com tipos de ordem ainda mais alta. Por exemplo, podemos procurar um tipo com tipo
(* -> *) -> *
. Um exemplo simples disso pode serShape
tentar encher um recipiente desse tipo* -> *
.Isso é útil para caracterizar
Traversable
s em Haskell, por exemplo, pois eles sempre podem ser divididos em sua forma e conteúdo.Como outro exemplo, vamos considerar uma árvore que está parametrizada no tipo de branch que possui. Por exemplo, uma árvore normal pode ser
Mas podemos ver que o tipo de branch contém a
Pair
deTree a
s e, portanto, podemos extrair essa parte do tipo parametricamenteEste
TreeG
construtor de tipo tem tipo(* -> *) -> * -> *
. Podemos usá-lo para fazer outras variações interessantes, como umRoseTree
Ou patológicos como um
MaybeTree
Ou um
TreeTree
Outro lugar onde isso aparece é em "álgebras de functores". Se eliminarmos algumas camadas de abstração, isso pode ser melhor considerado como uma dobra, como
sum :: [Int] -> Int
. As álgebras são parametrizadas no functor e na portadora . O functor tem tipo* -> *
e o tipo de portadora, de*
modo completotem tipo
(* -> *) -> * -> *
.Alg
útil por causa de sua relação com tipos de dados e esquemas de recursão construídos sobre eles.Finalmente, embora sejam teoricamente possíveis, nunca vi um construtor de tipo ainda mais alto. Às vezes vemos funções desse tipo, como
mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
, mas acho que você terá que cavar no prólogo de tipos ou na literatura com tipos dependentes para ver esse nível de complexidade nos tipos.fonte
TreeTree
é apenas patológico, mas de forma mais prática significa que você tem dois tipos diferentes de árvores entrelaçadas entre si - levar essa ideia um pouco mais longe pode fornecer algumas noções seguras de tipo muito poderosas, como vermelho estaticamente seguro / árvores negras e o tipo FingerTree estaticamente equilibrado.Considere a
Functor
classe de tipo em Haskell, ondef
é uma variável de tipo de tipo superior:O que essa assinatura de tipo diz é que fmap muda o parâmetro de tipo de
f
dea
parab
, mas deixaf
como estava. Portanto, se você usarfmap
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:O problema com essa primeira tentativa é que uma implementação da interface pode retornar qualquer classe que implemente
Functor
. Alguém poderia escrever umFunnyList<A> implements Functor<A>
cujomap
método retorne um tipo diferente de coleção, ou mesmo algo que não seja uma coleção, mas ainda seja umFunctor
. Além disso, ao usar omap
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:map
método sempre retorna a mesmaFunctor
subclasse que o receptor.Functor
método não relacionado ao resultado demap
.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:Isso ajuda a impedir que os implementadores dessas interfaces mais estreitas retornem o tipo errado de
Functor
domap
método, mas como não há limite para quantasFunctor
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 deMonad<B>
na seguinte interface: AFAIK :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:
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 :O problema aqui é que isso não se restringe
FB
a ter o mesmoF
queFA
- então, quando você declara um tipoList<A> implements Functor<List<A>, A>
, omap
método ainda pode retornar aNotAList<B> implements Functor<NotAList<B>, B>
.Tentativa final, em Java, usando tipos brutos (contêineres não parametrizados):
Aqui
F
será instanciado para tipos não parametrizados como apenasList
ouMap
. Isso garante que aFunctorStrategy<List>
só possa retornar aList
- 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 escreverT
eList<T>
, mas nãoT<String>
. Os tipos de tipo superior removem essa restrição, de modo que você poderia ter algo assim (não totalmente pensado):E abordando este bit em particular:
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 paraSeq
, você obtém a operação do mapa "gratuitamente" ao reutilizarSeq.map
.Em Haskell, entretanto, a
Functor
classe é mais geral do que isso; não está vinculado à noção de sequências. Você pode implementarfmap
para tipos que não têm um bom mapeamento para sequências, comoIO
ações, combinadores de analisador, funções, etc .:O conceito de "mapeamento" realmente não está vinculado a sequências. É melhor entender as leis do functor:
Muito informalmente:
É por isso que você deseja
fmap
preservar o tipo - porque assim que você obtémmap
operações que produzem um tipo de resultado diferente, se torna muito, muito mais difícil fazer garantias como essa.fonte
fmap
emFunction a
quando ele já tem uma.
operação? Eu entendo por que.
faz sentido ser a definição defmap
op, mas eu simplesmente não entendo onde você precisa usar emfmap
vez de.
. Talvez se você pudesse dar um exemplo onde isso seria útil, me ajudasse a entender.double
de um functor, ondedouble [1, 2, 3]
dá[2, 4, 6]
edouble sin
dá um fn que é o dobro do pecado. Eu posso ver onde se você começar a pensar nessa mentalidade, quando você executa um mapa em um array, você espera um array de volta, não apenas um seq, porque, bem, estamos trabalhando em arrays aqui.Functor
e deixar o cliente da biblioteca selecioná-lo. A resposta de J. Abrahamson fornece um exemplo: dobras recursivas podem ser generalizadas usando functores. Outro exemplo são as mônadas gratuitas; você pode pensar nisso como uma espécie de biblioteca de implementação de interpretador genérico, em que o cliente fornece o "conjunto de instruções" arbitrárioFunctor
.Functor
ou aSemiGroup
. Onde os programas reais mais usam esse recurso de linguagem?Não quero repetir informações em algumas respostas excelentes já aqui, mas há um ponto-chave que gostaria de acrescentar.
Você geralmente não precisa de tipos de tipo superior para implementar qualquer mônada ou functor em particular (ou functor aplicativo, ou seta, ou ...). Mas fazer isso é basicamente perder o ponto.
Em geral, descobri que quando as pessoas não veem a utilidade de functores / mônadas / sei lá o quê, geralmente é porque estão pensando nessas coisas uma de cada vez . As operações Functor / monad / etc realmente não adicionam nada a qualquer instância (em vez de chamar bind, fmap, etc, eu poderia apenas chamar quaisquer operações que usei para implementar bind, fmap, etc). O que você realmente quer essas abstrações é para que você possa ter um código que funcione genericamente com qualquer functor / monad / etc.
Em um contexto em que esse código genérico é amplamente usado, isso significa que sempre que você escreve uma nova instância de mônada, seu tipo imediatamente ganha acesso a um grande número de operações úteis que já foram escritas para você . Esse é o ponto de ver mônadas (e functores, e ...) em todos os lugares; não para que eu possa usar
bind
em vez deconcat
emap
para implementarmyFunkyListOperation
(o que me ganha nada em si), mas sim de modo que quando eu venho a necessidademyFunkyParserOperation
emyFunkyIOOperation
eu possa voltar a utilizar o código que originalmente viu em termos de listas, porque isso é realmente mônada-generic .Mas para abstrair em um tipo parametrizado como uma mônada com segurança de tipo , você precisa de tipos de tipo superior (como também explicado em outras respostas aqui).
fonte
Para uma perspectiva mais específica do .NET, escrevi uma postagem no blog sobre isso há algum tempo. O ponto crucial é que, com tipos de tipo superior, você poderia potencialmente reutilizar os mesmos blocos LINQ entre
IEnumerables
eIObservables
, mas sem tipos de tipo superior isso é impossível.O mais próximo que você poderia começar (eu descobri depois de postar o blog) é fazer o seu próprio
IEnumerable<T>
eIObservable<T>
e estendeu ambos de umIMonad<T>
. Isso permitiria que você reutilizasse seus blocos LINQ se eles estivessem denotadosIMonad<T>
, mas não é mais seguro de digitação porque permite que você misture e combineIObservables
eIEnumerables
dentro do mesmo bloco, que embora possa parecer intrigante habilitar isso, você basicamente obter algum comportamento indefinido.Escrevi um post posterior sobre como Haskell torna isso fácil. (Um no-op, na verdade - restringir um bloco a um certo tipo de mônada requer código; permitir a reutilização é o padrão).
fonte
IObservables
em código de produção.IObservable
, e você usa eventos no capítulo WinForms de seu próprio livro.O exemplo mais usado de polimorfismo de tipo superior em Haskell é a
Monad
interface.Functor
eApplicative
são de tipo superior da mesma maneira, então vou mostrarFunctor
para mostrar algo conciso.Agora, examine essa definição, observando como a variável de tipo
f
é usada. Você verá que issof
não pode significar um tipo que tenha valor. Você pode identificar valores nessa assinatura de tipo porque eles são argumentos e resultados de funções. Portanto, as variáveis de tipoa
eb
são tipos que podem ter valores. Assim como as expressões de tipof a
ef b
. Mas nãof
ela mesma.f
é um exemplo de uma variável de tipo de tipo superior. Dado que esse*
é o tipo de tipo que pode ter valores,f
deve ter o tipo* -> *
. Ou seja, é preciso um tipo que pode ter valores, porque sabemos de um exame anterior quea
eb
deve ter valores. E também sabemos dissof a
ef b
deve ter valores, portanto, retorna um tipo que deve ter valores.Isso faz com que o seja
f
usado na definição deFunctor
uma variável de tipo de tipo superior.As interfaces
Applicative
eMonad
adicionam mais, mas são compatíveis. Isso significa que eles funcionam em variáveis de tipo com kind* -> *
também.Trabalhar em tipos de tipo superior introduz um nível adicional de abstração - você não está restrito a apenas criar abstrações sobre tipos básicos. Você também pode criar abstrações sobre tipos que modificam outros tipos.
fonte