Na página 839 da segunda edição, Steve McConnell está discutindo todas as maneiras pelas quais os programadores podem "conquistar a complexidade" em grandes programas. Suas dicas culminam com esta afirmação:
"A programação orientada a objetos fornece um nível de abstração que se aplica a algoritmos e dados ao mesmo tempo , um tipo de abstração que somente a decomposição funcional não fornece".
Juntamente com sua conclusão de que "reduzir a complexidade é indiscutivelmente a chave mais importante para ser um programador eficaz" (mesma página), isso parece um grande desafio à programação funcional.
O debate entre FP e OO é frequentemente enquadrado pelos proponentes do FP em torno das questões de complexidade que derivam especificamente dos desafios de concorrência ou paralelização. Mas a simultaneidade certamente não é o único tipo de complexidade que os programadores de software precisam conquistar. Talvez o foco na redução de um tipo de complexidade a aumente bastante em outras dimensões, de modo que, em muitos casos, o ganho não valha o custo.
Se mudarmos os termos da comparação entre FP e OO de questões específicas como concorrência ou reutilização para o gerenciamento da complexidade global, como seria esse debate?
EDITAR
O contraste que eu queria destacar é que o OO parece encapsular e abstrair da complexidade dos dados e dos algoritmos, enquanto a programação funcional parece encorajadora, deixando os detalhes de implementação das estruturas de dados mais "expostos" ao longo do programa.
Veja, por exemplo, Stuart Halloway (um proponente do Clojure FP) aqui dizendo que "a superespecificação dos tipos de dados" é "consequência negativa do estilo OO idiomático" e favorecendo a conceitualização de um AddressBook como um vetor ou mapa simples em vez de um objeto OO mais rico com propriedades e métodos adicionais (não-vetoriais e não semelhantes a mapl). (Além disso, os proponentes OO e Design Orientado a Domínio podem dizer que a exposição de um Catálogo de Endereços como vetor ou mapa superexpõe os dados encapsulados a métodos irrelevantes ou até perigosos do ponto de vista do domínio).
Respostas:
Lembre-se de que o livro foi escrito há mais de 20 anos. Para programadores profissionais da época, o FP não existia - estava inteiramente no campo de acadêmicos e pesquisadores.
Precisamos enquadrar a "decomposição funcional" no contexto apropriado do trabalho. O autor não está se referindo à programação funcional. Precisamos vincular isso de volta à "programação estruturada" e à
GOTO
bagunça que veio antes. Se o seu ponto de referência for um FORTRAN / COBOL / BASIC antigo que não possui funções (talvez, se você tiver sorte, obteria um único nível de GOSUB) e todas as suas variáveis forem globais, poderá interromper o programa em camadas de funções é um grande benefício.OOP é um refinamento adicional desse tipo de 'decomposição funcional'. Não apenas você pode agrupar instruções em funções, mas também agrupar funções relacionadas com os dados em que estão trabalhando. O resultado é um pedaço de código claramente definido que você pode ver e entender (idealmente) sem precisar percorrer toda a sua base de código para descobrir o que mais pode operar nos seus dados.
fonte
Imagino que os proponentes da programação funcional argumentariam que a maioria das linguagens FP fornece mais meios de abstração do que a "decomposição funcional sozinha" e de fato permitem meios de abstrações comparáveis em poder aos das Linguagens Orientadas a Objetos. Por exemplo, pode-se citar as classes de tipo de Haskell ou os módulos de ordem superior de ML como meio de abstração. Portanto, a declaração (que eu tenho certeza que era sobre orientação a objetos versus programação procedural, não programação funcional) não se aplica a elas.
Também deve ser destacado que FP e OOP são conceitos ortogonais e não mutuamente exclusivos. Portanto, não faz sentido compará-los entre si. Você poderia muito bem comparar "OOP imperativo" (por exemplo, Java) versus "OOP funcional" (por exemplo, Scala), mas a declaração que você citou não se aplicaria a essa comparação.
fonte
Acho a programação funcional extremamente útil para gerenciar a complexidade. Porém, você tende a pensar na complexidade de uma maneira diferente, definindo-a como funções que atuam em dados imutáveis em níveis diferentes, em vez de encapsulamento no sentido de POO.
Por exemplo, recentemente escrevi um jogo no Clojure, e todo o estado do jogo foi definido em uma única estrutura de dados imutável:
E o loop principal do jogo pode ser definido como a aplicação de algumas funções puras ao estado do jogo em um loop:
A função principal chamada é
update-game
, que executa uma etapa de simulação, considerando um estado anterior do jogo e alguma entrada do usuário, e retorna o novo estado do jogo.Então, onde está a complexidade? Na minha opinião, foi bem administrado:
OOP também pode gerenciar a complexidade por meio do encapsulamento, mas se você comparar isso com o OOP, o funcional abordará algumas vantagens muito grandes:
Por fim, para pessoas interessadas em obter mais informações sobre como gerenciar a complexidade em linguagens funcionais versus linguagens OOP, recomendo fortemente o vídeo do discurso de Rich Hickey, Simple Made Easy (filmado na conferência de tecnologia Strange Loop )
fonte
A decomposição funcional sozinha não é suficiente para criar qualquer tipo de algoritmo ou programa: você também precisa representar os dados. Penso que a afirmação acima pressupõe implicitamente (ou pelo menos pode ser entendida como) que os "dados" no caso funcional são do tipo mais rudimentar: apenas listas de símbolos e nada mais. Programar em tal linguagem obviamente não é muito conveniente. No entanto, muitas, especialmente as novas e modernas linguagens funcionais (ou multiparadigmáticas), como o Clojure, oferecem estruturas de dados avançadas: não apenas listas, mas também strings, vetores, mapas e conjuntos, registros, estruturas - e objetos! - com metadados e polimorfismo.
O enorme sucesso prático das abstrações de OO dificilmente pode ser contestado. Mas é a última palavra? Como você escreveu, os problemas de simultaneidade já são os principais problemas, e o OO clássico não contém nenhuma idéia de simultaneidade. Como resultado, as soluções OO de fato para lidar com a simultaneidade são apenas fitas adesivas sobrepostas: funciona, mas é fácil de estragar, retira uma quantidade considerável de recursos cerebrais da tarefa essencial em questão e não é bem dimensionada. Talvez seja possível tirar o melhor de muitos mundos. É isso que as línguas multiparadigmáticas modernas estão buscando.
fonte
O estado mutável é a raiz da maioria das complexidades e problemas relacionados à programação e ao design de software / sistema.
OO abraça um estado mutável. FP detesta estado mutável.
Tanto o OO quanto o FP têm seus usos e pontos positivos. Escolha sabiamente. E lembre-se do ditado: "Fechamentos são objetos do pobre homem. Objetos são fechamento do pobre homem".
fonte
A programação funcional pode ter objetos, mas esses objetos tendem a ser imutáveis. Funções puras (funções sem efeitos colaterais) operam nessas estruturas de dados. É possível criar objetos imutáveis em linguagens de programação orientadas a objetos, mas eles não foram projetados para isso e não é assim que costumam ser usados. Isso dificulta o raciocínio sobre programas orientados a objetos.
Vamos dar um exemplo muito simples. Digamos que a Oracle decidiu que o Java Strings deveria ter um método reverso e você escreveu o código a seguir.
o que a última linha avalia? Você precisa de um conhecimento especial da classe String para saber que isso seria avaliado como falso.
E se eu fiz minha própria classe WuHoString
É impossível saber o que a última linha avalia.
Em um estilo de programação funcional, seria escrito da seguinte maneira:
e deveria ser verdade.
Se uma função em uma das classes mais básicas é tão difícil de raciocinar, então nos perguntamos se a introdução dessa idéia de objetos mutáveis aumentou ou diminuiu a complexidade.
Obviamente, existem todos os tipos de definições do que constitui orientação a objetos e o que significa ser funcional e o que significa ter os dois. Para mim, você pode ter um "estilo de programação funcional" na linguagem que não possui funções como funções de primeira classe, mas outras linguagens são criadas para isso.
fonte
Eu acho que na maioria dos casos, a abstração clássica de POO não cobre a complexidade da simultaneidade. Portanto, OOP (pelo seu significado original) não exclui FP, e é por isso que vemos coisas como scala.
fonte
A resposta depende do idioma. Lisps, por exemplo, têm a realmente puro corretamente que o código é de dados - os algoritmos que você escreve são realmente apenas listas Lisp! Você armazena dados da mesma maneira que escreve o programa. Essa abstração é simultaneamente mais simples e mais completa que o OOP e permite que você faça coisas realmente legais (consulte macros).
Haskell (e linguagem semelhante, imagino) tem uma resposta completamente diferente: tipos de dados algébricos. Um tipo de dados algébrico é como uma
C
estrutura, mas com mais opções. Esses tipos de dados fornecem a abstração necessária para modelar dados; As funções fornecem a abstração necessária para modelar algoritmos. Classes de tipos e outros recursos avançados fornecem um nível ainda mais alto de abstração sobre ambos.Por exemplo, estou trabalhando em uma linguagem de programação chamada TPL por diversão. Tipos de dados algébricos torná-la realmente fácil para representar valores:
O que isso diz - de uma maneira muito visual - é que um TPLValue (qualquer valor no meu idioma) pode ser um
Null
ou umNumber
com umInteger
valor ou mesmo umFunction
com uma lista de valores (os parâmetros) e um valor final (o corpo )Em seguida, posso usar classes de tipo para codificar algum comportamento comum. Por exemplo, eu poderia criar uma
TPLValue
instância doShow
que significa que pode ser convertida em uma string.Além disso, posso usar minhas próprias classes de tipo quando precisar especificar o comportamento de certos tipos (incluindo aqueles que não foram implementados por mim). Por exemplo, eu tenho uma
Extractable
classe de tipo que me permite escrever uma função que recebeTPLValue
ae retorna um valor normal apropriado. Assim,extract
pode converter aNumber
para umInteger
ou aString
para aString
enquantoInteger
eString
são instâncias deExtractable
.Finalmente, a principal lógica do meu programa está em várias funções como
eval
eapply
. Esses são realmente o núcleo - eles pegamTPLValue
se os transformam em maisTPLValue
s, além de lidar com estados e erros.No geral, as abstrações que estou usando no meu código Haskell são realmente mais poderosas do que o que eu teria usado em uma linguagem OOP.
fonte
eval
. "Ei, olhe para mim! Não preciso escrever minhas próprias falhas de segurança; tenho uma vulnerabilidade de execução de código arbitrária embutida na linguagem de programação!" Conflitar dados com código é a causa raiz de uma das duas classes mais populares de vulnerabilidades de segurança de todos os tempos. Sempre que você vê alguém ser hackeado por causa de um ataque de injeção de SQL (entre muitas outras coisas), é porque algum programador lá fora não sabe como separar corretamente os dados do código.eval
não depende muito da estrutura do Lisp - você pode tereval
em linguagens como JavaScript e Python. O poder real vem das macros de escrita, que são basicamente programas que atuam em programas como dados e geram outros programas. Isso torna a linguagem muito flexível e cria abstrações poderosas com facilidade.and
. curto-circuitoor
.let
.let-rec
.cond
.defn
. Nada disso pode ser implementado com funções em linguagens de pedidos aplicativas.for
(lista de compreensões).dotimes
.doto
.and
!" Ouço: "olhe para mim, minha linguagem está tão prejudicada que nem sequer provoca um curto-circuitoand
e eu tenho que reinventar a roda para tudo !"A frase citada não tem mais validade, até onde posso ver.
As linguagens OO contemporâneas não podem abstrair sobre tipos cujo tipo não é *, ou seja, tipos com classificação mais alta são desconhecidos. Seu sistema de tipos não permite expressar a idéia de "algum contêiner com elementos Int, que permite mapear uma função sobre os elementos".
Portanto, essa função básica como Haskells
não pode ser escrito facilmente em Java *), por exemplo, pelo menos não de maneira segura. Portanto, para obter a funcionalidade básica, você precisa escrever muitos clichês, porque precisa
E, no entanto, esses cinco métodos são basicamente o mesmo código, mais ou menos. Em contraste, em Haskell, eu precisaria de:
Observe que isso não vai mudar com o Java 8 (apenas se pode aplicar funções mais facilmente, mas, exatamente, o problema acima se materializará. Contanto que você nem sequer tenha funções de ordem superior, é provável que nem mesmo capaz de entender para que servem os tipos mais elevados.)
Mesmo as novas linguagens OO, como o Ceilão, não têm tipos mais elevados. (Perguntei a Gavin King recentemente, e ele me disse que não era importante no momento.) Mas não sei sobre Kotlin.
*) Para ser justo, você pode ter uma interface Functor que possui um método fmap. O ruim é que você não pode dizer: Ei, eu sei como implementar o fmap para a classe de biblioteca SuperConcurrentBlockedDoublyLinkedDequeHasMap, caro compilador, por favor, aceite que, a partir de agora, todos os SuperConcurrentBlockedDoublyLinkedDequeHasMaps são Functors.
fonte
Qualquer pessoa que já tenha programado no dBase saberia como as macros de linha única foram úteis para criar código reutilizável. Embora eu não tenha programado no Lisp, li de muitos outros que juram por macros de tempo de compilação. A idéia de injetar código no código em tempo de compilação é usada de forma simples em todos os programas em C com a diretiva "include". Como o Lisp pode fazer isso com um programa Lisp e porque o Lisp é altamente reflexivo, você inclui inclusões muito mais flexíveis.
Qualquer programador que apenas pegue uma string de texto arbitrária da Web e a repasse para o banco de dados não é um programador. Da mesma forma, qualquer pessoa que permita que os dados do "usuário" se tornem automaticamente código executável é obviamente estúpida. Isso não significa que permitir que os programas manipulem dados no momento da execução e depois os executem como código é uma má idéia. Acredito que esta técnica será indispensável no futuro, e que terá um código "inteligente" que realmente escreverá a maioria dos programas. Todo o "problema de dados / código" ou não é uma questão de segurança no idioma.
Um dos problemas com a maioria dos idiomas é que eles foram criados para uma única pessoa off-line executar algumas funções por conta própria. Os programas do mundo real exigem que muitas pessoas tenham acesso o tempo todo e ao mesmo tempo a partir de múltiplos núcleos e vários clusters de computadores. A segurança deve fazer parte do idioma, e não do sistema operacional, e em um futuro não muito distante.
fonte