Qual é a vantagem de currying?

154

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?

Cientista maluco
fonte
54
O curry por si só é essencialmente inútil, mas ter todas as funções ativadas por padrão torna muitos outros recursos muito mais agradáveis ​​de usar. É difícil apreciar isso até você realmente usar uma linguagem funcional por um tempo.
CA McCann
4
Algo que foi mencionado de passagem por delnan em um comentário sobre a resposta de JoelEtherton, mas que eu pensei em mencionar explicitamente, é que (pelo menos em Haskell) você pode aplicar parcialmente não apenas funções, mas também tipos construtores - isso pode ser bastante acessível; isso pode ser algo para se pensar.
paul #
Todos deram exemplos de Haskell. Pode-se imaginar que o curry é útil apenas em Haskell.
Dilsh R
@ManojR Todos não deram exemplos em Haskell.
Phwd 5/02/2013
1
A questão gerou uma discussão bastante interessante no Reddit .
yannis

Respostas:

126

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

add x y = x + y

e que você deseja adicionar 2 a todos os membros de uma lista. Em Haskell, você faria o seguinte:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

Aqui a sintaxe é mais clara do que se você tivesse que criar uma função add2

add2 y = add 2 y
map add2 [1, 2, 3]

ou se você tivesse que criar uma função lambda anônima:

map (\y -> 2 + y) [1, 2, 3]

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:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

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:

myFunc :: (Key -> Value) -> .....

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 mapou filter. 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.

Boris
fonte
31
Vale ressaltar que, por causa disso, a ordem dos argumentos usada para funções no Haskell geralmente se baseia na probabilidade de aplicação parcial, o que, por sua vez, faz com que os benefícios descritos acima se apliquem (ha, ha) em mais situações. O curry, por padrão, acaba sendo ainda mais benéfico do que é aparente em exemplos específicos, como os aqui.
CA McCann
wat. "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"
Mateen Ulhaq
@MateenUlhaq É uma continuação da frase anterior, onde suponho que queremos obter um valor com base em uma chave e temos duas maneiras de fazer isso. A frase enumera essas duas maneiras. Na primeira maneira, você recebe uma lista de pares de chave / valor e a chave para a qual queremos encontrar o valor e, da outra maneira, um mapa adequado e, novamente, uma chave. Pode ser útil examinar o código imediatamente após a frase.
Boris
53

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:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

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 de mappode ser escrito de duas maneiras:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

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 mappara 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)para f, portanto, fapenas assume argumentos. Essa é realmente uma distinção muito útil que eu gostaria que outras línguas fizessem, porque torna muito natural escrever algo como:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

Você não pode fazer isso facilmente com linguagens que têm a ideia de vários argumentos!

Tikhon Jelvis
fonte
1
"As linguagens no estilo ML não têm noção de múltiplos argumentos, sob forma de curry ou não-curry": nesse aspecto, o estilo ML de Haskell é?
Giorgio
1
@Giorgio: Sim.
Tikhon Jelvis 02/02
1
Interessante. Conheço alguns Haskell e estou aprendendo SML agora, por isso é interessante ver diferenças e semelhanças entre os dois idiomas.
Giorgio
Grande resposta, e se você ainda não está convencido basta pensar sobre pipelines Unix que são semelhantes aos fluxos lambda
Sridhar Sarnobat
A resposta "prática" não é muito relevante porque a verbosidade geralmente é evitada por aplicação parcial , e não por currying. E eu argumentaria aqui que a sintaxe da abstração lambda (apesar da declaração de tipo) é mais feia do que aquela (pelo menos) no Scheme, pois precisa de regras sintáticas especiais mais integradas para analisá-la corretamente, o que incha a especificação da linguagem sem nenhum ganho sobre propriedades semânticas.
FrankHB
24

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.

psr
fonte
14

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.

Alex R
fonte
2
Embora a motivação aqui seja teórica, acho que a simplicidade é quase sempre uma vantagem prática também. Não me preocupar com funções com vários argumentos facilita minha vida quando eu programa, exatamente como faria se estivesse trabalhando com semântica.
Tikhon Jelvis
2
@TikhonJelvis Quando você está programando, porém, o curry oferece outras coisas para se preocupar, como o compilador não perceber o fato de que você passou poucos argumentos para uma função ou até mesmo receber uma mensagem de erro incorreta; quando você não usa curry, o erro é muito mais aparente.
Alex R
Nunca tive problemas assim: o GHC, no mínimo, é muito bom nesse sentido. O compilador sempre captura esse tipo de problema e também possui boas mensagens de erro.
Tikhon Jelvis
1
Não posso concordar que as mensagens de erro sejam consideradas boas. Útil, sim, mas eles ainda não são bons. Ele também só captura esse tipo de problema se resultar em um erro de tipo, ou seja, se você tentar usar o resultado posteriormente como algo diferente de uma função (ou você digitar anotado, mas contar com erros legíveis tem seus próprios problemas) ); o local relatado do erro é divorciado do local real.
Alex R
14

(Vou dar exemplos em Haskell.)

  1. Ao usar linguagens funcionais, é muito conveniente que você possa aplicar parcialmente uma função. Como em Haskell, (== x)é uma função que retorna Truese seu argumento for igual a um determinado termo x:

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    sem currying, teríamos um código um pouco menos legível:

    mem x lst = any (\y -> y == x) lst
    
  2. 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:

    mem = any . (==)
    

    Aqui vemos ==como uma função de apara a -> Boole anycomo uma função de a -> Boolpara [a] -> Bool. Simplesmente compondo-os, obtemos o resultado. Tudo isso graças ao curry.

  3. 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 comopartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

De fato, (uncurry (++) . partition (< 10)) [4,12,11,1]avalia como [4,1,12,11].

Há também vantagens teóricas importantes:

  1. O curry é essencial para idiomas que não possuem tipos de dados e têm apenas funções, como o cálculo lambda . Embora essas linguagens não sejam úteis para uso prático, elas são muito importantes do ponto de vista teórico.
  2. Isso está conectado à propriedade essencial das linguagens funcionais - as funções são objetos de primeira classe. Como vimos, a conversão de (a, b) -> cpara a -> (b -> c)significa que o resultado da última função é do tipo b -> c. Em outras palavras, o resultado é uma função.
  3. A (des) currying está intimamente ligada às categorias fechadas cartesianas , que são uma maneira categórica de visualizar os cálculos lambda digitados.
Petr Pudlák
fonte
Para o bit "código muito menos legível", não deveria mem x lst = any (\y -> y == x) lst? (Com uma barra invertida).
stusmith
Sim, obrigado por apontar isso, eu vou corrigi-lo.
Petr Pudlák
9

Curry não é apenas açúcar sintático!

Considere as assinaturas de tipo de add1(não acumulado) e add2(com curry):

add1 : (int * int) -> int
add2 : int -> (int -> int)

(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 de inte inte retorna um int. add2é uma função que recebe um inte retorna outra função que, por sua vez, recebe um inte retorna um int.

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:

apply(f, b) = f b

Agora podemos ver a diferença entre add1e add2mais claramente. add1é chamado com uma tupla de 2:

apply(add1, (3, 5))

mas add2é chamado com um int e, em seguida, seu valor de retorno é chamado com outroint :

apply(apply(add2, 3), 5)

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, mapsobre ela em uma lista) que adicionou 5 ao seu parâmetro. Você poderia escrever addFiveToParam x = x+5ou fazer o equivalente a um lambda embutido, mas também poderia muito mais facilmente (especialmente em casos menos triviais que este) add2 5!

Chama de Ptharien
fonte
3
Entendo que exista uma grande diferença nos bastidores do meu exemplo, mas o resultado parece ser uma simples mudança sintática.
Mad Scientist
5
Currying não é um conceito muito profundo. Trata-se de simplificar o modelo subjacente (consulte Lambda Calculus) ou em idiomas que possuem tuplas de qualquer maneira, na verdade, trata-se de conveniência sintática de aplicação parcial. Não subestime a importância da conveniência sintática.
Peaker
9

Curry é apenas açúcar sintático, mas você está entendendo um pouco o que o açúcar faz, eu acho. Tomando o seu exemplo,

fun add x y = x + y

é realmente açúcar sintático para

fun add x = fn y => x + y

Ou seja, (adicionar x) retorna uma função que recebe um argumento y e adiciona x a y.

fun addTuple (x, y) = x + 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:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

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.

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

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:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
gnud
fonte
1
Eu entendi que a função ao curry tem uma assinatura de tipo diferente e que na verdade é uma função que retorna outra função. Estava faltando a parte da aplicação parcial.
Cientista Louco
9

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

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

Em cada caso, a variável type cpode 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.

Geoff Reedy
fonte
6

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:

f(x,y,z) = x + y + z;

Ao vincular 1 a x e aplicá-lo parcialmente à função acima, f(x,y,z)você obtém:

f(1,y,z) = f'(y,z);

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:

f'(2,3) = f''();

Onde f''() = 1 + 2 + 3:;

Agora, a qualquer momento, você pode escolher para avaliar f, f'ou f''. Então eu posso fazer:

print(f''()) // and it would return 6;

ou

print(f'(1,1)) // and it would return 3;

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:

f(x,y,z) = x + y + z;

Se você o curry, você terá uma cadeia de 3 funções:

f'(x) -> f''(y) -> f'''(z)

Onde:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

Agora, se você ligar f'(x)com x = 1:

f'(1) = 1 + f''(y);

Você retorna uma nova função:

g(y) = 1 + f''(y);

Se você ligar g(y)com y = 2:

g(2) = 1 + 2 + f'''(z);

Você retorna uma nova função:

h(z) = 1 + 2 + f'''(z);

Finalmente, se você ligar h(z)com z = 3:

h(3) = 1 + 2 + 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:

f(x,y,z) = x + y + z;

Você pode escrever um fechamento:

f(x) = x + f'(y, z);

Onde:

f'(y,z) = x + y + z;

f'está fechado x. Significado que f'pode ler o valor de x que está dentro f.

Portanto, se você ligar fpara x = 1:

f(1) = 1 + f'(y, z);

Você obteria um fechamento:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Agora, se você ligou closureOfFcom y = 2e z = 3:

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 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.

Didier A.
fonte
1
Então a resposta é: currying não tem vantagem?
ceving 13/12/16
1
@ceving Até onde eu sei, isso está correto. Na prática, o curry e a aplicação parcial oferecem os mesmos benefícios. A escolha de qual implementar em um idioma é feita por motivos de implementação; um pode ser mais fácil de implementar do que outro, dado um determinado idioma.
Didier A.
5

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). Essa opmacro fará de você uma função que recebe alguns argumentos x 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 opoperador. 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.

Kaz
fonte
"Currying ... permite criar uma nova função ... fixando alguns parâmetros" - não, uma função do tipo a -> b -> cnão possui parâmetro s (plural), possui apenas um parâmetro c,. Quando chamado, ele retorna uma função do tipo a -> b.
precisa
4

Para a função

fun add(x, y) = x + y

É da forma f': 'a * 'b -> 'c

Para avaliar um fará

add(3, 5)
val it = 8 : int

Para a função ao curry

fun add x y = x + y

Para avaliar um fará

add 3
val it = fn : int -> int

Onde é uma computação parcial, especificamente (3 + y), que pode ser completada com

it 5
val it = 8 : int

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

Por que alguém precisaria disso?

Digamos que xno 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.

x = twoSecondsComputation(z)

Portanto, a função agora parece

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

Do tipo add : int * int -> int

Agora queremos calcular essa função para um intervalo de números, vamos mapeá-la

val result1 = map (fn x => add (20, x)) [3, 5, 7];

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.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

Da forma ao curry add : int -> int -> int

Agora pode-se fazer,

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

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ó.

phwd
fonte
3

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.

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

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.

mortalapeman
fonte
2

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 3e (== v). Sem currying, você deve usar expressões lambda: x => add 3 xe x => 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, xse já houver um xescopo.

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>.

Craig Gidney
fonte
2

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.

Peter Mortensen
fonte
1
Como exatamente a criação de muito mais quadros de pilha a serem criados torna as coisas mais eficientes?
Mason Wheeler
1
@MasonWheeler: Eu não saberia, pois disse que não sou especialista em linguagens funcionais ou em currying específico. Eu rotulei este wiki da comunidade especificamente por causa disso.
Joel Etherton
4
@MasonWheeler Você tem razão em escrever esta resposta, mas deixe-me explicar e dizer que a quantidade de quadros de pilha realmente criados depende muito da implementação. Por exemplo, na máquina G sem marca de rotação (STG; a maneira como o GHC implementa Haskell) atrasa a avaliação real até que ela acumule todos os argumentos (ou pelo menos quantos forem necessários). Não consigo me lembrar se isso é feito para todas as funções ou apenas para construtores, mas acho que deveria ser possível para a maioria das funções. (Então, novamente, o conceito de "quadros de pilha" não se aplica realmente ao STG.)
1

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).

Rick O'Shea
fonte