Duas maneiras de currying em Scala; qual é o caso de uso para cada um?

83

Estou tendo uma discussão sobre as listas de parâmetros múltiplos no guia de estilo do Scala que mantenho. Percebi que existem duas maneiras de currying e estou me perguntando quais são os casos de uso:

def add(a:Int)(b:Int) = {a + b}
// Works
add(5)(6)
// Doesn't compile
val f = add(5)
// Works
val f = add(5)_
f(10) // yields 15

def add2(a:Int) = { b:Int => a + b }
// Works
add2(5)(6)
// Also works
val f = add2(5)
f(10) // Yields 15
// Doesn't compile
val f = add2(5)_

O guia de estilo implica incorretamente que são iguais, quando claramente não são. O guia está tentando fazer uma observação sobre funções de curry criadas e, embora a segunda forma não seja curry "pelo livro", ainda é muito semelhante à primeira forma (embora indiscutivelmente mais fácil de usar porque você não precisa o _)

Daqueles que usam esses formulários, qual é o consenso sobre quando usar um formulário em vez de outro?

davetron5000
fonte

Respostas:

137

Métodos de lista de parâmetros múltiplos

Para inferência de tipo

Métodos com várias seções de parâmetro podem ser usados ​​para auxiliar a inferência de tipo local, usando parâmetros na primeira seção para inferir argumentos de tipo que fornecerão um tipo esperado para um argumento na seção subsequente. foldLeftna biblioteca padrão é o exemplo canônico disso.

def foldLeft[B](z: B)(op: (B, A) => B): B

List("").foldLeft(0)(_ + _.length)

Se fosse escrito como:

def foldLeft[B](z: B, op: (B, A) => B): B

Seria necessário fornecer tipos mais explícitos:

List("").foldLeft(0, (b: Int, a: String) => a + b.length)
List("").foldLeft[Int](0, _ + _.length)

Para API fluente

Outro uso para métodos de seção de vários parâmetros é criar uma API que se pareça com uma construção de linguagem. O chamador pode usar colchetes em vez de parênteses.

def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)

loop(2) {
   println("hello!")
}

A aplicação de N listas de argumentos a métodos com M seções de parâmetros, onde N <M, podem ser convertidos em uma função explicitamente com a _, ou implicitamente, com um tipo esperado de FunctionN[..]. Este é um recurso de segurança, consulte as notas de mudança para Scala 2.0, nas Referências do Scala, para um histórico.

Funções Curried

Funções curried (ou simplesmente funções que retornam funções) podem ser aplicadas mais facilmente a N listas de argumentos.

val f = (a: Int) => (b: Int) => (c: Int) => a + b + c
val g = f(1)(2)

Esta pequena conveniência às vezes vale a pena. Observe que as funções não podem ser do tipo paramétricas, portanto, em alguns casos, um método é necessário.

Seu segundo exemplo é um híbrido: um método de seção de um parâmetro que retorna uma função.

Computação Multi Stage

Onde mais as funções curried são úteis? Aqui está um padrão que surge o tempo todo:

def v(t: Double, k: Double): Double = {
   // expensive computation based only on t
   val ft = f(t)

   g(ft, k)
}

v(1, 1); v(1, 2);

Como podemos compartilhar o resultado f(t)? Uma solução comum é fornecer uma versão vetorial de v:

def v(t: Double, ks: Seq[Double]: Seq[Double] = {
   val ft = f(t)
   ks map {k => g(ft, k)}
}

Feio! Envolvemos questões não relacionadas - calcular g(f(t), k)e mapear uma sequência de ks.

val v = { (t: Double) =>
   val ft = f(t)
   (k: Double) => g(ft, k)       
}
val t = 1
val ks = Seq(1, 2)
val vs = ks map (v(t))

Também podemos usar um método que retorna uma função. Neste caso, é um pouco mais legível:

def v(t:Double): Double => Double = {
   val ft = f(t)
   (k: Double) => g(ft, k)       
}

Mas se tentarmos fazer o mesmo com um método com várias seções de parâmetros, ficaremos presos:

def v(t: Double)(k: Double): Double = {
                ^
                `-- Can't insert computation here!
}
retrônimo
fonte
3
Ótimas respostas; gostaria de ter mais votos positivos do que apenas um. Vou digerir e aplicar ao guia de estilo; se for bem-sucedido, estas são as respostas escolhidas ...
davetron5000
1
Você pode querer corrigir seu exemplo de loop para:def loop[A](n: Int)(body: => A): Unit = (0 until n) foreach (n => body)
Omer van Kloeten
Isso não compila:val f: (a: Int) => (b: Int) => (c: Int) = a + b + c
nilskp
"A aplicação de N listas de argumentos a métodos com M seções de parâmetros, onde N <M, podem ser convertidos em uma função explicitamente com _, ou implicitamente, com um tipo esperado de FunctionN [..]." <br/> Não deveria ser FunctionX [..], onde X = MN?
stackoverflower
"Isso não compila: val f: (a: Int) => (b: Int) => (c: Int) = a + b + c" Eu não acho "f: (a: Int) = > (b: Int) => (c: Int) "é a sintaxe certa. Provavelmente retrônimo significava "f: Int => Int => Int => Int". Como => é associativo à direita, isso é na verdade "f: Int => (Int => (Int => Int))". Portanto, f (1) (2) é do tipo Int => Int (ou seja, o bit mais interno no tipo f)
stackoverflower
16

Você pode curry apenas funções, não métodos. addé um método, portanto, você precisa de _para forçar sua conversão em uma função. add2retorna uma função, portanto, _não é apenas desnecessário, mas não faz sentido aqui.

Considerando como diferentes métodos e funções são (por exemplo, a partir da perspectiva da JVM), Scala faz um bom trabalho muito borrar a linha entre eles e fazendo "a coisa certa" na maioria dos casos, mas não é uma diferença, e às vezes você só precisa para saber sobre isso.

Landei
fonte
Isso faz sentido, então como você chama a forma def add (a: Int) (b: Int) then? Qual é o termo / frase que descreve a diferença entre esse def e def add (a: Int, b: Int)?
davetron5000
@ davetron5000 o primeiro é um método com várias listas de parâmetros, o segundo é um método com uma lista de parâmetros.
Jesper
5

Acho que ajuda a compreender as diferenças se eu adicionar que com def add(a: Int)(b: Int): Intvocê basicamente apenas definir um método com dois parâmetros, apenas esses dois parâmetros são agrupados em duas listas de parâmetros (veja as consequências disso em outros comentários). Na verdade, esse método é apenas no int add(int a, int a)que diz respeito a Java (não Scala!). Quando você escreve add(5)_, isso é apenas um literal de função, uma forma mais curta de { b: Int => add(1)(b) }. Por outro lado, com add2(a: Int) = { b: Int => a + b }você define um método que tem apenas um parâmetro, e para Java será scala.Function add2(int a). Quando você escreve add2(1)em Scala, é apenas uma chamada de método simples (em oposição a um literal de função).

Observe também que addtem (potencialmente) menos sobrecarga do que add2se você fornecer todos os parâmetros imediatamente. Como add(5)(6)apenas traduzido add(5, 6)no nível da JVM, nenhum Functionobjeto é criado. Por outro lado, add2(5)(6)primeiro criará um Functionobjeto que o encerra 5e, em seguida, o chamará apply(6).

ddekany
fonte