o que pode dar errado no contexto da programação funcional se meu objeto é mutável?

9

Eu posso ver os benefícios de objetos mutáveis ​​vs imutáveis, como objetos imutáveis, que levam muito tempo para solucionar problemas na programação multiencadeada devido ao estado compartilhado e gravável. Pelo contrário, objetos mutáveis ​​ajudam a lidar com a identidade do objeto, em vez de criar uma nova cópia todas as vezes e, assim, também melhoram o desempenho e o uso da memória, especialmente para objetos maiores.

Uma coisa que estou tentando entender é o que pode dar errado em ter objetos mutáveis ​​no contexto da programação funcional. Como um dos pontos que me disseram é que o resultado de chamar funções em ordem diferente não é determinístico.

Estou procurando um exemplo concreto real, onde é muito evidente o que pode dar errado usando objetos mutáveis ​​na programação de funções. Basicamente, se é ruim, é ruim, independentemente do OO ou paradigma de programação funcional, certo?

Creio que abaixo da minha própria declaração responde a essa pergunta. Mas ainda preciso de um exemplo para que eu possa senti-lo mais naturalmente.

OO ajuda a gerenciar a dependência e a escrever programas mais fáceis e de manutenção com a ajuda de ferramentas como encapsulamento, polimorfismo etc.

A programação funcional também tem o mesmo motivo de promover código sustentável, mas usando o estilo que elimina a necessidade de usar ferramentas e técnicas de OO - uma das quais acredito ser a minimização de efeitos colaterais, funções puras etc.

rahulaga_dev
fonte
11
@ Ruben eu diria que a maioria das linguagens funcionais permitem variáveis ​​mutáveis, mas diferem em usá-las, por exemplo, variáveis ​​mutáveis ​​têm um tipo diferente
jk.
11
Eu acho que você pode ter se misturado imutável e mutável no seu primeiro parágrafo?
jk.
11
@jk., ele certamente fez. Editado para corrigir isso.
David Arno
6
A programação funcional do @Ruben é um paradigma. Como tal, não requer uma linguagem de programação funcional. E algumas linguagens fp como F # têm esse recurso .
Christophe
11
@ Ruben não, especificamente, eu estava pensando em Mvars em haskell hackage.haskell.org/package/base-4.9.1.0/docs/… diferentes idiomas têm soluções diferentes, é claro, ou IORefs hackage.haskell.org/package/base-4.11.1.0 /docs/Data-IORef.html embora, é claro, você usasse ambos de mônadas
jk.

Respostas:

7

Eu acho que a importância é melhor demonstrada comparando com uma abordagem OO

por exemplo, digamos que temos um objeto

Order
{
    string Status {get;set;}
    Purchase()
    {
        this.Status = "Purchased";
    }
}

No paradigma OO, o método é anexado aos dados e faz sentido que esses dados sejam alterados pelo método.

var order = new Order();
order.Purchase();
Console.WriteLine(order.Status); // "Purchased"

No Paradigma Funcional, definimos um resultado em termos da função. um pedido adquirido É o resultado da função de compra aplicada a um pedido. Isso implica algumas coisas das quais precisamos ter certeza

var order = new Order(); //this is a 'new order'
var purchasedOrder = purchase(order); // this is a 'purchased order'
Console.WriteLine(order.Status); // "New" order is still a 'new order'

Você esperaria order.Status == "Comprado"?

Isso também implica que nossas funções são idempotentes. ie executá-los duas vezes deve produzir o mesmo resultado cada vez.

var order = new Order(); //new order
var purchasedOrder = purchase(order); //purchased order
var purchasedOrder2 = purchase(order); //another purchased order
var purchasedOrder = purchase(purchasedOrder); //error! cant purchase an order twice

Se o pedido for alterado pela função de compra, o PurchaseOrder2 falhará.

Ao definir as coisas como resultados de funções, nos permite usar esses resultados sem realmente calculá-los. Que, em termos de programação, é adiada para execução.

Isso pode ser útil por si só, mas uma vez que não temos certeza sobre quando uma função realmente acontecerá E estamos bem com isso, podemos aproveitar o processamento paralelo muito mais do que em um paradigma OO.

Sabemos que executar uma função não afetará os resultados de outra função; para que possamos deixar o computador para executá-los na ordem que desejar, usando quantos threads desejar.

Se uma função modifica sua entrada, temos que ter muito mais cuidado com essas coisas.

Ewan
fonte
obrigado !! muito útil. Portanto, a nova implementação de compra seria semelhante Order Purchase() { return new Order(Status = "Purchased") } para que o status seja campo somente leitura. ? Novamente, por que essa prática é mais relevante no contexto do paradigma de programação de funções? Os benefícios que você mencionou também podem ser vistos na programação OO, certo?
Rhulaga_dev 30/04/19
no OO, você esperaria que object.Purchase () modificasse o objeto. Você poderia fazê-lo imutável, mas então por que não mudar para um paradigma funcional completo
Ewan
Eu acho que o problema está tendo que visualizar porque sou um desenvolvedor c # puro que é orientado a objetos pela natureza. Então, o que você diz na linguagem que adota a programação funcional não exigirá que a função 'Purchase ()', que retorna o pedido adquirido, seja anexada a qualquer classe ou objeto, certo?
Rhulaga_dev 30/04
3
você pode escrever c # funcional altere seu objeto para uma estrutura, torne-o imutável e escreva um código Func <Order, Order> Purchase
Ewan
12

A chave para entender por que objetos imutáveis ​​são benéficos não está na tentativa de encontrar exemplos concretos no código funcional. Como a maioria dos códigos funcionais é escrita usando linguagens funcionais, e a maioria das linguagens funcionais é imutável por padrão, a própria natureza do paradigma é projetada para evitar que o que você está procurando aconteça.

O principal a perguntar é: qual é esse benefício da imutabilidade? A resposta é: evita a complexidade. Digamos que temos duas variáveis xe y. Ambos começam com o valor de 1. yembora dobre a cada 13 segundos. Qual será o valor de cada um deles em 20 dias? xserá 1. Isso é fácil. No entanto, seria necessário trabalhar y, pois é muito mais complexo. Que hora do dia em 20 dias? Preciso levar em consideração o horário de verão? A complexidade do yversus xé muito mais.

E isso ocorre no código real também. Toda vez que você adiciona um valor mutante à mistura, isso se torna outro valor complexo para você manter e calcular na sua cabeça ou no papel ao tentar escrever, ler ou depurar o código. Quanto mais complexidade, maior a chance de você cometer um erro e introduzir um bug. O código é difícil de escrever; difícil de ler; difícil de depurar: é difícil acertar o código.

A mutabilidade não é ruim . Um programa com mutabilidade zero não pode ter resultado, o que é bastante inútil. Mesmo que a mutabilidade seja gravar um resultado na tela, disco ou qualquer outra coisa, ele precisa estar lá. O que é ruim é complexidade desnecessária. Uma das maneiras mais simples de reduzir a complexidade é tornar as coisas imutáveis ​​por padrão e torná-las mutáveis ​​quando necessário, devido a razões de desempenho ou funcionais.

David Arno
fonte
4
"uma das maneiras mais simples de reduzir a complexidade é tornar as coisas imutáveis ​​por padrão e torná-las mutáveis ​​quando necessário": Resumo muito bom e conciso.
Giorgio
2
@DavidArno A complexidade que você descreve dificulta o raciocínio do código. Você também tocou nisso quando disse "O código é difícil de escrever; difícil de ler; difícil de depurar; ...". Gosto de objetos imutáveis ​​porque eles tornam muito mais fácil o raciocínio do código, não apenas por mim, mas por observadores que observam sem conhecer o projeto inteiro.
Disassemble-number-5
11
@RahulAgarwal, " Mas por que esse problema se torna mais proeminente no contexto da programação funcional ". Não faz. Eu acho que talvez eu esteja confuso com o que você está perguntando, pois o problema é muito menos proeminente no PF, pois o FP incentiva a imutabilidade, evitando assim o problema.
David Arno
11
@djechlin, " Como seu exemplo de 13 segundos se torna mais fácil de analisar com código imutável? " Ele não pode: yprecisa sofrer mutação; isso é um requisito. Às vezes, precisamos ter um código complexo para atender a requisitos complexos. O que eu estava tentando enfatizar é que complexidade desnecessária deve ser evitada. Os valores mutantes são inerentemente mais complexos que os fixos; portanto, para evitar complexidade desnecessária, apenas modifique valores quando for necessário.
30518 David Arno
3
A mutabilidade cria uma crise de identidade. Sua variável não tem mais uma única identidade. Em vez disso, sua identidade agora depende do tempo. Então, simbolicamente, em vez de um único x, agora temos uma família x_t. Qualquer código que use essa variável agora também precisará se preocupar com o tempo, causando complexidade extra mencionada na resposta.
precisa
8

o que pode dar errado no contexto da programação funcional

As mesmas coisas que podem dar errado na programação não funcional: você pode obter efeitos colaterais inesperados e indesejados , que é uma causa bem conhecida de erros desde a invenção das linguagens de programação com escopo definido.

IMHO, a única diferença real entre programação funcional e não-funcional é que, no código não-funcional, você normalmente espera efeitos colaterais; na programação funcional, não.

Basicamente, se é ruim, é ruim, independentemente do OO ou paradigma de programação funcional, certo?

Claro - efeitos colaterais indesejados são uma categoria de bugs, independentemente do paradigma. O oposto também é verdadeiro - efeitos colaterais deliberadamente usados ​​podem ajudar a lidar com problemas de desempenho e são normalmente necessários para a maioria dos programas do mundo real quando se trata de E / S e de sistemas externos - também independentemente do paradigma.

Doc Brown
fonte
4

Acabei de responder a uma pergunta do StackOverflow que ilustra bem a sua pergunta. O principal problema das estruturas de dados mutáveis ​​é que sua identidade é válida apenas em um instante exato no tempo, portanto as pessoas tendem a se concentrar o máximo possível no pequeno ponto do código em que sabem que a identidade é constante. Neste exemplo em particular, ele faz muito log dentro de um loop for:

for (elem <- rows map (row => s3 map row)) {
  val elem_str = elem.map(_.toString)

  logger.info("verifying the S3 bucket passed from the ctrl table for each App")
  logger.info(s"Checking on App Code: ${elem head}")

  listS3Buckets(elem_str(1), elem_str(2)) match {

    case Some(allBktsInfo) =>
      logger.info(s"App: ${elem_str head} provided the bucket name as: ${elem_str(3)}")
      if (allBktsInfo.exists(x => x.getName == elem_str(3))) {
        logger.info(s"Provided S3 bucket: ${elem_str(3)} exists")
        println(s"s3 ${elem_str(3)} bucket exists")
      } else {
        logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
        logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
        excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
        println(s"s3 bucket ${elem_str(3)} doesn't exists")
    }

    case None =>
      logger.info(s"WARNING: Provided S3 bucket ${elem_str(3)} doesn't exists")
      logger.info(s"WARNING: Dropping the App: ${elem_str.head} from backup schedule")
      excludeList += elem_str.head // If the bucket is invalid then we exclude from backup
}

Quando você está acostumado à imutabilidade, não há medo de que a estrutura de dados seja alterada se você esperar demais, para que você possa executar tarefas que são logicamente separadas à sua vontade, de uma maneira muito mais dissociada:

val (exists, missing) = rows partition bucketExists
missing foreach {row =>
  logger.info(s"WARNING: Provided S3 bucket ${row("s3_primary_bkt_name")} doesn't exist")
  logger.info(s"WARNING: Dropping the App: ${row("app")} from backup schedule")
}
Karl Bielefeldt
fonte
3

A vantagem de usar objetos imutáveis ​​é que, se alguém recebe uma referência a um objeto com uma certa propriedade quando o destinatário a examina e precisa fornecer a algum outro código uma referência a um objeto com a mesma propriedade, pode simplesmente passar ao longo da referência ao objeto, sem levar em consideração quem mais poderia ter recebido a referência ou o que eles poderiam fazer com o objeto [já que não há mais ninguém que possa fazer com o objeto] ou quando o receptor pode examiná-lo [uma vez que todos as propriedades serão as mesmas, independentemente de quando forem examinadas].

Por outro lado, o código que precisa fornecer a alguém uma referência a um objeto mutável que terá uma certa propriedade quando o receptor a examinar (supondo que o próprio receptor não a mude) também precisa saber que nada além do receptor jamais mudará essa propriedade, ou saiba quando o destinatário acessará essa propriedade e saiba que nada mudará essa propriedade até a última vez que o destinatário a examinar.

Eu acho que é muito útil para a programação em geral (não apenas a programação funcional) pensar em objetos imutáveis ​​como se estivessem em três categorias:

  1. Objetos que não podem permitir que nada os altere, mesmo com uma referência. Tais objetos e referências a eles se comportam como valores e podem ser compartilhados livremente.

  2. Objetos que permitiriam ser alterados por código que tenha referências a eles, mas cujas referências nunca serão expostas a nenhum código que realmente os alteraria. Esses objetos encapsulam valores, mas eles só podem ser compartilhados com códigos confiáveis, para não alterá-los ou expô-los ao código que possa funcionar.

  3. Objetos que serão alterados. Esses objetos são melhor visualizados como contêineres e referências a eles como identificadores .

Um padrão útil é geralmente fazer com que um objeto crie um contêiner, preencha-o usando um código confiável para não manter uma referência posteriormente e, em seguida, tenha as únicas referências que já existirem em qualquer lugar do universo, em código que nunca modificará o objeto uma vez preenchido. Embora o contêiner possa ser de um tipo mutável, ele pode ser fundamentado em (*) como se fosse imutável, pois nada jamais o modificaria. Se todas as referências ao contêiner forem mantidas em tipos de invólucros imutáveis ​​que nunca alterem seu conteúdo, esses invólucros poderão ser passados ​​com segurança, como se os dados contidos nele fossem mantidos em objetos imutáveis, pois as referências aos invólucros podem ser compartilhadas e examinadas livremente em a qualquer momento.

(*) No código multithread, pode ser necessário usar "barreiras de memória" para garantir que, antes que qualquer thread possa ver qualquer referência ao wrapper, os efeitos de todas as ações no contêiner sejam visíveis para esse thread, mas esse é um caso especial mencionado aqui apenas para completar.

supercat
fonte
obrigado pela resposta impressionante !! Eu acho que provavelmente a fonte da minha confusão é porque sou do c # background e estou aprendendo "escrevendo código de estilo funcional em c #", que fica por toda parte dizendo evitando objetos mutáveis ​​- mas acho que linguagens que adotam paradigmas de programação funcional promovem (ou reforçam - não tenho certeza se a aplicação for correta) imutabilidade.
Rahulaga_dev 30/04
@RahulAgarwal: É possível ter referências a um objeto encapsular um valor cujo significado não seja afetado pela existência de outras referências ao mesmo objeto, ter uma identidade que as associe a outras referências ao mesmo objeto, ou a nenhuma delas. Se o estado da palavra real for alterado, o valor ou a identidade de um objeto associado a esse estado poderá ser constante, mas não ambos - será preciso mudar. Os US $ 50.000 é o que deve fazer o que.
Supercat
1

Como já foi mencionado, o problema com o estado mutável é basicamente uma subclasse do maior problema de efeitos colaterais , onde o tipo de retorno de uma função não descreve com precisão o que a função realmente faz, porque, neste caso, também faz mutação de estado. Esse problema foi solucionado por algumas novas linguagens de pesquisa, como a F * ( http://www.fstar-lang.org/tutorial/ ). Essa linguagem cria um sistema de efeitos semelhante ao sistema de tipos, onde uma função não apenas declara estaticamente seu tipo, mas também seus efeitos. Dessa forma, os chamadores da função estão cientes de que uma mutação de estado pode ocorrer ao chamar a função e esse efeito é propagado para os chamadores.

Aaron M. Eshbach
fonte