Por que os programas funcionais têm uma correlação entre o sucesso e a correção da compilação?

12

Eu tenho tendência para a programação funcional há 4 anos, desde que comecei a trabalhar com o LINQ. Recentemente, escrevi um código C # funcional puro e notei, em primeira mão, o que tenho lido sobre programas funcionais - que, uma vez compilados, tendem a estar corretos.

Tentei explicar por que esse é o caso, mas não consegui.

Um palpite é que, ao aplicar princípios de OO, você tem uma "camada de abstração" que não está presente em programas funcionais e essa camada de abstração possibilita que os contratos entre os objetos estejam corretos enquanto a implementação estiver incorreta.

Alguém já pensou nisso e apresentou a razão abstrata subjacente para a correlação entre o sucesso da compilação e a correção do programa na programação funcional?

Anodeto de Aaron
fonte
9
Lisp é uma linguagem funcional, mas ainda não possui verificações de tempo de compilação. O mesmo para algumas outras linguagens funcionais. Uma caracterização mais precisa dos idiomas de que você fala seria: Idiomas com poderosos sistemas do tipo formal (pelo menos Hindley-Milner).
1
@ delnan Eu não diria que o Lisp é uma linguagem de programação funcional, embora possa ser usada para escrever código de programação funcional. Clojure que é um dialeto Lisp é uma linguagem de programação funcional
sakisk
2
Eu concordo com @delnan. Essa afirmação está mais relacionada às linguagens de programação funcional de tipo estatístico, especialmente Haskell, que usa o sistema Hindley-Milner. Penso que a ideia principal é que, se você acertar os tipos, aumenta a confiança de que seu programa está correto.
Sakisk
1
O código funcional pode ter tantas abstrações e indireções quanto o seu código OOP típico (se não mais). O diabo está nos detalhes - menos efeitos colaterais e nenhum nulo significa menos estado invisível para rastrear e menos chances de estragar. Observe que você pode aplicar esses mesmos princípios nas principais linguagens imperativas, é apenas mais trabalho e geralmente mais detalhado (por exemplo, ter que dar um tapa finalem tudo).
Doval
1
Não é uma resposta completa, mas a digitação do programa é uma forma bruta de verificação formal do programa. Em geral, os programas orientados a objetos têm sistemas de tipos complicados ou muito simples, pois a substituição precisa ser levada em consideração - na maioria dos casos, eles são tornados doentios por conveniência. OTOH, os sistemas do tipo ML podem usar CH em toda a extensão, para que você possa codificar uma prova apenas nos tipos e usar o compilador como verificador de prova.
Maciej Piechotka

Respostas:

12

Posso escrever essa resposta como alguém que prova muito as coisas, portanto, para mim, a correção não é apenas o que funciona, mas o que funciona e é fácil de provar.

Em muitos sentidos, a programação funcional é mais restritiva do que a programação imperativa. Afinal, nada impede que você nunca mude uma variável em C! De fato, a maioria dos recursos nas linguagens FP é simples de falar em termos de apenas alguns recursos principais. Tudo se resume a lambdas, aplicativo de função e correspondência de padrões!

No entanto, como pagamos antecipadamente ao flautista, temos muito menos com o que lidar e temos muito menos opções de como as coisas podem dar errado. Se você é um fã de 1984, a liberdade é realmente escravidão! Usando 101 truques diferentes para um programa, temos que raciocinar sobre as coisas como se qualquer uma dessas 101 coisas pudesse acontecer! Isso é realmente difícil de fazer, como se vê :)

Se você começar com uma tesoura de segurança em vez de uma espada, correr é moderadamente menos perigoso.

Agora, examinamos sua pergunta: como tudo isso se encaixa no "compila e funciona!" fenômenos. Acho que grande parte disso é o mesmo motivo pelo qual é fácil provar o código! Afinal, quando você escreve um software, está construindo alguma prova informal de que está correto. Por causa disso, o que é coberto pelas suas provas onduladas naturais e os próprios compiladores têm noção de correção (verificação tipográfica) é bastante.

À medida que você adiciona recursos e interações complicadas entre eles, o que não é verificado pelo sistema de tipos aumenta. No entanto, sua capacidade de construir provas informais não parece melhorar! Isso significa que há mais coisas que podem passar pela sua inspeção inicial e devem ser capturadas mais tarde.

Daniel Gratzer
fonte
1
Eu gosto da sua resposta, mas eu não ver como ele responde à pergunta do OP
sakisk
1
@faif Expandiu minha resposta. TLDR: todo mundo é matemático.
Daniel Gratzer
"Ao usar 101 truques diferentes para um programa, temos que raciocinar sobre as coisas como se alguma dessas 101 coisas pudesse acontecer!": Li em algum lugar que você precisa ser um gênio para programar com mutações, porque você precisa muita informação em sua cabeça.
Giorgio
12

a razão abstrata subjacente para a correlação entre o sucesso da compilação e a correção do programa na programação funcional?

Estado mutável.

Compiladores verificam as coisas estaticamente. Eles garantem que seu programa seja bem formado e o sistema de tipos fornece um mecanismo para tentar garantir que o tipo certo de valores seja permitido no tipo certo de local. O sistema de tipos também tenta garantir que o tipo certo de semântica seja permitido no tipo certo de lugares.

Assim que seu programa introduz o estado, essa última restrição se torna menos útil. Você não apenas precisa se preocupar com os valores certos nos pontos certos, mas também deve levar em consideração essa alteração de valor em pontos arbitrários do seu programa. Você precisa levar em consideração a semântica do seu código mudando ao lado desse estado.

Se você estiver executando bem a programação funcional, não há (ou muito pouco) estado mutável.

Há algum debate sobre a causa aqui - se programas sem estado funcionam após a compilação com mais freqüência porque o compilador pode pegar mais bugs ou se programas sem estado funcionam após a compilação com mais freqüência porque esse estilo de programação produz menos bugs.

Provavelmente é uma mistura de ambos na minha experiência.

Telastyn
fonte
2
"É provavelmente uma mistura de ambos na minha experiência.": Eu tenho a mesma experiência. A digitação estática também captura erros no tempo de compilação ao usar uma linguagem imperativa (por exemplo, Pascal). No FP, evitar a mutabilidade e, eu acrescentaria, o uso de um estilo de programação mais declarativo facilita a discussão sobre o código. Se um idioma oferece ambos, você obtém as duas vantagens.
Giorgio
7

Simplificando, as restrições significam que há menos maneiras corretas de organizar as coisas, e as funções de primeira classe facilitam o fatoramento de coisas como estruturas de loop. Pegue o loop desta resposta , por exemplo:

for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        iterator.remove();
    }
}

Essa é a única maneira imperativa e segura em Java para remover um elemento de uma coleção enquanto você está iterando. Há muitas maneiras que parecem muito próximas, mas estão erradas. As pessoas que desconhecem esse método às vezes passam por maneiras complicadas para evitar o problema, como iterando uma cópia.

Não é terrivelmente difícil tornar esse genérico, portanto ele funcionará mais do que apenas coleções Strings, mas sem funções de primeira classe, você não pode substituir o predicado (a condição dentro do if), portanto esse código tende a ser copiado e colado e modificado ligeiramente.

Combine funções de primeira classe que lhe dão a capacidade para passar o predicado como um parâmetro, com a restrição de imutabilidade que o torna muito chato se você não faz, e você vem com blocos de construção simples, como filter, como neste código Scala isso faz a mesma coisa:

list filter (!_.isEmpty)

Agora pense no que o sistema de tipos verifica para você, no momento da compilação, no caso do Scala, mas essas verificações também são feitas por sistemas de tipos dinâmicos na primeira vez em que você o executa:

  • listdeve ser algum tipo de tipo que suporte o filtermétodo, ou seja, uma coleção.
  • Os elementos de listdevem ter um isEmptymétodo que retorne um booleano.
  • A saída será uma coleção (potencialmente) menor com o mesmo tipo de elementos.

Uma vez que essas coisas foram verificadas, que outras maneiras restam para o programador estragar? Esqueci acidentalmente o !que causou uma falha extremamente óbvia no caso de teste. Esse é praticamente o único erro disponível para ser cometido, e eu o cometi apenas porque estava traduzindo diretamente do código que testou a condição inversa.

Esse padrão é repetido repetidamente. As funções de primeira classe permitem refatorar as coisas em pequenos utilitários reutilizáveis ​​com semântica precisa, restrições como a imutabilidade fornecem o ímpeto para fazê-lo e a verificação de tipo dos parâmetros desses utilitários deixa pouco espaço para estragar tudo.

Obviamente, tudo depende do programador saber que a função simplificadora filterjá existe e ser capaz de encontrá-la ou reconhecer o benefício de criar você mesmo. Tente implementar isso sozinho em qualquer lugar usando apenas recursão da cauda, ​​e você estará de volta no mesmo barco de complexidade que a versão imperativa, apenas pior. Só porque você pode escrevê-lo de maneira muito simples, não significa que a versão simples seja óbvia.

Karl Bielefeldt
fonte
"Depois que essas coisas são verificadas, que outras maneiras ainda faltam para o programador estragar?": Isso de alguma forma confirma minha experiência de que (1) digitação estática + (2) estilo funcional deixam menos maneiras de estragar as coisas. Como resultado, tendem a obter um programa correto mais rapidamente e preciso escrever menos testes de unidade ao usar o FP.
Giorgio
2

Eu não acho que exista uma correlação significativa entre a compilação da programação funcional e a correção do tempo de execução. Pode haver alguma correlação entre a compilação digitada estaticamente e a correção do tempo de execução, pois pelo menos você pode ter os tipos certos, se não estiver transmitindo.

O aspecto da linguagem de programação que pode, de alguma forma, correlacionar a compilação bem-sucedida com a correção do tipo de tempo de execução, como você descreve, é digitação estática e, mesmo assim, apenas se você não estiver enfraquecendo o verificador de tipos com moldes que só podem ser afirmados em tempo de execução (em ambientes com valores ou locais fortemente digitados, por exemplo, Java ou .Net) ou não (em ambientes onde as informações de tipo são perdidas ou com digitação fraca, por exemplo, C e C ++).

No entanto, a programação funcional em si pode ajudar de outras maneiras, como evitar dados compartilhados e estado mutável.

Ambos os aspectos juntos podem ter uma correlação significativa na correção, mas você deve estar ciente de que não ter erros de compilação e tempo de execução não diz nada, estritamente falando, sobre a correção em um sentido mais amplo, como no programa faz o que deve fazer e falha rapidamente sobre entrada inválida ou falha incontrolável do tempo de execução. Para isso, você precisa de regras de negócios, requisitos, casos de uso, asserções, testes de unidade, testes de integração etc. No final, pelo menos na minha opinião, eles fornecem muito mais confiança do que a programação funcional, a digitação estática ou ambas.

acelent
fonte
Este. A correção de um programa não pode ser julgada por uma compilação bem-sucedida. Se um compilador puder entender os requisitos muitas vezes conflitantes e imprecisos de todas as pessoas que contribuíram para as especificações do programa, talvez uma compilação bem-sucedida possa ser considerada como correta. Mas esse compilador mítico não precisaria de um programador! Embora possa ser um pouco mais elevada correlação geral entre compilação e correção para funcional versus programa imperativas, é uma parte tão pequena do julgamento total de correção que eu acho que é basicamente irrelevantes
Jordan Rieger
2

Explicação para gerentes:

Um programa funcional é como uma grande máquina onde tudo está conectado, tubos, cabos. [Um carro]

Um programa processual é como um prédio com salas contendo uma pequena máquina, armazenando produtos parciais em caixas, obtendo produtos parciais de outros lugares. [Uma fábrica]

Então, quando a máquina funcional já se encaixa: ela deve produzir alguma coisa. Se um complexo processual for executado, você poderá ter supervisionado efeitos específicos, introduzido o caos, sem garantir o funcionamento. Mesmo se você tiver uma lista de verificação de tudo que está sendo corretamente integrado, existem tantos estados, situações possíveis (produtos parciais por aí, baldes transbordando, falta), que as garantias são difíceis de dar.


Mas, sério, o código processual não especifica a semântica do resultado desejado, tanto quanto o código funcional. Programadores de procedimentos podem mais facilmente se livrar de códigos e dados circunstanciais e introduzir várias maneiras de fazer uma coisa (algumas delas imperfeitas). Normalmente, dados estranhos são criados. Programadores funcionais podem levar mais tempo quando o problema fica mais complexo?

Uma linguagem funcional de tipo forte ainda pode fazer melhores dados e análise de fluxo. Com uma linguagem processual, o objetivo de um programa geralmente precisa ser definido fora do programa, como uma análise formal de correção.

Joop Eggen
fonte
1
Melhor anologia: a programação funcional é como um suporte técnico sem clientes - tudo é maravilhoso (desde que você não questione a finalidade ou a eficiência).
Brendan
O @Brendan car & factory não forma uma analogia tão ruim. Ele tenta explicar por que os programas (em pequena escala) em uma linguagem funcional têm mais probabilidade de funcionar e são menos propensos a erros do que uma "fábrica". Mas, para resgatar, digamos, OOP chega que uma fábrica pode produzir várias coisas e é maior. Sua comparação é adequada; com que freqüência ouve-se o FP pode paralelizar e otimizar enormemente, mas com efeito (sem trocadilhos) produz resultados lentos. Eu ainda mantenho a FP.
Joop Eggen
A programação funcional em escala funciona muito bem para um site en.wikipedia.org/wiki/Spherical_cow .
Den
@ Den Eu mesmo não temeria problemas de viabilidade trabalhando em um projeto de FP em larga escala. Até adoro. A generalização tem sua limitação. Mas, como essa última afirmação é uma generalização também ... (graças para a vaca esférica)
Joop Eggen