A imutabilidade na programação funcional realmente existe?

9

Embora eu trabalhe como programador em minha vida cotidiana e use todas as linguagens da moda (Python, Java, C, etc), ainda não tenho uma visão clara do que é a programação funcional. Pelo que li, uma propriedade das linguagens funcionais é que as estruturas de dados são imutáveis . Para mim, isso por si só levanta muitas questões. Mas primeiro vou escrever um pouco do que entendo de imutabilidade e, se estiver errado, fique à vontade para me corrigir.

Minha compreensão da imutabilidade:

  • Quando um programa inicia, ele possui estruturas de dados fixas com dados fixos
  • Não se pode adicionar novos dados a essas estruturas
  • Não há variáveis ​​no código
  • Você pode simplesmente "copiar" dos dados já calculados ou atualmente calculados
  • Por tudo isso, a imutabilidade adiciona enorme complexidade de espaço a um programa

Minhas perguntas:

  1. Se as estruturas de dados devem permanecer como são (imutáveis), como diabos alguém adiciona um novo item em uma lista?
  2. Qual é o sentido de ter um programa que não pode obter novos dados? Digamos que você tenha um sensor conectado ao seu computador que deseja alimentar dados para o programa. Isso significa que não podemos armazenar os dados recebidos em nenhum lugar?
  3. Como a programação funcional é boa para o aprendizado de máquina nesse caso? Como o aprendizado de máquina se baseia no pressuposto de atualizar a "percepção" do programa - armazenando assim novos dados.
Pithikos
fonte
2
Discordo de você quando diz que não há variáveis ​​no código funcional. Existem variáveis ​​no sentido matemático de "Uma quantidade que pode assumir qualquer um de um conjunto de valores". Eles não são mutáveis , com certeza, mas também não são em matemática.
Édouard
11
Eu acho que sua confusão é porque você está pensando em linguagens funcionais de maneira abstrata. Basta pegar qualquer programa no Haskell - por exemplo, um programa que leia uma lista de números do console, classifique-o rapidamente e o emita - e descubra como ele funciona e como refuta suas suspeitas. Não há como realmente esclarecer as coisas sem olhar para exemplos de programas reais, em vez de filosofar. Você encontrará muitos programas em qualquer tutorial do Haskell.
JKFF
@jkff O que você está tentando dizer? Esse Haskel possui recursos não funcionais. A questão não é sobre Haskell, mas sobre programação funcional. Ou você está afirmando que tudo o que é funcional? Quão? Então, o que deveria estar errado com filosofar, como você diz. De que maneira a abstração é confusa? A questão do OP é muito sensata.
babou
@babou Estou tentando dizer que a melhor maneira de entender como uma linguagem de programação puramente funcional pode implementar eficientemente algoritmos e estruturas de dados é ver exemplos de algoritmos e estruturas de dados implementados eficientemente em uma linguagem de programação funcional. Parece-me que o OP estava tentando entender como é conceitualmente possível - acho que a maneira mais rápida de entender isso é ver exemplos, em vez de ler uma explicação conceitual, por mais detalhada que seja.
JKFF
Uma maneira de observar a programação funcional é dizer que está programando sem efeitos colaterais. Você pode fazer isso no seu idioma "na moda" de sua escolha. Apenas não evite todas as reatribuições: por exemplo, em Java, todas as suas variáveis ​​serão finais e todos os seus métodos serão somente leitura.
Reinierpost

Respostas:

10

Quando um programa inicia, ele possui estruturas de dados fixas com dados fixos

Isso é um pouco de um equívoco. Ele tem uma forma fixa e um conjunto fixo de regras de reescrita, mas essas regras de reescrita podem explodir em algo muito maior. Por exemplo, a expressão [1..100000000] em Haskell é representada por uma quantidade muito pequena de código, mas sua forma normal é massiva.

Não se pode adicionar novos dados a essas estruturas

Sim e não. O subconjunto puramente funcional de uma linguagem como Haskell ou ML não pode obter dados do mundo exterior, mas qualquer linguagem para programação prática possui um mecanismo para inserir dados do mundo externo no subconjunto puramente funcional. No Haskell, isso é feito com muito cuidado, mas no ML você pode fazer isso sempre que quiser.

Não há variáveis ​​no código

Isso é verdade, mas não confunda isso com a idéia de que nada pode ser nomeado. Você nomeia expressões úteis o tempo todo e as reutiliza constantemente. Também o ML e o Haskell, todos os Lisp que experimentei, e os híbridos como o Scala, todos têm um meio de criar variáveis. Eles simplesmente não são comumente usados. E novamente os subconjuntos puramente funcionais de tais idiomas não os possuem.

Você pode simplesmente "copiar" dos dados já calculados ou atualmente calculados

Você pode executar o cálculo reduzindo-o para a forma normal. A melhor coisa a fazer é provavelmente escrever programas em uma linguagem funcional para ver como eles realmente fazem cálculos.

Por exemplo, "soma [1..1000]" não é um cálculo que eu quero executar, mas é bastante prático por Haskell. Demos uma pequena expressão que tinha significado para nós e Haskell nos deu o número correspondente. Portanto, ele definitivamente realiza o cálculo.

Se as estruturas de dados devem permanecer como são (imutáveis), como diabos alguém adiciona um novo item em uma lista?

Você não adiciona um novo item a uma lista, cria uma nova lista a partir do antigo. Como o antigo não pode ser modificado, é perfeitamente seguro usá-lo na nova lista ou em qualquer outro lugar que você desejar. Muito mais dados podem ser compartilhados com segurança nesse esquema.

Qual é o sentido de ter um programa que não pode obter novos dados? Digamos que você tenha um sensor conectado ao seu computador que deseja alimentar dados para o programa. Isso significa que não podemos armazenar os dados recebidos em nenhum lugar?

Quanto à entrada do usuário, qualquer linguagem de programação prática tem uma maneira de obter entrada do usuário. Isto acontece. No entanto, há um subconjunto totalmente funcional desses idiomas em que você escreve a maior parte do seu código e colhe as vantagens dessa maneira.

Como a programação funcional é boa para o aprendizado de máquina nesse caso? Como o aprendizado de máquina se baseia no pressuposto de atualizar a "percepção" do programa - armazenando assim novos dados.

Esse seria o caso do aprendizado ativo, mas a maioria dos aprendizado de máquina com os quais trabalhei (eu trabalho como um macaco de código em um grupo de aprendizado de máquina e o faço há alguns anos) tem um processo de aprendizado único, onde todos os dados de treinamento são carregados de uma vez. Mas para o aprendizado ativo, você não pode fazer as coisas de forma 100% puramente funcional. Você precisará ler alguns dados do mundo exterior.

Jake
fonte
Sinto que você convenientemente ignorou o que pode ser o ponto mais importante no post do @ Pithikos, que é a questão do espaço - programas funcionais usam mais espaço do que os imperativos (você não pode escrever algoritmos no local e outros)
user541686
2
Isso simplesmente não é verdade. A falta de mutação é amplamente compensada pelo compartilhamento e, acima de tudo, a diferença de tamanho a que você se refere é absurdamente pequena nos compiladores modernos. A maior parte do código nas listas do haskell está efetivamente no lugar ou não usa memória nenhuma.
Jake
11
Eu acho que você deturpa ML um pouco. Sim, a E / S pode acontecer em qualquer lugar, mas a maneira como novas informações são introduzidas nas estruturas existentes é rigorosamente controlada.
Dfeuer
@ Pitithos, existem variáveis ​​em todo o lugar; eles são diferentes do que você está acostumado, como Édouard indicou. E as coisas são continuamente alocadas e o lixo coletado. Depois de realmente entrar na programação funcional, você terá uma noção melhor de como ela realmente funciona.
Dfeuer
11
É verdade que existem algoritmos que não têm uma implementação puramente funcional com a mesma complexidade de tempo da implementação imperativa mais conhecida - por exemplo, a estrutura de dados Union-Find (e, um, matrizes :)). Imagino que também haja casos como este para o espaço complexidade. Mas essas são exceções - os algoritmos / estruturas de dados mais comuns têm implementações com complexidade equivalente de tempo e espaço. É uma questão subjetiva de estilo de programação e (a um fator constante) de qualidade do compilador.
jkff 21/02
4

Imutabilidade ou mutabilidade não são conceitos que fazem sentido na programação funcional.

O contexto computacional

Essa é uma pergunta muito boa, que é um acompanhamento interessante (não duplicado) de outra recente: Qual é a diferença entre atribuição, avaliação e vinculação de nome?

Em vez de responder às suas declarações uma a uma, estou tentando aqui fornecer uma visão geral estruturada do que está em jogo.

Há vários problemas a serem considerados para responder a você, incluindo:

  • O que é um modelo de computação e quais conceitos fazem sentido para um determinado modelo

  • Qual é o significado das palavras que você está usando e como isso depende do contexto

O estilo de programação funcional parece tolo porque você o vê com um olhar imprescindível para o programador. Mas é um paradigma diferente, e seus conceitos e percepção imperativos são estranhos, fora de lugar. Os compiladores não têm tais preconceitos.

Mas a conclusão final é que é possível escrever programas de uma maneira puramente funcional, inclusive para aprendizado de máquina, pois a programação funcional não possui o conceito de armazenamento de dados. Eu pareço discordar nesse ponto com outras respostas.

Na esperança de que alguns se interessem, apesar da duração desta resposta.

Paradigmas computacionais

A questão é sobre programação funcional (também conhecida como programação aplicativa), um modelo específico de computação, cujo representante teórico e mais simples é o cálculo lambda.

Se você permanecer no nível teórico, existem muitos modelos de computação: a máquina de Turing (TM), a máquina RAM e outros , o cálculo lambda, lógica combinatória, teoria da função recursiva, sistemas semi-Thue etc. provou-se modelos equivalentes em termos do que eles podem abordar, e essa é a essência da tese de Church-Turing .

Um conceito importante é a redução de modelos entre si, que é a base para estabelecer as equivalências que levam à tese de Church-Turing. Visto da perspectiva dos programadores, reduzir um modelo para outro é basicamente o que geralmente é chamado de compilador. Se você usa a programação lógica como seu modelo de computação, é bem diferente do modelo fornecido pelo PC que você comprou em uma loja e o compilador converte programas escritos em linguagem de programação lógica para o modelo computacional representado pelo seu PC (praticamente computador RAM).

β

Na prática, as linguagens de programação que usamos tendem a misturar conceitos de diferentes origens teóricas, tentando fazê-lo para que partes selecionadas de um programa possam se beneficiar das propriedades de algum modelo, quando apropriado. Da mesma forma, as pessoas que constroem sistemas podem escolher idiomas diferentes para componentes diferentes, para melhor adequar o idioma à tarefa em questão.

Portanto, você raramente vê um paradigma de programação em estado puro em uma linguagem de programação. As linguagens de programação ainda são classificadas de acordo com o paradigma dominante, mas as propriedades da linguagem podem ser afetadas quando conceitos de outros paradigmas estão envolvidos, muitas vezes obscurecendo distinções e questões conceituais.

Normalmente, linguagens como Haskell e ML ou CAML são consideradas funcionais, mas podem permitir um comportamento imperativo ... Por que alguém poderia falar do " subconjunto puramente funcional "?

Então, pode-se afirmar que, você pode fazer isso ou aquilo na minha linguagem de programação funcional, mas não está realmente respondendo a uma pergunta sobre programação funcional quando se baseia no que pode ser considerado extra-funcional.

As respostas devem estar mais precisamente relacionadas a um paradigma específico, sem os extras.

O que é uma variável?

Outro problema é o uso da terminologia. Em matemática, uma variável é uma entidade que representa um valor indeterminado em algum domínio. É utilizado para vários fins. Utilizado em uma equação, pode representar qualquer valor que permita que a equação seja verificada. Essa visão é usada na programação lógica sob o nome de " variável lógica ", provavelmente porque a variável name já tinha outro significado quando a programação lógica foi desenvolvida.

Na programação imperativa tradicional, uma variável é entendida como algum tipo de contêiner (ou localização da memória) que pode memorizar a representação de um valor e, possivelmente, obter seu valor atual substituído por outro).

Na programação funcional, uma variável tem o mesmo propósito que em matemática que um espaço reservado para algum valor, ainda a ser fornecido. Na programação imperativa tradicional, esse papel é realmente desempenhado por constante (não deve ser confundido com literais cujo valor determinado é expresso com uma notação específica para esse domínio de valores, como 123, true, ["abdcz", 3,14]).

Variáveis ​​de qualquer tipo, bem como constantes, podem ser representadas por identificadores.

A variável imperativa pode ter seu valor alterado e essa é a base da mutabilidade. A variável funcional não pode.

As linguagens de programação geralmente permitem a criação de entidades maiores a partir das menores na linguagem.

Linguagens imperativas permitem que essas construções incluam variáveis ​​e é isso que fornece dados mutáveis.

Como ler um programa

Um programa é fundamentalmente uma descrição abstrata do seu algoritmo, é uma linguagem, seja um design pragmático ou uma linguagem paradigmaticamente pura.

Em princípio, você pode aceitar todas as afirmações do que é suposto significar abstratamente. Em seguida, o compilador traduzirá isso para alguma forma apropriada para o computador executar, mas esse não é o seu problema na primeira aproximação.

Obviamente, a realidade é um pouco mais dura, e geralmente é bom ter uma idéia do que acontece, para evitar estruturas com as quais o compilador não saberá lidar com uma execução eficiente. Mas isso já é otimização ... para quais compiladores podem ser muito bons, geralmente melhores que os programadores.

Programação funcional e mutabilidade

A mutabilidade é baseada na existência de variáveis ​​imperativas que podem conter valores, a serem alteradas por atribuição. Como estes não existem na programação funcional, tudo pode ser visto como imutável.

A programação funcional lida exclusivamente com valores.

Suas quatro primeiras afirmações sobre imutabilidade estão na maioria corretas, mas descreva com visão imperativa algo que não é imperativo. É um pouco como descrever com cores em um mundo onde todos são cegos. Você está usando conceitos estranhos à programação funcional.

Você tem apenas valores puros e uma matriz de números inteiros é um valor puro. Para obter outra matriz que difere apenas em um elemento, você deve usar um valor diferente. Alterar um elemento é apenas um conceito que não existe neste contexto. Você pode ter uma função que possui uma matriz e alguns índices como argumento e retorna um resultado que é uma matriz quase idêntica, que difere apenas onde indicado pelos índices. Mas ainda é um valor de matriz independente. Como esses valores são representados não é problema seu. Talvez eles "compartilhem" muito a tradução imperativa para o computador ... mas esse é o trabalho do compilador ... e você nem quer saber para que tipo de arquitetura de máquina está compilando.

Você não copia valores (não faz sentido, é um conceito estranho). Você apenas usa valores que existem nos domínios que você definiu em seu programa. Você os descreve (como literais) ou eles são o resultado da aplicação de uma função a alguns outros valores. Você pode dar um nome a eles (definindo assim uma constante) para garantir que o mesmo valor seja usado em diferentes locais do programa. Observe que o aplicativo de funções não deve ser percebido como um cálculo, mas como o resultado do aplicativo para os argumentos fornecidos. Escrever 5+2ou escrever 7é o mesmo. O que é consistente com o parágrafo anterior.

Não há variáveis ​​imperativas. Nenhuma atribuição é possível. Você pode vincular nomes apenas a valores (para formar constantes), diferentemente de linguagens imperativas nas quais é possível vincular nomes a variáveis ​​atribuíveis.

Se isso tem um custo em complexidade é totalmente incerto. Por um lado, a referência à complexidade diz respeito a paradigmas imperativos. Ele não está definido como tal para programação funcional, a menos que você opte por ler seu programa funcional como um imperativo, que não é a intenção do designer. De fato, a visão funcional visa deixar você não se preocupar com esses problemas e se concentrar no que está sendo calculado. É um pouco como especificação versus implementação.

O compilador precisa cuidar da implementação e ser inteligente o suficiente para melhor adaptar o que deve ser feito ao hardware que o fará, seja ele qual for.

Não estou dizendo que programadores nunca se preocupam com isso. Também não estou dizendo que linguagens de programação e tecnologia de compiladores são tão maduras quanto gostaríamos que elas fossem.

Respondendo as perguntas

  1. Você não modifica o valor existente (conceito de alienígena), mas calcula novos valores que diferem onde desejado, possivelmente por ter um elemento extra que é uma lista.

  2. O programa pode obter novos dados. O ponto principal é como você expressa isso no idioma. Você pode, por exemplo, considerar que o programa funciona com um valor específico, possivelmente sem tamanho, chamado de fluxo de entrada. É um valor que deveria estar ali (se já é conhecido por completo ou não, não é seu problema). Então você tem uma função que retorna um par composto pelo primeiro elemento do fluxo e pelo restante do fluxo.

    Você pode usar isso para criar redes de componentes de comunicação de maneira puramente aplicativa (corotinas)

  3. O aprendizado de máquina é apenas outro problema quando você precisa acumular dados e modificar valores. Na programação funcional, você não faz isso: apenas calcula novos valores que diferem adequadamente de acordo com os dados de treinamento. A máquina resultante também funcionará. O que você se preocupa é calcular a eficiência de tempo e espaço. Mas, novamente, essa é uma questão diferente, que idealmente deve ser tratada pelo compilador.

Considerações finais

É bem claro, pelos comentários ou pelas outras respostas, que as linguagens de programação funcional práticas não são puramente funcionais. Isso reflete o fato de que nossa tecnologia ainda precisa ser aprimorada, especialmente no que diz respeito à compilação.

É possível escrever em um estilo puramente aplicativo? A resposta é conhecida há cerca de 40 anos e é "sim". O próprio objetivo da semântica denotacional, tal como surgiu nos anos 70, era justamente traduzir (compilar) linguagens para um estilo puramente funcional, considerado melhor entendido matematicamente e, portanto, considerado um melhor financiamento para definir a semântica dos programas.

O aspecto interessante disso é que a estrutura de programação imperativa, incluindo variáveis, pode ser convertida em um estilo funcional, introduzindo domínios de valores apropriados, como um armazenamento de dados. E, apesar do estilo funcional, ele permanece surpreendentemente semelhante ao código dos compiladores reais escritos em estilo imperativo.

babou
fonte
0

É um equívoco que os programas funcionais não possam armazenar dados, e não acho que a resposta de Jakes tenha realmente explicado isso muito bem.

Programas funcionais são, como qualquer programa, realmente funcionam mapeando números inteiros para números inteiros. Qualquer programa imperativo operando em estruturas de dados mutáveis ​​tem uma contrapartida funcional. Este é apenas outro meio de alcançar o mesmo fim.

A maneira funcional de armazenar dados experimentais de alguma fonte seria chamar a função de armazenamento com a estrutura de dados como argumento e produzir uma concatenação da estrutura de dados existente e dos novos dados e, portanto, os dados são armazenados sem a noção de estruturas de dados mutáveis.

Pela minha própria experiência , acho que o conceito de estruturas de dados imutáveis ​​está levando os desenvolvedores convencionais a pensar que há certas coisas que são impraticáveis ​​ou mesmo impossíveis de fazer em um ambiente funcional. Este não é o caso.

Jeppe Hartmund
fonte
"Programas funcionais são, como qualquer programa, realmente funcionam mapeando números inteiros para números inteiros." Como é, digamos, Minecraft, realmente uma função que mapeia números inteiros para números inteiros?
David Richerby
Facilmente. Cada byte pode ser interpretado como um número inteiro binário. Um estado em um computador é uma coleção de bytes. Um programa, até o Minecraft, manipula um estado de computador, mapeando-o de um estado para outro.
Jeppe Hartmund
A entrada do usuário não parece se encaixar neste mundo.
David Richerby
A entrada do usuário faz parte de um estado de computadores. Não existe apenas na sua tela.
Jeppe Hartmund