Acabei de aprender sobre curry e, apesar de entender o conceito, não vejo grande vantagem em usá-lo.
Como um exemplo trivial, uso uma função que adiciona dois valores (escritos em ML). A versão sem curry seria
fun add(x, y) = x + y
e seria chamado como
add(3, 5)
enquanto a versão ao curry é
fun add x y = x + y
(* short for val add = fn x => fn y=> x + y *)
e seria chamado como
add 3 5
Parece-me que é apenas o açúcar sintático que remove um conjunto de parênteses da definição e da chamada da função. Eu vi o currying listado como um dos recursos importantes de uma linguagem funcional, e estou um pouco desapontado com isso no momento. O conceito de criar uma cadeia de funções que consome cada um um único parâmetro, em vez de uma função que utiliza uma tupla, parece bastante complicado de usar para uma simples mudança de sintaxe.
A sintaxe um pouco mais simples é a única motivação para currying, ou estou perdendo outras vantagens que não são óbvias no meu exemplo muito simples? O curry é apenas açúcar sintático?
fonte
Respostas:
Com funções ao curry, você obtém uma reutilização mais fácil de funções mais abstratas, desde que se especialize. Digamos que você tenha uma função de adição
e que você deseja adicionar 2 a todos os membros de uma lista. Em Haskell, você faria o seguinte:
Aqui a sintaxe é mais clara do que se você tivesse que criar uma função
add2
ou se você tivesse que criar uma função lambda anônima:
Também permite abstrair-se de diferentes implementações. Digamos que você tenha duas funções de pesquisa. Um de uma lista de pares chave / valor e uma chave para um valor e outro de um mapa de chaves para valores e uma chave para um valor, assim:
Em seguida, você pode criar uma função que aceite uma função de pesquisa de Chave para Valor. Você pode passar qualquer uma das funções de pesquisa acima, parcialmente aplicadas com uma lista ou um mapa, respectivamente:
Concluindo: o curry é bom, porque permite a você especializar / aplicar parcialmente funções usando uma sintaxe leve e depois passar essas funções parcialmente aplicadas para funções de ordem superior, como
map
oufilter
. As funções de ordem superior (que assumem funções como parâmetros ou as produzem como resultados) são o pão e a manteiga da programação funcional, e as funções de currying e parcialmente aplicadas permitem que as funções de ordem superior sejam usadas de maneira muito mais eficaz e concisa.fonte
A resposta prática é que o curry facilita muito a criação de funções anônimas. Mesmo com uma sintaxe lambda mínima, é uma vitória; comparar:
Se você tem uma sintaxe lambda feia, é ainda pior. (Estou olhando para você, JavaScript, Scheme e Python.)
Isso se torna cada vez mais útil à medida que você utiliza mais e mais funções de ordem superior. Embora eu use mais funções de ordem superior no Haskell do que em outros idiomas, descobri que realmente uso a sintaxe lambda menos porque algo como dois terços do tempo, o lambda seria apenas uma função parcialmente aplicada. (E na maioria das vezes extraí-o em uma função nomeada.)
Mais fundamentalmente, nem sempre é óbvio qual versão de uma função é "canônica". Por exemplo, pegue
map
. O tipo demap
pode ser escrito de duas maneiras:Qual é o "correto"? É realmente difícil de dizer. Na prática, a maioria dos idiomas usa o primeiro - o mapa pega uma função e uma lista e retorna uma lista. No entanto, fundamentalmente, o que o mapa realmente faz é mapear funções normais para listar funções - ele pega uma função e retorna uma função. Se o mapa é curry, você não precisa responder a essa pergunta: ele faz as duas coisas , de uma maneira muito elegante.
Isso se torna especialmente importante depois que você generaliza
map
para outros tipos que não a lista.Além disso, curry realmente não é muito complicado. Na verdade, é uma simplificação sobre o modelo que a maioria das linguagens usa: você não precisa de nenhuma noção de funções de vários argumentos inseridos em sua linguagem. Isso também reflete o cálculo lambda subjacente mais de perto.
Obviamente, as linguagens no estilo ML não têm a noção de vários argumentos em forma de curry ou não-curry. Na
f(a, b, c)
verdade, a sintaxe corresponde à passagem da tupla(a, b, c)
paraf
, portanto,f
apenas assume argumentos. Essa é realmente uma distinção muito útil que eu gostaria que outras línguas fizessem, porque torna muito natural escrever algo como:Você não pode fazer isso facilmente com linguagens que têm a ideia de vários argumentos!
fonte
A currying pode ser útil se você tiver uma função que está passando como um objeto de primeira classe e não receber todos os parâmetros necessários para avaliá-la em um único local no código. Você pode simplesmente aplicar um ou mais parâmetros quando obtê-los e passar o resultado para outro pedaço de código que possui mais parâmetros e concluir a avaliação.
O código para fazer isso será mais simples do que se você precisasse reunir todos os parâmetros primeiro.
Além disso, existe a possibilidade de mais reutilização de código, uma vez que funções que usam um único parâmetro (outra função ao curry) não precisam corresponder tão especificamente a todos os parâmetros.
fonte
A principal motivação (pelo menos inicialmente) para o curry não era prática, mas teórica. Em particular, o currying permite que você obtenha efetivamente funções de múltiplos argumentos sem definir semântica para elas ou definir semântica para produtos. Isso leva a uma linguagem mais simples, com tanta expressividade quanto a outra linguagem mais complicada e, portanto, é desejável.
fonte
(Vou dar exemplos em Haskell.)
Ao usar linguagens funcionais, é muito conveniente que você possa aplicar parcialmente uma função. Como em Haskell,
(== x)
é uma função que retornaTrue
se seu argumento for igual a um determinado termox
:sem currying, teríamos um código um pouco menos legível:
Isso está relacionado à programação tácita (veja também o estilo Pointfree no wiki da Haskell). Esse estilo não se concentra nos valores representados pelas variáveis, mas na composição de funções e como a informação flui através de uma cadeia de funções. Podemos converter nosso exemplo em um formulário que não usa variáveis:
Aqui vemos
==
como uma função dea
paraa -> Bool
eany
como uma função dea -> Bool
para[a] -> Bool
. Simplesmente compondo-os, obtemos o resultado. Tudo isso graças ao curry.O inverso, sem currying, também é útil em algumas situações. Por exemplo, digamos que queremos dividir uma lista em duas partes - elementos menores que 10 e o restante e concatenar essas duas listas. A divisão da lista é feita por (aqui também usamos curry ). O resultado é de tipo . Em vez de extrair o resultado em sua primeira e segunda parte e combiná-los usando , nós podemos fazer isso diretamente pelo uncurrying como
partition
(< 10)
<
([Int],[Int])
++
++
De fato,
(uncurry (++) . partition (< 10)) [4,12,11,1]
avalia como[4,1,12,11]
.Há também vantagens teóricas importantes:
(a, b) -> c
paraa -> (b -> c)
significa que o resultado da última função é do tipob -> c
. Em outras palavras, o resultado é uma função.fonte
mem x lst = any (\y -> y == x) lst
? (Com uma barra invertida).Curry não é apenas açúcar sintático!
Considere as assinaturas de tipo de
add1
(não acumulado) eadd2
(com curry):(Nos dois casos, os parênteses na assinatura de tipo são opcionais, mas os incluímos por uma questão de clareza.)
add1
é uma função que recebe um 2-tuplo deint
eint
e retorna umint
.add2
é uma função que recebe umint
e retorna outra função que, por sua vez, recebe umint
e retorna umint
.A diferença essencial entre os dois se torna mais visível quando especificamos o aplicativo de funções explicitamente. Vamos definir uma função (sem curry) que aplique seu primeiro argumento ao seu segundo argumento:
Agora podemos ver a diferença entre
add1
eadd2
mais claramente.add1
é chamado com uma tupla de 2:mas
add2
é chamado com umint
e, em seguida, seu valor de retorno é chamado com outroint
:EDIT: O benefício essencial de currying é que você obtenha aplicação parcial gratuitamente. Digamos que você desejou uma função do tipo
int -> int
(digamos,map
sobre ela em uma lista) que adicionou 5 ao seu parâmetro. Você poderia escreveraddFiveToParam x = x+5
ou fazer o equivalente a um lambda embutido, mas também poderia muito mais facilmente (especialmente em casos menos triviais que este)add2 5
!fonte
Curry é apenas açúcar sintático, mas você está entendendo um pouco o que o açúcar faz, eu acho. Tomando o seu exemplo,
é realmente açúcar sintático para
Ou seja, (adicionar x) retorna uma função que recebe um argumento y e adiciona x a y.
Essa é uma função que pega uma tupla e adiciona seus elementos. Essas duas funções são realmente bastante diferentes; eles aceitam argumentos diferentes.
Se você deseja adicionar 2 a todos os números em uma lista:
O resultado seria
[3,4,5]
.Se você deseja somar cada tupla em uma lista, por outro lado, a função addTuple se encaixa perfeitamente.
O resultado seria
[12,13,14]
.As funções ao curry são ótimas onde a aplicação parcial é útil - por exemplo, mapa, dobra, aplicativo, filtro. Considere esta função, que retorna o maior número positivo na lista fornecida, ou 0 se não houver números positivos:
fonte
Outra coisa que eu não vi mencionado ainda é que o curry permite abstração (limitada) sobre a aridade.
Considere estas funções que fazem parte da biblioteca de Haskell
Em cada caso, a variável type
c
pode ser um tipo de função, para que essas funções funcionem em algum prefixo da lista de parâmetros de seus argumentos. Sem currying, você precisaria de um recurso de linguagem especial para abstrair a aridade das funções ou ter muitas versões diferentes dessas funções especializadas para diferentes aridades.fonte
Meu entendimento limitado é o seguinte:
1) Aplicação de Função Parcial
Aplicação de Função Parcial é o processo de retornar uma função que requer um número menor de argumentos. Se você fornecer 2 de 3 argumentos, retornará uma função que aceita 3-2 = 1 argumento. Se você fornecer 1 dentre 3 argumentos, retornará uma função que aceita 3-1 = 2 argumentos. Se você quisesse, poderia aplicar parcialmente 3 em 3 argumentos e retornaria uma função que não aceita argumentos.
Dada a seguinte função:
Ao vincular 1 a x e aplicá-lo parcialmente à função acima,
f(x,y,z)
você obtém:Onde:
f'(y,z) = 1 + y + z;
Agora, se você ligasse y a 2 e z a 3, e se aplicasse parcialmente,
f'(y,z)
obteria:Onde
f''() = 1 + 2 + 3
:;Agora, a qualquer momento, você pode escolher para avaliar
f
,f'
ouf''
. Então eu posso fazer:ou
2) Caril
Curry, por outro lado, é o processo de dividir uma função em uma cadeia aninhada de funções de um argumento. Você nunca pode fornecer mais de um argumento, é um ou zero.
Então, dada a mesma função:
Se você o curry, você terá uma cadeia de 3 funções:
Onde:
Agora, se você ligar
f'(x)
comx = 1
:Você retorna uma nova função:
Se você ligar
g(y)
comy = 2
:Você retorna uma nova função:
Finalmente, se você ligar
h(z)
comz = 3
:Você voltou
6
.3) Fechamento
Finalmente, o encerramento é o processo de capturar uma função e dados juntos como uma única unidade. Um fechamento de função pode levar de 0 a um número infinito de argumentos, mas também está ciente dos dados não passados para ele.
Novamente, dada a mesma função:
Você pode escrever um fechamento:
Onde:
f'
está fechadox
. Significado quef'
pode ler o valor de x que está dentrof
.Portanto, se você ligar
f
parax = 1
:Você obteria um fechamento:
Agora, se você ligou
closureOfF
comy = 2
ez = 3
:O que retornaria
6
Conclusão
Caril, aplicação parcial e fechamentos são todos semelhantes, pois decompõem uma função em mais partes.
O curry decompõe uma função de vários argumentos em funções aninhadas de argumentos únicos que retornam funções de argumentos únicos. Não faz sentido curry uma função de um ou menos argumentos, uma vez que não faz sentido.
O aplicativo parcial decompõe uma função de vários argumentos em uma função de argumentos menores cujos argumentos agora ausentes foram substituídos pelo valor fornecido.
Closure decompõe uma função em uma função e um conjunto de dados em que variáveis dentro da função que não foram passadas podem olhar dentro do conjunto de dados para encontrar um valor ao qual vincular quando solicitado a avaliar.
O que é confuso sobre tudo isso é que eles podem ser usados para implementar um subconjunto dos outros. Então, em essência, eles são todos um detalhe de implementação. Todos eles fornecem um valor semelhante, pois você não precisa reunir todos os valores antecipadamente e pode reutilizar parte da função, pois a decompôs em unidades discretas.
Divulgação
Não sou especialista no assunto, só recentemente comecei a aprender sobre isso e, portanto, forneço meu entendimento atual, mas pode haver erros que eu convido você a destacar e corrigirei como / se Eu descubro qualquer.
fonte
Currying (aplicativo parcial) permite criar uma nova função a partir de uma função existente, corrigindo alguns parâmetros. É um caso especial de fechamento lexical em que a função anônima é apenas um invólucro trivial que passa alguns argumentos capturados para outra função. Também podemos fazer isso usando a sintaxe geral para fazer fechamentos lexicais, mas a aplicação parcial fornece um açúcar sintático simplificado.
É por isso que os programadores Lisp, quando trabalham em um estilo funcional, às vezes usam bibliotecas para aplicação parcial .
Em vez de
(lambda (x) (+ 3 x))
, o que nos dá uma função que adiciona 3 ao seu argumento, você pode escrever algo como(op + 3)
, e assim adicionar 3 a cada elemento de um alguma lista seria, então,(mapcar (op + 3) some-list)
em vez de(mapcar (lambda (x) (+ 3 x)) some-list)
. Essaop
macro fará de você uma função que recebe alguns argumentosx y z ...
e chama(+ a x y z ...)
.Em muitas linguagens puramente funcionais, a aplicação parcial é arraigada na sintaxe para que não haja
op
operador. Para acionar a aplicação parcial, basta chamar uma função com menos argumentos do que ela requer. Em vez de produzir um"insufficient number of arguments"
erro, o resultado é uma função dos argumentos restantes.fonte
a -> b -> c
não possui parâmetro s (plural), possui apenas um parâmetroc
,. Quando chamado, ele retorna uma função do tipoa -> b
.Para a função
É da forma
f': 'a * 'b -> 'c
Para avaliar um fará
Para a função ao curry
Para avaliar um fará
Onde é uma computação parcial, especificamente (3 + y), que pode ser completada com
adicionar no segundo caso é da forma
f: 'a -> 'b -> 'c
O que o currying está fazendo aqui é transformar uma função que leva dois acordos em um que leva apenas um retornando um resultado. Avaliação parcial
Digamos que
x
no RHS não seja apenas um int regular, mas uma computação complexa que leva um tempo para concluir, por aumento, pelo menos dois segundos.Portanto, a função agora parece
Do tipo
add : int * int -> int
Agora queremos calcular essa função para um intervalo de números, vamos mapeá-la
Para o exposto acima, o resultado de
twoSecondsComputation
é avaliado sempre. Isso significa que leva 6 segundos para esse cálculo.O uso de uma combinação de preparo e curry pode evitar isso.
Da forma ao curry
add : int -> int -> int
Agora pode-se fazer,
O
twoSecondsComputation
único precisa ser avaliado uma vez. Para aumentar a escala, substitua dois segundos por 15 minutos ou a qualquer hora e, em seguida, tenha um mapa com 100 números.Resumo : O curry é ótimo quando usado com outros métodos para funções de nível superior como uma ferramenta de avaliação parcial. Seu objetivo não pode realmente ser demonstrado por si só.
fonte
O curry permite uma composição flexível das funções.
Eu inventei uma função "curry". Nesse contexto, não me importo com o tipo de logger que recebo ou de onde ele vem. Eu não ligo para o que é a ação ou de onde ela vem. Tudo o que me interessa é processar minha entrada.
A variável do construtor é uma função que retorna uma função que retorna uma função que recebe minha entrada que faz meu trabalho. Este é um exemplo útil simples e não um objeto à vista.
fonte
Currying é uma vantagem quando você não possui todos os argumentos para uma função. Se você estiver avaliando completamente a função, não haverá diferença significativa.
A currying evita mencionar parâmetros ainda não necessários. É mais conciso e não requer a localização de um nome de parâmetro que não colide com outra variável no escopo (que é o meu benefício favorito).
Por exemplo, ao usar funções que aceitam funções como argumentos, geralmente você se encontra em situações em que precisa de funções como "adicionar 3 à entrada" ou "comparar entrada à variável v". Com o curry, essas funções são facilmente escritas:
add 3
e(== v)
. Sem currying, você deve usar expressões lambda:x => add 3 x
ex => x == v
. As expressões lambda têm o dobro do tempo e têm uma pequena quantidade de trabalho ocupado relacionado à escolha de um nome, além disso,x
se já houver umx
escopo.Um benefício colateral das linguagens baseadas no currying é que, ao escrever código genérico para funções, você não acaba com centenas de variantes com base no número de parâmetros. Por exemplo, em C #, um método 'curry' precisaria de variantes para Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R> e assim por diante para sempre. Em Haskell, o equivalente de um Func <A1, A2, R> é mais como um Func <Tuple <A1, A2>, R> ou um Func <A1, Func <A2, R >> (e um Func <R> é mais como um Func <Unit, R>); portanto, todas as variantes correspondem ao único caso Func <A, R>.
fonte
O principal raciocínio em que consigo pensar (e não sou especialista neste assunto de forma alguma) começa a mostrar seus benefícios à medida que as funções passam de trivial para não trivial. Em todos os casos triviais com a maioria dos conceitos dessa natureza, você não encontrará nenhum benefício real. No entanto, a maioria das linguagens funcionais faz uso pesado da pilha nas operações de processamento. Considere PostScript ou Lisp como exemplos disso. Ao usar o curry, as funções podem ser empilhadas de maneira mais eficaz e esse benefício se torna aparente à medida que as operações se tornam cada vez menos triviais. Da maneira atual, o comando e os argumentos podem ser lançados na pilha em ordem e disparados conforme necessário, para que sejam executados na ordem correta.
fonte
O curry depende crucialmente (definitivamente mesmo) da capacidade de retornar uma função.
Considere este pseudo-código (artificial).
var f = (m, x, b) => ... retorna algo ...
Vamos estipular que a chamada f com menos de três argumentos retorne uma função.
var g = f (0, 1); // isso retorna uma função vinculada a 0 e 1 (m e x) que aceita mais um argumento (b).
var y = g (42); // invoca g com o terceiro argumento ausente, usando 0 e 1 para me ex
O fato de você poder aplicar argumentos parcialmente e recuperar uma função reutilizável (vinculada aos argumentos que você forneceu) é bastante útil (e DRY).
fonte