Por que existem tantas funções `map` para diferentes tipos em F #

9

Estou aprendendo F #. Comecei o FP com Haskell e fiquei curioso por isso.

Como o F # é a linguagem .NET, parece-me mais razoável declarar a interface como Mappable, assim como a Functorclasse do tipo haskell .

insira a descrição da imagem aqui

Mas, como na figura acima, as funções F # são separadas e implementadas por si só. Qual é o objetivo do projeto? Para mim, introduzir Mappable.mape implementar isso para cada tipo de dados seria mais conveniente.

MyBug18
fonte
Esta pergunta não pertence ao SO. Não é um problema de programação. Sugiro que você pergunte no F # Slack ou em algum outro fórum de discussão.
Bent Tranberg
5
A leitura generosa do @BentTranberg The community is here to help you with specific coding, algorithm, or language problems.também abrangeria questões de design de linguagem, desde que os outros critérios fossem atendidos.
kaefer 8/04
3
Para encurtar a história, F # não possui classes de tipo e, portanto, precisa reimplementar mape outras funções comuns de ordem superior para cada tipo de coleção. Uma interface ajudaria pouco, pois ainda requer que cada tipo de coleção forneça uma implementação separada.
dumetrulo 8/04

Respostas:

19

Sim, uma pergunta muito direta na superfície. Mas se você dedicar um tempo para refletir até o fim, entrará nas profundezas da teoria dos tipos incomensurável. E a teoria dos tipos também olha para você.

Primeiro, é claro, você já descobriu corretamente que o F # não tem classes de tipo, e é por isso. Mas você propõe uma interface Mappable. Ok, vamos analisar isso.

Digamos que podemos declarar tal interface. Você pode imaginar como seria a assinatura?

type Mappable =
    abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>

Onde f é o tipo que implementa a interface. Oh espere! F # também não tem isso! Aqui festá uma variável do tipo com classificação mais alta e o F # não possui classificação mais alta. Não há como declarar uma função f : 'm<'a> -> 'm<'b>ou algo parecido.

Mas tudo bem, digamos que superamos esse obstáculo também. E agora temos uma interface Mappableque pode ser implementada por List, Array, Seqe da pia da cozinha. Mas espere! Agora, temos um método em vez de uma função, e os métodos não compõem bem! Vamos dar uma olhada em adicionar 42 a cada elemento de uma lista aninhada:

// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))

// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))

Olha: agora temos que usar uma expressão lambda! Não há como passar por isso.map implementação para outra função como um valor. Efetivamente, o final de "funciona como valores" (e sim, eu sei, usar um lambda não parece muito ruim neste exemplo, mas confie em mim, fica muito feio)

Mas espere, ainda não terminamos. Agora que é uma chamada de método, a inferência de tipo não funciona! Como a assinatura de tipo de um método .NET depende do tipo do objeto, não há como o compilador inferir os dois. Na verdade, esse é um problema muito comum para iniciantes quando interoperam com bibliotecas .NET. E a única solução é fornecer uma assinatura de tipo:

add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))

Ah, mas isso ainda não é suficiente! Mesmo que eu tenha fornecido uma assinatura para nestedListsi mesmo, não forneci uma assinatura para o parâmetro do lambda l. Qual deve ser essa assinatura? Você diria que deveria ser fun (l: #Mappable) -> ...? Ah, e agora finalmente conseguimos classificar os tipos N, como você vê, #Mappableé um atalho para "qualquer tipo 'aque 'a :> Mappable" - ou seja, uma expressão lambda que é genérica.

Ou, alternativamente, poderíamos voltar à bondade superior e declarar o tipo de nestedListmais precisamente:

add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...

Mas ok, vamos deixar de lado a inferência de tipo por enquanto e voltar à expressão lambda e como agora não podemos passar mapcomo valor para outra função. Digamos que estendamos um pouco a sintaxe para permitir algo como o que Elm faz com os campos de registro:

add42 nestedList = nestedList.map (.map ((+) 42))

Qual seria o tipo de .mapser? Teria que ser um tipo restrito , como em Haskell!

.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>

Uau OK. Pondo de lado o fato de que o .NET nem sequer permite a existência de tais tipos, efetivamente acabamos de recuperar as classes de tipo!

Mas há uma razão para o F # não ter classes de tipo em primeiro lugar. Muitos aspectos desse motivo estão descritos acima, mas uma maneira mais concisa de colocá-lo é: simplicidade .

Para você ver, este é um novelo de lã. Depois de ter classes de tipo, você deve ter restrições, maior gentileza, classificação N (ou pelo menos classificação 2) e, antes que você perceba, está solicitando tipos impredicativos, funções de tipo, GADTs e todas as resto disso.

Mas Haskell paga um preço por todos os presentes. Acontece que não há uma boa maneira de inferir todas essas coisas. Tipos de tipos mais altos meio que funcionam, mas as restrições já meio que não. Rank-N - nem sonhe com isso. E mesmo quando funciona, você obtém erros de tipo que você precisa ter um PhD para entender. E é por isso que em Haskell você é gentilmente incentivado a colocar assinaturas de tipo em tudo. Bem, nem tudo - tudo , mas realmente quase tudo. E onde você não coloca assinaturas de tipo (por exemplo, dentro lete where) - surpresa-surpresa, esses lugares são realmente monomorfizados, então você está basicamente de volta à terra simplista de F #.

No F #, por outro lado, as assinaturas de tipo são raras, principalmente apenas para documentação ou para interoperabilidade .NET. Fora desses dois casos, você pode escrever um programa grande e complexo em F # e não usar uma assinatura de tipo uma vez. A inferência de tipo funciona bem, porque não há nada muito complexo ou ambíguo para ele lidar.

E essa é a grande vantagem do F # sobre o Haskell. Sim, Haskell permite expressar coisas super complexas de uma maneira muito precisa, isso é bom. Mas o F # permite que você seja muito insensível, quase como Python ou Ruby, e ainda assim o compilador o pega se você tropeçar.

Fyodor Soikin
fonte