Os fechamentos são considerados um estilo funcional impuro?


33

Os fechamentos são considerados impuros na programação funcional?

Parece que geralmente se pode evitar fechamentos passando valores diretamente para uma função. Portanto, os fechamentos devem ser evitados sempre que possível?

Se eles são impuros, e eu estou correto ao afirmar que eles podem ser evitados, por que tantas linguagens de programação funcionais suportam fechamentos?

Um dos critérios para uma função pura é que "A função sempre avalia o mesmo valor de resultado, dados os mesmos valores de argumento ".

Suponha

f: x -> x + y

f(3)nem sempre dará o mesmo resultado. f(3)depende do valor do yqual não é um argumento f. Portanto, fnão é uma função pura.

Como todos os fechamentos dependem de valores que não são argumentos, como é possível que qualquer fechamento seja puro? Sim, em teoria, o valor fechado pode ser constante, mas não há como saber isso apenas olhando o código-fonte da própria função.

Onde isso me leva é que a mesma função pode ser pura em uma situação, mas impura em outra. Nem sempre se pode determinar se uma função é pura ou não, estudando seu código-fonte. Em vez disso, pode ser necessário considerá-lo no contexto de seu ambiente no momento em que está sendo chamado antes que essa distinção possa ser feita.

Estou pensando nisso corretamente?


6
Uso fechos o tempo todo em Haskell, e Haskell é o mais puro possível.
9788 Thomas Eding

5
Em uma linguagem funcional pura, ynão pode mudar, portanto a saída de f(3)sempre será a mesma.
Lily Chung

4
yfaz parte da definição de f, embora não esteja explicitamente marcado como uma entrada para f- ainda é o caso fdefinido em termos de y(poderíamos denotar a função f_y, para tornar a dependência yexplícita) e, portanto, alterar yfornece uma função diferente . A função específica f_ydefinida para um particular yé muito pura. (Por exemplo, as duas funções f: x -> x + 3e f: x -> x + 5são diferentes funções, e ambos pura, mesmo que aconteceu a usar a mesma letra para denotar-los.)
ShreevatsaR

Respostas:


26

A pureza pode ser medida por duas coisas:

  1. A função sempre retorna a mesma saída, dada a mesma entrada; isto é, é referencialmente transparente?
  2. A função modifica algo fora de si, ou seja, tem efeitos colaterais?

Se a resposta para 1 for sim e a resposta para 2 for não, então a função é pura. Os fechamentos apenas tornam uma função impura se você modificar a variável fechada.


O primeiro item não é determinismo? Ou isso também faz parte da pureza? Não estou muito familiarizado com o conceito de "pureza" no contexto da programação.

4
@ JimmyHoffa: Não necessariamente. Você pode colocar a saída de um timer de hardware em uma função e nada fora da função é modificado.
21714 Robert

1
@RobertHarvey Isso é tudo sobre como definimos entradas para uma função? Minha citação da wikipedia foca nos argumentos das funções, enquanto você considera adicionalmente as variáveis ​​fechadas como entradas.
precisa saber é o seguinte

8
@ user2179977: a menos que sejam mutáveis, você não deve considerar as variáveis ​​de fechamento como entradas adicionais para a função. Em vez disso, você deve considerar o fechamento em si uma função e uma função diferente quando fecha sobre um valor diferente de y. Assim, por exemplo, definimos uma função gque g(y)é ela mesma a função x -> x + y. Então gé uma função de números inteiros que retorna funções, g(3)é uma função de números inteiros que retorna números inteiros e g(2)é uma função diferente de números inteiros que retorna números inteiros. Todas as três funções são puras.
18710 Steve Jobs

1
@Darkhogg: Sim. Veja minha atualização.
9788 Robert Harvey

10

Os fechamentos aparecem Lambda Calculus, que é a forma mais pura de programação funcional possível, então eu não os chamaria de "impuros" ...

Os fechamentos não são "impuros" porque as funções em linguagens funcionais são cidadãos de primeira classe - isso significa que podem ser tratados como valores.

Imagine o seguinte (pseudocódigo):

foo(x) {
    let y = x + 1
    ...
}

yé um valor. Seu valor depende x, mas xé imutável, portanto y, o valor também é imutável. Podemos chamar foomuitas vezes com argumentos diferentes que produzirão ys diferentes , mas ytodos eles vivem em escopos diferentes e dependem de xs diferentes, para que a pureza permaneça intacta.

Agora vamos mudar:

bar(x) {
    let y(z) = x + z
    ....
}

Aqui estamos usando um fechamento (estamos fechando sobre x), mas é exatamente o mesmo que em foo- chamadas diferentes para barcom argumentos diferentes criam valores diferentes de y(lembre-se - funções são valores) que são todos imutáveis, portanto a pureza permanece intacta.

Além disso, observe que os fechamentos têm um efeito muito semelhante ao curry:

adder(a)(b) {
    return a + b
}
baz(x) {
    let y = adder(x)
    ...
}

baznão é realmente diferente de bar- em ambos, criamos um valor de função nomeado yque retorna seu argumento mais x. De fato, no Lambda Calculus você usa fechamentos para criar funções com vários argumentos - e ainda não é impuro.


9

Outros abordaram bem a questão geral em suas respostas, portanto, examinarei apenas a confusão que você sinaliza em sua edição.

O fechamento não se torna uma entrada da função, mas 'entra' no corpo da função. Para ser mais concreto, uma função refere-se a um valor no escopo externo de seu corpo.

Você tem a impressão de que ela torna a função impura. Esse não é o caso, em geral. Na programação funcional, os valores são imutáveis ​​na maioria das vezes . Isso também se aplica ao valor fechado.

Digamos que você tenha um código como este:

let make y =
    fun x -> x + y

Chamar make 3e make 4fornecerá duas funções com encerramentos sobre makeo yargumento. Um deles retornará x + 3, o outro x + 4. No entanto, são duas funções distintas e ambas são puras. Eles foram criados usando a mesma makefunção, mas é isso.

Observe na maioria das vezes alguns parágrafos atrás.

  1. No Haskell, que é puro, você pode fechar apenas sobre valores imutáveis. Não há estado mutável para fechar. Você tem certeza de obter uma função pura dessa maneira.
  2. Em linguagens funcionais impuras, como F #, você pode fechar as células e os tipos de referência e obter uma função impura em vigor. Você está certo no sentido de que precisa rastrear o escopo dentro do qual a função é definida para saber se é pura ou não. Você pode facilmente saber se um valor é mutável nesses idiomas, portanto, isso não é um problema.
  3. Em linguagens OOP que oferecem suporte a fechamentos, como C # e JavaScript, a situação é semelhante a linguagens funcionais impuras, mas o rastreamento do escopo externo fica mais complicado, pois as variáveis ​​são mutáveis ​​por padrão.

Observe que, para 2 e 3, esses idiomas não oferecem garantias sobre pureza. A impureza não é propriedade do fechamento, mas da própria linguagem. Os fechamentos não mudam muito a imagem por si mesmos.


1
Você pode absolutamente fechar valores mutáveis ​​em Haskell, mas isso seria anotado com a mônada de E / S.
precisa saber é o seguinte

1
@jozefg não, você fecha um IO Avalor imutável e seu tipo de fechamento é IO (B -> C)ou algo assim. A pureza é mantida
Caleth

5

Normalmente, peço que você esclareça sua definição de "impuro", mas, neste caso, isso realmente não importa. Supondo que você esteja contrastando com o termo puramente funcional , a resposta é "não", porque não há nada sobre fechamentos que seja inerentemente destrutivo. Se o seu idioma fosse puramente funcional sem encerramentos, ainda seria puramente funcional com os encerramentos. Se, em vez disso, você quer dizer "não funcional", a resposta ainda é "não"; fechamentos facilitam a criação de funções.

Parece que geralmente se pode evitar fechamentos passando dados diretamente para uma função.

Sim, mas sua função teria mais um parâmetro e isso mudaria de tipo. Os fechamentos permitem criar funções baseadas em variáveis sem adicionar parâmetros. Isso é útil quando você tem, digamos, uma função que usa 2 argumentos e deseja criar uma versão dela que usa apenas 1 argumento.

EDIT: No que diz respeito à sua própria edição / exemplo ...

Suponha

f: x -> x + y

f (3) nem sempre dará o mesmo resultado. f (3) depende do valor de y que não é um argumento de f. Assim, f não é uma função pura.

Depende é a escolha errada da palavra aqui. Citando o mesmo artigo da Wikipedia que você fez:

Na programação de computadores, uma função pode ser descrita como uma função pura se ambas as instruções sobre a função se mantiverem:

  1. A função sempre avalia o mesmo valor de resultado, dados os mesmos valores de argumento. O valor do resultado da função não pode depender de nenhuma informação ou estado oculto que possa mudar à medida que a execução do programa prossegue ou entre diferentes execuções do programa, nem pode depender de nenhuma entrada externa de dispositivos de E / S.
  2. A avaliação do resultado não causa nenhum efeito colateral ou saída semanticamente observável, como mutação de objetos mutáveis ​​ou saída para dispositivos de E / S.

Supondo que yseja imutável (que geralmente é o caso em linguagens funcionais), a condição 1 é satisfeita: para todos os valores de x, o valor de f(x)não muda. Isso deve ficar claro pelo fato de que ynão é diferente de uma constante e x + 3é puro. Também está claro que não há mutação ou E / S acontecendo.


3

Muito rapidamente: uma substituição é "referencialmente transparente" se "substituir igual leva a gostar" e uma função é "pura" se todos os seus efeitos estiverem contidos em seu valor de retorno. Ambos podem ser precisos, mas é vital notar que eles não são idênticos nem que um implica o outro.

Agora vamos falar sobre fechamentos.

"Encerramentos" chatos (principalmente puros)

Os fechamentos ocorrem porque, ao avaliarmos um termo lambda, interpretamos variáveis ​​(vinculadas) como pesquisas de ambiente. Assim, quando retornarmos um termo lambda como resultado de uma avaliação, as variáveis ​​dentro dele "fecharão" os valores que eles assumiram quando ele foi definido.

No cálculo lambda simples, isso é trivial e toda a noção simplesmente desaparece. Para demonstrar isso, aqui está um interpretador de cálculo lambda relativamente leve:

-- untyped lambda calculus values are functions
data Value = FunVal (Value -> Value)

-- we write expressions where variables take string-based names, but we'll
-- also just assume that nobody ever shadows names to avoid having to do
-- capture-avoiding substitutions

type Name = String

data Expr
  = Var Name
  | App Expr Expr
  | Abs Name Expr

-- We model the environment as function from strings to values, 
-- notably ignoring any kind of smooth lookup failures
type Env = Name -> Value

-- The empty environment
env0 :: Env
env0 _ = error "Nope!"

-- Augmenting the environment with a value, "closing over" it!
addEnv :: Name -> Value -> Env -> Env
addEnv nm v e nm' | nm' == nm = v
                  | otherwise = e nm

-- And finally the interpreter itself
interp :: Env -> Expr -> Value
interp e (Var name) = e name          -- variable lookup in the env
interp e (App ef ex) =
  let FunVal f = interp e ef
      x        = interp e ex
  in f x                              -- application to lambda terms
interp e (Abs name expr) =
  -- augmentation of a local (lexical) environment
  FunVal (\value -> interp (addEnv name value e) expr)

A parte importante a ser observada é addEnvquando aumentamos o ambiente com um novo nome. Essa função é chamada apenas "dentro" do Abstermo de tração interpretado (termo lambda). O ambiente é "procurado" sempre que avaliamos um Vartermo e, portanto, eles Vardecidem o que quer que seja Namereferido no Envque foi capturado pela Abstração que contém o Var.

Agora, novamente, em termos simples de LC, isso é chato. Isso significa que variáveis ​​ligadas são apenas constantes, na medida em que alguém se importa. Eles são avaliados direta e imediatamente como os valores que eles denotam no ambiente, com o escopo lexicamente até aquele momento.

Isso também é (quase) puro. O único significado de qualquer termo em nosso cálculo lambda é determinado pelo seu valor de retorno. A única exceção é o efeito colateral da não rescisão, que é incorporado pelo termo Omega:

-- in simple LC syntax:
--
-- (\x -> (x x)) (\x -> (x x))
omega :: Expr
omega = App (Abs "x" (App (Var "x") 
                          (Var "x")))
            (Abs "x" (App (Var "x") 
                          (Var "x")))

Fechamentos interessantes (impuros)

Agora, para certos antecedentes, os fechamentos descritos na LC simples acima são chatos porque não há noção de poder interagir com as variáveis ​​que encerramos. Em particular, a palavra "encerramento" tende a invocar código como o seguinte Javascript

> function mk_counter() {
  var n = 0;
  return function incr() {
    return n += 1;
  }
}
undefined

> var c = mk_counter()
undefined
> c()
1
> c()
2
> c()
3

Isso demonstra que fechamos a nvariável na função interna incre a chamada incrinterage significativamente com essa variável. mk_counteré puro, mas incré decididamente impuro (e também não é referencialmente transparente).

O que difere entre essas duas instâncias?

Noções de "variável"

Se observarmos o que substituição e abstração significam no sentido claro de LC, notamos que elas são decididamente claras. Variáveis ​​literalmente nada mais são do que pesquisas imediatas no ambiente. A abstração do Lambda nada mais é do que criar um ambiente aumentado para avaliar a expressão interna. Não há espaço neste modelo para o tipo de comportamento que vimos com mk_counter/ incrporque não há variação permitida.

Para muitos, esse é o cerne do que "variável" significa - variação. No entanto, os semanticistas gostam de distinguir entre o tipo de variável usada na LC e o tipo de "variável" usada no Javascript. Para fazer isso, eles tendem a chamar o último de "célula mutável" ou "slot".

Essa nomenclatura segue o longo uso histórico de "variável" em matemática, onde significava algo mais parecido com "desconhecido": a expressão (matemática) "x + x" não permite xvariar ao longo do tempo, mas sim ter significado independentemente do valor (único, constante) xleva.

Assim, dizemos "slot" para enfatizar a capacidade de colocar valores em um slot e removê-los.

Para aumentar ainda mais a confusão, em Javascript esses "slots" têm a mesma aparência de variáveis: escrevemos

var x;

para criar um e depois quando escrevemos

x;

isso indica que estamos pesquisando o valor atualmente armazenado nesse slot. Para tornar isso mais claro, linguagens puras tendem a pensar em slots como nomes (matemáticos, cálculo lambda). Nesse caso, devemos rotular explicitamente quando obtemos ou colocamos um slot. Essa notação tende a parecer

-- create a fresh, empty slot and name it `x` in the context of the 
-- expression E
let x = newSlot in E

-- look up the value stored in the named slot named `x`, return that value
get x

-- store a new value, `v`, in the slot named `x`, return the slot
put x v

A vantagem dessa notação é que agora temos uma firme distinção entre variáveis ​​matemáticas e slots mutáveis. Variáveis ​​podem usar slots como seus valores, mas o slot específico nomeado por uma variável é constante em todo o seu escopo.

Usando essa notação, podemos reescrever o mk_counterexemplo (desta vez em uma sintaxe semelhante a Haskell, embora semântica decididamente não semelhante a Haskell):

mkCounter = 
  let x = newSlot 
  in (\() -> let old = get x 
             in get (put x (old + 1)))

Neste caso, estamos usando procedimentos que manipulam esse slot mutável. Para implementá-lo, precisamos fechar não apenas um ambiente constante de nomes como xtambém um ambiente mutável que contenha todos os slots necessários. Isso está mais próximo da noção comum de "fechamento" que as pessoas amam tanto.

Mais uma vez, mkCounteré muito impuro. Também é muito referencialmente opaco. Mas observe que os efeitos colaterais não surgem da captura ou fechamento do nome, mas da captura da célula mutável e das operações de efeito colateral, como gete put.

Por fim, acho que esta é a resposta final para sua pergunta: a pureza não é afetada pela captura de variáveis ​​(matemáticas), mas pelas operações de efeito colateral executadas em slots mutáveis ​​denominados por variáveis ​​capturadas.

É apenas em linguagens que não tentam se aproximar da LC ou não tentam manter a pureza que esses dois conceitos são tão frequentemente conflitantes que levam à confusão.


1

Não, os fechamentos não fazem com que uma função seja impura, desde que o valor fechado seja constante (nem alterado pelo fechamento nem por outro código), como é o caso usual na programação funcional.

Observe que, embora você sempre possa transmitir um valor como argumento, normalmente não pode fazê-lo sem uma quantidade considerável de dificuldade. Por exemplo (coffeescript):

closedValue = 42
return (arg) -> console.log "#{closedValue} #{arg}"

Por sua sugestão, você pode simplesmente retornar:

return (arg, closedValue) -> console.log "#{closedValue} #{arg}"

Como essa função não está sendo chamada neste momento, apenas definida , você deve encontrar uma maneira de passar o valor desejado para closedValueo ponto em que a função está realmente sendo chamada. Na melhor das hipóteses, isso cria muito acoplamento. Na pior das hipóteses, você não controla o código no ponto de chamada, portanto é efetivamente impossível.

As bibliotecas de eventos em idiomas que não oferecem suporte a fechamentos geralmente oferecem outra maneira de passar dados arbitrários de volta para o retorno de chamada, mas não é bonito e cria muita complexidade para o mantenedor da biblioteca e os usuários da biblioteca.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.