Decidi assumir a tarefa de aprender programação funcional. Até agora tem sido uma explosão, e eu 'vi a luz' por assim dizer. Infelizmente, eu realmente não conheço nenhum programador funcional do qual possa responder perguntas. Apresentando o Stack Exchange.
Estou fazendo um curso de desenvolvimento de web / software, mas meu instrutor não está familiarizado com programação funcional. Ele está bem comigo, e me pediu para ajudá-lo a entender como funciona para que ele possa ler melhor meu código.
Decidi que a melhor maneira de fazer isso seria ilustrando uma função matemática simples, como aumentar um valor a uma potência. Em teoria, eu poderia facilmente fazer isso com uma função predefinida, mas isso derrotaria o objetivo de um exemplo.
Enfim, estou tendo alguma dificuldade em descobrir como manter um valor. Como esta é uma programação funcional, não posso mudar de variável. Se eu codificasse isso imperativamente, ficaria assim:
(A seguir, todos os pseudocódigos)
f(x,y) {
int z = x;
for(int i = 0, i < y; i++){
x = x * z;
}
return x;
}
Na programação funcional, eu não tinha certeza. Isto é o que eu vim com:
f(x,y,z){
if z == 'null',
f(x,y,x);
else if y > 1,
f(x*z,y-1,z);
else
return x;
}
Isto está certo? Eu preciso manter um valor, z
nos dois casos, mas não tinha certeza de como fazer isso na programação de funções. Em teoria, do jeito que eu fiz isso funciona, mas eu não tinha certeza se estava 'certo'. Tem algum jeito melhor de fazer isso?
y
.Respostas:
Primeiro de tudo, parabéns por "ver a luz". Você fez do mundo do software um lugar melhor, expandindo seus horizontes.
Segundo, honestamente, não há como um professor que não entende de programação funcional ser capaz de dizer algo útil sobre o seu código, além de comentários triviais, como "a indentação parece". Isso não é tão surpreendente em um curso de desenvolvimento web, pois a maioria do desenvolvimento web é feita usando HTML / CSS / JavaScript. Dependendo do quanto você realmente se preocupa em aprender o desenvolvimento da Web, você pode se esforçar para aprender as ferramentas que seu professor está ensinando (por mais doloroso que seja isso - eu sei por experiência própria).
Para abordar a questão declarada: se seu código imperativo usa um loop, é provável que seu código funcional seja recursivo.
Observe que esse algoritmo é realmente mais ou menos idêntico ao código imperativo. De fato, pode-se considerar o loop acima como um açúcar sintático para processos recursivos iterativos.
Como uma observação lateral, não há necessidade de um valor
z
no seu código imperativo ou funcional. Você deveria ter escrito sua função imperativa da seguinte maneira:em vez de alterar o significado da variável
x
.fonte
pow
não está certo. Como é,pow 3 3
retorna81
, em vez de27
. Deve serelse x * pow x (y-1).
Este é realmente apenas um adendo à resposta de Gardenhead, mas eu gostaria de ressaltar que há um nome para o padrão que você está vendo: dobrar.
Na programação funcional, uma dobra é uma maneira de combinar uma série de valores que "lembram" um valor entre cada operação. Considere adicionar imperativamente uma lista de números:
Tomamos uma lista de valores
xs
e um estado inicial de0
(representado portotal
neste caso). Então, para cadax
noxs
, nós combinamos esse valor com o estado atual de acordo com alguma operação combinando (neste caso disso), e usar o resultado como o novo estado. Em essência,sum_all([1, 2, 3])
é equivalente a(3 + (2 + (1 + 0)))
. Esse padrão pode ser extraído em uma função de ordem superior , uma função que aceita funções como argumentos:Essa implementação de
fold
ainda é imperativa, mas também pode ser feita recursivamente:Expressa em termos de dobra, sua função é simplesmente:
... onde
repeat(x, n)
retorna uma lista den
cópias dex
.Muitas linguagens, particularmente aquelas voltadas para a programação funcional, oferecem dobragem em sua biblioteca padrão. Até o Javascript fornece sob o nome
reduce
. Em geral, se você estiver usando a recursão para "lembrar" um valor em algum tipo de loop, provavelmente desejará uma dobra.fonte
fold(repeat(base, power), 1, *)
scan
é essencialmente umfold
onde, em vez de apenas combinar a lista de valores em um valor, ele é combinado e cada valor intermediário é cuspido ao longo do caminho, produzindo uma lista de todos os estados intermediários que a dobra criou em vez de apenas produzir o estado final. É implementável em termos defold
(toda operação de loop é).combiner_func
é um argumento esum_all
está passando uma função anônima (esse é olambda
bit - ele cria um valor de função sem nomeá-lo) que define como ele deseja combinar dois itens.Esta é uma resposta suplementar para ajudar a explicar mapas e dobras. Para os exemplos abaixo, usarei esta lista. Lembre-se, esta lista é imutável, portanto nunca mudará:
Vou usar números nos meus exemplos porque eles levam a códigos de fácil leitura. Lembre-se, no entanto, as dobras podem ser usadas para qualquer coisa que um loop imperativo tradicional possa ser usado.
Um mapa pega uma lista de algo e uma função e retorna uma lista que foi modificada usando a função. Cada item é passado para a função e se torna o que a função retornar.
O exemplo mais fácil disso é apenas adicionar um número a cada número em uma lista. Vou usar o pseudocódigo para torná-lo independente de idioma:
Se você imprimisse
numbers2
, veria[3, 4, 5, 6, 7]
qual é a primeira lista com 2 adicionadas a cada elemento. Observe que a funçãoadd-two
foi atribuídamap
para uso.As dobras s são semelhantes, exceto que a função que você precisa fornecer a elas deve receber 2 argumentos. O primeiro argumento é geralmente o acumulador (na dobra esquerda, que é o mais comum). O acumulador são os dados que são transmitidos durante o loop. O segundo argumento é o item atual da lista; assim como acima para a
map
função.Se você imprimisse
sum
, veria a soma da lista de números: 15.Aqui estão os argumentos para
fold
fazer:Esta é a função que estamos dando a dobra. A dobra passará a função do acumulador atual e o item atual da lista. O que quer que a função retorne se tornará o novo acumulador, que será passado para a função na próxima vez. É assim que você "lembra" dos valores quando faz um loop no estilo FP. Eu dei uma função que pega 2 números e os adiciona.
Este é o acumulador inicial; o que o acumulador inicia como antes de qualquer item da lista ser processado. Quando você soma números, qual é o total antes de adicionar números? 0, que eu passei como o segundo argumento.
Por fim, como no mapa, também passamos na lista de números para processamento.
Se as dobras ainda não fazem sentido, considere isso. Quando você escreve:
Você está basicamente colocando a função passada entre cada item na lista e adicionando o acumulador inicial à esquerda ou à direita (dependendo se é uma dobra esquerda ou direita), então:
Torna-se:
O que equivale a 15.
Use a
map
quando desejar transformar uma lista em outra lista, do mesmo tamanho.Use a
fold
quando desejar transformar uma lista em um único valor, como somar uma lista de números.Como o @Jorg apontou nos comentários, o "valor único" não precisa ser algo simples como um número; pode ser qualquer objeto único, incluindo uma lista ou uma tupla! O jeito que eu realmente clicou nas dobras para mim foi definir um mapa em termos de dobra. Observe como o acumulador é uma lista:
Honestamente, depois de entender cada um, você perceberá que quase todos os ciclos podem ser substituídos por uma dobra ou um mapa.
fonte
i
nunca serem definidas), mas acho que você tem a ideia certa. Um problema com o seu exemplo é: a função (x
) passa apenas um elemento da lista por vez, não a lista inteira. Na primeira vez em quex
é chamado, é passado seu acumulador inicial (y
) como primeiro argumento e o primeiro elemento como segundo argumento. Na próxima vez em que for executado,x
será passado o novo acumulador à esquerda (o que forx
retornado na primeira vez) e o segundo elemento da lista como seu segundo argumento.fold
quando quiser transformar uma lista em um único valor (como somar uma lista de números)." - Eu só quero observar que esse "valor único" pode ser arbitrariamente complexo ... incluindo uma lista! Na verdade,fold
é um método geral de iteração, ele pode fazer tudo o que a iteração pode fazer. Por exemplo,map
pode ser expresso trivialmente comofunc map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)
Aqui, o "valor único" calculado porfold
é na verdade uma lista.fold
. E não precisa ser uma lista, toda coleção que pode ser expressa como vazia / não vazia serve. O que basicamente significa que qualquer iterador fará. (Eu acho que jogando a palavra "catamorphism" lá seria demais para a introdução de um novato, embora :-D)É difícil encontrar bons problemas que não possam ser resolvidos com a funcionalidade de criação. E se estiver embutido, deve ser usado como exemplo de bom estilo na linguagem x.
No haskell, por exemplo, você já tem a função
(^)
no Prelude.Ou se você quiser fazer isso de forma mais programática
product (replicate y x)
O que estou dizendo é que é difícil mostrar os pontos fortes de um estilo / idioma se você não usar os recursos que ele fornece. No entanto, pode ser um bom passo para mostrar como funciona nos bastidores, mas acho que você deve codificar da melhor maneira em qualquer idioma que estiver usando e depois ajudar a pessoa de lá a entender o que está acontecendo, se necessário.
fonte
product
é apenas uma função de atalho parafold
a multiplicação como função e 1 como argumento inicial, e quereplicate
é uma função que produz um iterador (ou lista; como observei). acima dos dois são essencialmente indistinguíveis em haskell) que fornece um número determinado de saídas idênticas. Agora deve ser fácil entender como essa implementação faz a mesma coisa que a resposta de @ Jack acima, apenas usando versões predefinidas de casos especiais das mesmas funções para torná-la mais sucinta.