Todas as linguagens funcionais usam coleta de lixo?

32

Existe uma linguagem funcional que permita usar a semântica da pilha - destruição determinística automática no final do escopo?

user467799
fonte
Destruição determinística é realmente útil apenas com efeitos colaterais. No contexto de pura programação funcional, isso significa apenas garantir que certas ações (monádicas) sempre sejam executadas no final de uma sequência. Como um aparte, é fácil escrever uma linguagem concatenativa funcional que não precise de coleta de lixo.
quer
Estou interessado no conteúdo da pergunta, o que se tem a ver com o outro?
mattnz
1
Em uma linguagem funcional sem coleta de lixo, não vejo como o compartilhamento estrutural de estruturas de dados imutáveis ​​é possível. Pode ser possível criar esse idioma, mas não é o que eu usaria.
dan_waterworth
O Rust possui muitos recursos comumente identificados como 'funcionais' (pelo menos, são comumente desejados em idiomas não funcionais como recursos funcionais). Estou curioso para saber o que está faltando. Por padrão, são encerrados, fechamentos, passagem de funções, sobrecarga de princípios, ADTs (sem GADTs ainda), correspondência de padrões, tudo sem GC. O quê mais?
Noein

Respostas:

10

Não que eu saiba, embora eu não seja um especialista em programação funcional.

Parece bastante difícil em princípio, porque os valores retornados das funções podem conter referências a outros valores que foram criados (na pilha) dentro da mesma função, ou podem ter sido passados ​​tão facilmente como um parâmetro ou referenciados por algo passado em como um parâmetro. Em C, esse problema é tratado ao permitir que ponteiros oscilantes (ou mais precisamente, comportamento indefinido) possam ocorrer se o programador não acertar as coisas. Esse não é o tipo de solução que os designers de linguagem funcional aprovam.

Existem possíveis soluções, no entanto. Uma idéia é tornar a vida útil do valor parte do tipo do valor, juntamente com as referências a ele, e definir regras baseadas em tipo que impedem que valores alocados à pilha sejam retornados ou referenciados por algo retornado de função. Não resolvi as implicações, mas suspeito que seria horrível.

Para o código monádico, há outra solução que é (realmente ou quase) monádica também e poderia fornecer um tipo de IORef destruído automaticamente por determinação determinística. O princípio é definir ações de "aninhamento". Quando combinados (usando um operador associativo), eles definem um fluxo de controle de aninhamento - acho que "elemento XML", com os valores mais à esquerda, fornecendo o par externo de etiqueta de início e fim. Essas "tags XML" estão apenas definindo a ordem das ações monádicas em outro nível de abstração.

Em algum momento (no lado direito da cadeia de composição associativa), você precisa de algum tipo de terminador para finalizar o aninhamento - algo para preencher o buraco no meio. A necessidade de um terminador é o que provavelmente significa que o operador de composição de aninhamento não é monádico; porém, novamente, não tenho muita certeza, pois não trabalhei nos detalhes. Como toda aplicação que o terminador faz é converter uma ação de aninhamento em efetivamente uma ação monádica normal composta, talvez não - isso não afeta necessariamente o operador de composição de aninhamento.

Muitas dessas ações especiais teriam uma etapa "tag final" nula e equiparariam a etapa "tag inicial" a alguma ação monádica simples. Mas alguns representariam declarações de variáveis. Eles representariam o construtor com a tag de início e o destruidor com a tag de finalização. Então você recebe algo como ...

act = terminate ((def-var "hello" ) >>>= \h ->
                 (def-var " world") >>>= \w ->
                 (use-val ((get h) ++ (get w)))
                )

Traduzindo para uma composição monádica com a seguinte ordem de execução, cada tag (não elemento) se torna uma ação monádica normal ...

<def-var val="hello">  --  construction
  <def-var val=" world>  --  construction
    <use-val ...>
      <terminator/>
    </use-val>  --  do nothing
  </def-val>  --  destruction
</def-val>  --  destruction

Regras como essa podem permitir a implementação de RAII no estilo C ++. As referências do tipo IORef não podem escapar de seu escopo, por razões semelhantes às por que os IORefs normais não podem escapar da mônada - as regras da composição associativa não fornecem como a referência escapar.

EDIT - eu quase esqueci de dizer - há uma área definida que não tenho certeza sobre aqui. É importante garantir que uma variável externa não possa fazer referência a uma interna, basicamente, portanto, deve haver restrições quanto ao que você pode fazer com essas referências do tipo IORef. Mais uma vez, não trabalhei com todos os detalhes.

Portanto, a construção poderia, por exemplo, abrir um arquivo cuja destruição é encerrada. A construção poderia abrir um encaixe cuja destruição se fecha. Basicamente, como em C ++, as variáveis ​​se tornam gerenciadores de recursos. Mas, diferentemente do C ++, não há objetos alocados em heap que não podem ser destruídos automaticamente.

Embora essa estrutura suporte RAII, você ainda precisa de um coletor de lixo. Embora uma ação de aninhamento possa alocar e liberar memória, tratando-a como um recurso, ainda existem todas as referências a valores funcionais (potencialmente compartilhados) dentro desse pedaço de memória e em outros lugares. Como a memória pode ser simplesmente alocada na pilha, evitando a necessidade de um heap free, o significado real (se houver) é para outros tipos de gerenciamento de recursos.

Portanto, o que isso alcança é separar o gerenciamento de recursos no estilo RAII do gerenciamento de memória, pelo menos no caso em que o RAII se baseia no escopo de aninhamento simples. Você ainda precisa de um coletor de lixo para gerenciamento de memória, mas obtém limpeza determinística automática, segura e oportuna, de outros recursos.

Steve314
fonte
Não vejo por que um GC é necessário em todas as linguagens funcionais. se você tiver uma estrutura RAII estilo C ++, o compilador também poderá usar esse mecanismo. Valores compartilhados não são problema para estruturas RAII (consulte C ++ shared_ptr<>), você ainda mantém a destruição determinística. A única coisa complicada para a RAII são as referências cíclicas; RAII funcionará corretamente se o gráfico de propriedade for um Gráfico Acíclico Dirigido.
MSalters
O fato é que o estilo de programação funcional é praticamente construído em torno de funções de fechamento / lambdas / anônimas. Sem o GC, você não tem a mesma liberdade para usar seus encerramentos, portanto seu idioma se torna significativamente menos funcional.
comingstorm
@comingstorm - o C ++ possui lambdas (a partir do C ++ 11), mas nenhum coletor de lixo padrão. As lambdas também carregam seu ambiente em um fechamento - e os elementos nesse ambiente podem ser passados ​​por referência, bem como a possibilidade de ponteiros serem passados ​​por valor. Mas, como escrevi no meu segundo parágrafo, o C ++ permite a possibilidade de oscilações de ponteiros - é responsabilidade dos programadores (e não dos compiladores ou ambientes de tempo de execução) garantir que isso nunca aconteça.
Steve314
@MSalters - existem custos envolvidos para garantir que nenhum ciclo de referência possa ser criado. Portanto, é pelo menos não trivial responsabilizar o idioma por essa restrição. A atribuição a um ponteiro provavelmente se torna uma operação de tempo não constante. Embora ainda possa ser a melhor opção em alguns casos. A coleta de lixo evita esse problema, com custos diferentes. Responsabilizar o programador é outro. Não há nenhuma razão forte para que ponteiros pendentes devam ser bons em linguagens imperativas, mas não funcionais, mas ainda não recomendo escrever Haskell com ponteiro pendente.
Steve314
Eu argumentaria que o gerenciamento manual de memória significa que você não tem a mesma liberdade de usar os fechamentos C ++ 11 que os fechamentos Lisp ou Haskell. (Eu estou realmente muito interessados em compreender os detalhes desta troca, como eu gostaria de escrever uma sistemas funcionais linguagem de programação ...)
comingstorm
3

Se você considera C ++ uma linguagem funcional (possui lambdas), é um exemplo de linguagem que não usa uma coleta de lixo.

BЈовић
fonte
8
E se você não considerar o C ++ uma linguagem funcional (IMHO não é, embora você possa escrever um programa funcional com ele, também pode escrever alguns programas extremamente funcionais (funcionais ....) com ele)
mattnz
@mattnz Então acho que a resposta não se aplica. Eu não sei o que acontece em outros idiomas (como por exemplo Haskel)
BЈовић
9
Dizendo C ++ é funcional é como dizer que Perl é orientada a objetos ...
Dinâmica
Pelo menos os compiladores c ++ podem verificar os efeitos colaterais. (via const)
tp1 10/03/12
@ tp1 - (1) Espero que isso não volte ao idioma de quem é melhor e (2) isso não é verdade. Primeiro, os efeitos realmente importantes são principalmente E / S. Segundo, mesmo para efeitos na memória mutável, o const não os bloqueia. Mesmo que você assuma que não há possibilidade de subverter o sistema de tipos (geralmente razoável em C ++), há o problema de constância lógica e a palavra-chave C ++ "mutável". Basicamente, você ainda pode ter mutações, apesar da const. Espera-se que você garanta que o resultado ainda seja "logicamente" o mesmo, mas não necessariamente a mesma representação.
Steve314
2

Devo dizer que a pergunta é um pouco mal definida, porque pressupõe que há uma coleção padrão de "linguagens funcionais". Quase toda linguagem de programação suporta alguma quantidade de programação funcional. E, quase toda linguagem de programação suporta alguma quantidade de programação imperativa. Onde traçar a linha para dizer qual é uma linguagem funcional e qual é uma linguagem imperativa, além de guiada por preconceitos culturais e dogmas populares?

Uma maneira melhor de formular a pergunta seria "é possível suportar a programação funcional em uma memória alocada por pilha". A resposta é, como já mencionado, muito difícil. O estilo de programação funcional promove a alocação de estruturas de dados recursivas à vontade, o que requer uma memória heap (seja coleta de lixo ou contagem de referência). No entanto, existe uma técnica de análise de compilador bastante sofisticada chamada análise de memória baseada em região, na qual o compilador pode dividir o heap em grandes blocos que podem ser alocados e desalocados automaticamente, de maneira semelhante à alocação de pilha. A página da Wikipedia lista várias implementações da técnica, para linguagens "funcionais" e "imperativas".

Uday Reddy
fonte
1
A contagem de referência da IMO é a coleta de lixo, e ter um monte não implica que essas sejam as únicas opções de qualquer maneira. Faça mallocs e mfrees usando um heap, mas não possui coletor de lixo (padrão) e apenas contagens de referência se você escrever o código para fazer isso. O C ++ é quase o mesmo - ele possui (como padrão, no C ++ 11) ponteiros inteligentes com contagem de referência integrada, mas você ainda pode fazer o manual novo e excluir se realmente precisar.
Steve314
Um motivo comum para afirmar que a contagem de referência não é coleta de lixo é que ela falha na coleta de ciclos de referência. Isso certamente se aplica a implementações simples (provavelmente incluindo ponteiros inteligentes em C ++ - não verifiquei), mas nem sempre é o caso. Pelo menos uma máquina virtual Java (da IBM, IIRC) usou a contagem de referência como base para sua coleta de lixo.
Steve314