Qual é a vantagem de currying?

148

Acabei de aprender sobre o curry e, embora eu ache que entendi o conceito, não vejo nenhuma grande vantagem em usá-lo.

Como um exemplo trivial, eu 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 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 ser apenas açúcar sintático que remove um conjunto de parênteses da definição e da chamada da função. Eu vi currying listado como uma das características importantes de uma linguagem funcional, e estou um pouco desapontado com isso no momento. O conceito de criar uma cadeia de funções que consuma cada um parâmetro, em vez de uma função que usa 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 o curry, ou estou perdendo algumas outras vantagens que não são óbvias no meu exemplo muito simples? Está curry apenas açúcar sintático?

    
por Mad Scientist 01.02.2013 / 20:36
fonte

15 respostas

123

Com funções curried, você obtém uma reutilização mais fácil de funções mais abstratas, desde que você se especialize. Digamos que você tenha uma função de adição

add x y = x + y

e que você deseja adicionar 2 a cada membro de uma lista. Em Haskell você faria isso:

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 fazer uma função lambda anônima:

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

Também permite abstrair de diferentes implementações. Digamos que você tenha duas funções de pesquisa. Um de uma lista de pares de chave / valor e uma chave para um valor e outro de um mapa de chaves para valores e uma chave para um valor, como este:

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ê poderia passar qualquer uma das funções de pesquisa acima, aplicadas parcialmente com uma lista ou um mapa, respectivamente:

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

Em conclusão: o currying é bom, porque permite especializar / aplicar parcialmente as funções usando uma sintaxe leve e, em seguida, passar essas funções parcialmente aplicadas para funções de ordem superior, como map ou filter . As funções de ordem mais alta (que assumem funções como parâmetros ou as produzem como resultados) são o pão com manteiga da programação funcional, e as funções curry e parcialmente aplicadas permitem que funções de ordem superior sejam usadas de forma muito mais eficaz e concisa.

    
por 01.02.2013 / 21:23
fonte
53

A resposta prática é que o currying facilita muito a criação de funções anônimas. Mesmo com uma sintaxe lambda mínima, é uma espécie de vitória; compare:

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ê usa mais e mais funções de ordem superior. Enquanto eu uso mais funções de ordem mais alta no Haskell do que em outras linguagens, eu descobri que eu realmente uso a sintaxe lambda menos porque algo como dois terços do tempo, o lambda seria apenas uma função parcialmente aplicada. (E muito da outra vez eu extraí-lo em uma função nomeada.)

Mais fundamentalmente, nem sempre é óbvio qual versão de uma função é "canônica". Por exemplo, leve map . O tipo de map pode ser escrito de duas maneiras:

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

Qual é o "correto"? É realmente difícil dizer. Na prática, a maioria dos idiomas usa o primeiro - o mapa recebe 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 ambos , de uma maneira muito elegante.

Isso se torna especialmente importante quando você generaliza map para outros tipos além da lista.

Além disso, o curry realmente não é muito complicado. Na verdade, é um pouco de simplificação em relação ao modelo usado pela maioria das linguagens: você não precisa de nenhuma noção de funções de vários argumentos inseridos em seu idioma. Isso também reflete o cálculo lambda subjacente mais de perto.

Naturalmente, as linguagens no estilo ML não possuem uma noção de múltiplos argumentos em formato curry ou não-curvo. A sintaxe f(a, b, c) corresponde a passar na tupla (a, b, c) para f , então f ainda aceita argumentos. Esta é realmente uma distinção muito útil que eu gostaria que outras linguagens tivessem, porque torna muito natural escrever algo como:

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

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

    
por 01.02.2013 / 21:31
fonte
21

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 lugar no código. Você pode simplesmente aplicar um ou mais parâmetros ao obtê-los e passar o resultado para outro trecho de código que tenha mais parâmetros e finalize a avaliação nele.

O código para realizar isso será mais simples do que se você precisar reunir todos os parâmetros primeiro.

Além disso, há a possibilidade de mais reutilização de código, já que funções que usam um único parâmetro (outra função curried) não precisam corresponder especificamente a todos os parâmetros.

    
por 01.02.2013 / 21:07
fonte
14

A principal motivação (pelo menos inicialmente) para o currying não era prático, mas teórico. Em particular, currying permite efetivamente obter funções multi-argumentos sem realmente definir semântica para eles ou definir semântica para produtos. Isso leva a uma linguagem mais simples, com tanta expressividade quanto outra linguagem mais complicada e, portanto, é desejável.

    
por 02.02.2013 / 15:12
fonte
14

(Vou dar exemplos em Haskell.)

  1. Ao usar linguagens funcionais, é muito prático aplicar parcialmente uma função. Como em (== x) de Haskell é uma função que retorna True se seu argumento for igual a um dado termo x :

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

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

    mem x lst = any (\y -> y == x) lst
    
  2. Isto está relacionado com Programação tácita (ver também Estilo sem ponto no wiki do Haskell). Esse estilo não se concentra em valores representados por variáveis, mas em funções de composição e em como as informações fluem 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, visualizamos == como uma função de a a a -> Bool e any como uma função de a -> Bool a [a] -> Bool . Simplesmente compondo-os, obtemos o resultado. Tudo isso graças ao curry.

  3. O contrário, sem curry, 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 então concatenar essas duas listas. A divisão da lista é feita por partition (< 10) (aqui também usamos curried < ). O resultado é do tipo ([Int],[Int]) . Em vez de extrair o resultado em sua primeira e segunda parte e combiná-los usando ++ , podemos fazer isso diretamente, ocultando ++ as

    uncurry (++) . partition (< 10)
    

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

Existem também importantes vantagens teóricas:

  1. O currying é 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. Isto está conectado com a propriedade essencial de linguagens funcionais - funções são objetos de primeira classe. Como vimos, a conversão de (a, b) -> c para a -> (b -> c) significa que o resultado da última função é do tipo b -> c . Em outras palavras, o resultado é uma função.
  3. O
  4. (Un) currying está intimamente ligado às categorias fechadas cartesianas , que é uma maneira categórica de visualizar os lambda calculi digitados.
por 01.02.2013 / 22:14
fonte
9

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

Considere as assinaturas de tipos de add1 (uncurried) e add2 (curried):

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

(Em ambos os casos, os parênteses na assinatura de tipo são opcionais, mas eu os incluí para maior clareza.)

add1 é uma função que usa uma 2-tupla de int e int e retorna int . add2 é uma função que usa int e retorna outra função que, por sua vez, recebe int e retorna int .

A diferença essencial entre os dois torna-se mais visível quando especificamos o aplicativo de função explicitamente. Vamos definir uma função (não curried) que aplique seu primeiro argumento ao segundo argumento:

apply(f, b) = f b

Agora podemos ver a diferença entre add1 e add2 mais claramente. add1 é chamado com uma 2-tupla:

apply(add1, (3, 5))

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

apply(apply(add2, 3), 5)

EDITAR: O benefício essencial do currying é que você obtém aplicação parcial de graça. Vamos dizer que você queria uma função do tipo int -> int (digamos, para map sobre uma lista) que adicionou 5 ao seu parâmetro. Você poderia escrever addFiveToParam x = x+5 , ou você poderia fazer o equivalente com um lambda embutido, mas você poderia muito mais facilmente (especialmente em casos menos triviais do que este) escrever add2 5 !

    
por 01.02.2013 / 20:51
fonte
9

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

fun add x y = x + y

é na verdade açúcar sintático para

fun add x = fn y => x + y

Ou seja, (add 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 muito diferentes; eles levam argumentos diferentes.

Se você quiser adicionar 2 a todos os números de 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ê quiser somar cada tupla em uma lista, por outro lado, a função addTuple se ajusta 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] .

Funções curadas 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 
    
por 02.02.2013 / 15:43
fonte
9

Outra coisa que eu não vi ainda é que o currying permite abstração (limitada) sobre a aridade.

Considere estas funções que fazem parte da biblioteca do 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 de tipo c pode ser um tipo de função para que essas funções funcionem em algum prefixo da lista de parâmetros do seu argumento. Sem curry, você precisaria de um recurso de linguagem especial para abstrair sobre a funcionalidade ou ter muitas versões diferentes dessas funções especializadas para diferentes arities.

    
por 03.02.2013 / 04:01
fonte
6

Meu entendimento limitado é esse:

1) Aplicação da função parcial

Aplicação de Função Parcial é o processo de retornar uma função que recebe um número menor de argumentos. Se você fornecer 2 de 3 argumentos, ele retornará uma função que leva 3-2 = 1 argumento. Se você fornecer 1 de 3 argumentos, ele retornará uma função que aceita 3-1 = 2 argumentos. Se você quisesse, você poderia aplicar parcialmente 3 de 3 argumentos e retornaria uma função que não aceita argumentos.

Então, a seguinte função:

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

Ao vincular 1 a x e aplicar parcialmente à função acima f(x,y,z) você obteria:

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

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

Agora, se você vincular y a 2 e z a 3, e aplicar parcialmente f'(y,z) , você terá:

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

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

A qualquer momento, você pode optar por 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) Curry

Currying , 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 1 argumento, é um ou zero.

Então, a mesma função:

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

Se você curry, você teria 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ê chamar f'(x) com x = 1 :

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

Você recebe uma nova função:

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

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

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

Você recebe uma nova função:

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

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

h(3) = 1 + 2 + 3;

Você está retornando 6 .

3) Encerramento

Finalmente, Closure é o processo de capturar uma função e dados juntos como uma única unidade. Um fechamento de função pode levar 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 em x . O que significa que f' pode ler o valor de x dentro de f .

Se você fosse chamar f com x = 1 :

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

Você teria um fechamento:

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

Agora, se você chamou closureOfF com y = 2 e z = 3 :

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

Qual seria o retorno de 6

Conclusão

O currying, a aplicação parcial e os encerramentos são todos semelhantes, na medida em que eles decompõem uma função em mais partes.

Currying 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 adianta restringir uma função de um ou menos argumentos, pois 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 ausentes agora foram substituídos pelo valor fornecido.

O Closure decompõe uma função em uma função e um conjunto de dados em que as variáveis dentro da função que não foram passadas podem procurar dentro do conjunto de dados para encontrar um valor para vincular quando solicitado a avaliar.

O que é confuso sobre tudo isso é que eles podem ser usados para implementar um subconjunto dos outros. Então, na essência, eles são todos detalhes 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, já que a decompôs em unidades discretas.

Divulgação

Eu não sou de modo algum um especialista no assunto, só recentemente comecei a aprender sobre isso, e então eu forneço meu entendimento atual, mas ele pode ter erros que eu convido você a apontar, e eu vou corrigir como / se eu descobrir algum.

    
por 16.04.2015 / 07:44
fonte
5

Currying (aplicação parcial) permite criar uma nova função a partir de uma função existente, corrigindo alguns parâmetros. É um caso especial de fechamento léxico onde 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 closes 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)) , que nos dá uma função que adiciona 3 ao seu argumento, você pode escrever algo como (op + 3) , e então adicionar 3 a cada elemento de alguma lista seria (mapcar (op + 3) some-list) em vez de %código%. Esta macro (mapcar (lambda (x) (+ 3 x)) some-list) fará de você uma função que recebe alguns argumentos op e invoca x y z ... .

Em muitas linguagens puramente funcionais, a aplicação parcial é incorporada à sintaxe para que não haja nenhum operador (+ a x y z ...) . Para acionar a aplicação parcial, você simplesmente chama uma função com menos argumentos do que requer. Em vez de produzir um erro op , o resultado é uma função dos argumentos restantes.

    
por 01.02.2013 / 22:48
fonte
4

Para a função

fun add(x, y) = x + y

Está no formato f': 'a * 'b -> 'c

Para avaliar, um fará

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

Para a função curry

fun add x y = x + y

Para avaliar, um fará

add 3
val it = fn : int -> int

Onde é um cálculo parcial, especificamente (3 + y), que então pode completar o cálculo com

it 5
val it = 8 : int

adicione no segundo caso o formato f: 'a -> 'b -> 'c

O que o currying está fazendo aqui é transformar uma função que leva dois acordos em um que só leva um retornando um resultado. Avaliação parcial

Why would one need this?

Digamos que x no RHS não é apenas um int regular, mas sim um cálculo complexo que demora um pouco para ser concluído, para augments, sake, dois segundos.

x = twoSecondsComputation(z)

Então 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 acima, o resultado de twoSecondsComputation é avaliado a cada vez. Isso significa que leva 6 segundos para esse cálculo.

Usando uma combinação de encenação e curry, é possível evitar isso.

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

da forma curried add : int -> int -> int

Agora pode-se fazer,

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

O twoSecondsComputation precisa ser avaliado apenas uma vez. Para subir a escala, substitua dois segundos por 15 minutos, ou qualquer hora, e então tenha um mapa contra 100 números.

Resumo : O currying é ótimo quando usado com outros métodos para funções de alto nível como uma ferramenta de avaliação parcial. Sua finalidade não pode ser demonstrada por si mesma.

    
por 02.02.2013 / 16:22
fonte
3

O curry permite a composição flexível das funções.

Eu criei uma função "curry". Neste contexto, não me importo com o tipo de logger que recebo ou de onde vem. Eu não me importo com o que a ação é ou de onde 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 simples e útil e não um objeto à vista.

    
por 01.02.2013 / 21:53
fonte
2

O curry é uma vantagem quando você não tem todos os argumentos para uma função. Se você estiver avaliando totalmente a função, não há diferença significativa.

Currying permite que você evite mencionar parâmetros ainda não necessários. É mais conciso e não requer encontrar um nome de parâmetro que não colida com outra variável no escopo (que é meu benefício favorito).

Por exemplo, ao usar funções que tomam funções como argumentos, você frequentemente se encontrará em situações em que precisa de funções como "adicionar 3 à entrada" ou "comparar a entrada à variável v". Com curry, estas funções são facilmente escritas: add 3 e (== v) . Sem curry, você precisa usar expressões lambda: x => add 3 x e x => x == v . As expressões lambda são duas vezes mais longas e têm um pequeno trabalho ocupado relacionado a escolher um nome além de x se já houver um x no escopo.

Um benefício colateral das linguagens baseadas no currying é que, ao escrever código genérico para funções, você não tem centenas de variantes com base no número de parâmetros. Por exemplo, em C #, um método de "curry" precisaria de variantes para as funções Func, Func, Func, A1, A2, Func, < A1, A2, A3, R > e assim por diante. para sempre. Em Haskell, o equivalente de um Func < A1, A2, R > é mais parecido com um Func < Tuple < A1, A2 & gt ;, R > ou um Func < A1, Func < A2, R > > (e um Func < R > é mais parecido com um Func < Unit, R >), pelo que todas as variantes correspondem ao único Func < A, R > caso.

    
por 01.02.2013 / 23:39
fonte
2
O raciocínio principal que eu posso pensar (e eu não sou um especialista sobre este assunto, por qualquer meio) começa a mostrar seus benefícios à medida que as funções se deslocam de triviais para não-triviais. 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 intenso da pilha nas operações de processamento. Considere PostScript ou Lisp como exemplos disso. Ao utilizar o currying, as funções podem ser empilhadas de forma mais eficaz e esse benefício se torna aparente à medida que as operações se tornam menos e menos triviais. Na maneira curry, 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.

    
por 09.11.2013 / 11:13
fonte
0

Currying depende crucialmente (definitivamente até mesmo) da capacidade de retornar uma função.

Considere este pseudo código (inventado).

var f = (m, x, b) = > ... devolve algo ...

Vamos estipular que chamar f com menos de três argumentos retorna uma função.

var g = f (0, 1); // isso retorna uma função ligada a 0 e 1 (me x) que aceita mais um argumento (b).

var y = g (42); // invoca g com o terceiro argumento ausente, usando 0 e 1 para m e x

Você pode aplicar argumentos parcialmente e recuperar uma função reutilizável (vinculada a esses argumentos que você forneceu) é bastante útil (e DRY).

    
por 06.07.2014 / 18:27
fonte