Scala currying vs частично прикладные функции

Я понимаю, что здесь есть несколько вопросов о , что currying и частично примененные функции, но я спрашиваю, как они отличаются. В качестве простого примера, здесь используется кардинальная функция для нахождения четных чисел:

def filter(xs: List[Int], p: Int => Boolean): List[Int] =
   if (xs.isEmpty) xs
   else if (p(xs.head)) xs.head :: filter(xs.tail, p)
   else filter(xs.tail, p)

def modN(n: Int)(x: Int) = ((x % n) == 0)

Итак, вы можете написать следующее, чтобы использовать это:

val nums = List(1,2,3,4,5,6,7,8)
println(filter(nums, modN(2))

который возвращает: List(2,4,6,8). Но я обнаружил, что могу сделать одно и то же:

def modN(n: Int, x: Int) = ((x % n) == 0)

val p = modN(2, _: Int)
println(filter(nums, p))

который также возвращает: List(2,4,6,8).

Итак, мой вопрос: какое основное различие между этими двумя, и когда вы будете использовать один над другим? Является ли это слишком упрощенным примером, чтобы показать, почему один из них будет использоваться над другим?

Ответ 1

Семантическая разница была достаточно хорошо объяснена в ответе, связанном с Plasty Grove.

Что касается функциональности, то, похоже, не очень большая разница. Давайте посмотрим на некоторые примеры, чтобы проверить это. Во-первых, нормальная функция:

scala> def modN(n: Int, x: Int) = ((x % n) == 0)
scala> modN(5, _ : Int)
res0: Int => Boolean = <function1>

Итак, мы получаем частично примененный <function1>, который принимает Int, потому что мы уже дали ему первое целое число. Все идет нормально. Теперь к currying:

scala> def modNCurried(n: Int)(x: Int) = ((x % n) == 0)

В этих обозначениях вы наивно ожидаете, что следующее будет работать:

scala> modNCurried(5)
<console>:9: error: missing arguments for method modN;
follow this method with `_' if you want to treat it as a partially applied function
          modNCurried(5)

Итак, нотация списка параметров с несколькими параметрами на самом деле не создает сразу функцию curried (предположительно, чтобы избежать ненужных накладных расходов), но ожидает, что вы явно укажете, что вы хотите, чтобы она была нарисована (обозначение имеет некоторые другие преимущества):

scala> modNCurried(5) _
res24: Int => Boolean = <function1>

Это то же самое, что и раньше, поэтому нет никакой разницы здесь, кроме обозначения. Другой пример:

scala> modN _
res35: (Int, Int) => Boolean = <function2>

scala> modNCurried _
res36: Int => (Int => Boolean) = <function1>

Это демонстрирует, как частично применение "нормальной" функции приводит к функции, которая принимает все параметры, тогда как частично применение функции с несколькими списками параметров создает цепочку функций по одному в списке параметров, который, все возвращают новую функцию:

scala> def foo(a:Int, b:Int)(x:Int)(y:Int) = a * b + x - y
scala> foo _
res42: (Int, Int) => Int => (Int => Int) = <function2>

scala> res42(5)
<console>:10: error: not enough arguments for method apply: (v1: Int, v2: Int)Int => (Int => Int) in trait Function2.
Unspecified value parameter v2.

Как вы можете видеть, поскольку первый список параметров foo имеет два параметра, первая функция в цепочке с картой имеет два параметра.


Таким образом, частично прикладные функции на самом деле не отличаются друг от друга по форме. Это легко проверить, если вы можете преобразовать любую функцию в карри:

scala> (modN _).curried
res45: Int => (Int => Boolean) = <function1

scala> modNCurried _
res46: Int => (Int => Boolean) = <function1>

Post Scriptum

Примечание. Причина, по которой ваш пример println(filter(nums, modN(2)) работает без подчеркивания после modN(2), кажется, что компилятор Scala просто предполагает, что подчеркивание является удобством для программиста.


Дополнение: Как @asflierl правильно указал, Scala, похоже, не может вывести тип при частичном применении "нормальных" функций:

scala> modN(5, _)
<console>:9: error: missing parameter type for expanded function ((x$1) => modN(5, x$1))

В то время как эта информация доступна для функций, написанных с использованием нескольких списков параметров:

scala> modNCurried(5) _
res3: Int => Boolean = <function1>

Это ответы показывает, как это может быть очень полезно.

Ответ 2

Currying имеет отношение к кортежам: превращение функции, которая принимает аргумент кортежа в один, который принимает n отдельных аргументов и наоборот. Помните, что это ключ к различию карри и частичного приложения, даже на языках, которые не поддерживают карри.

curry :: ((a, b) -> c) -> a -> b -> c 
   -- curry converts a function that takes all args in a tuple
   -- into one that takes separate arguments

uncurry :: (a -> b -> c) -> (a, b) -> c
   -- uncurry converts a function of separate args into a function on pairs.

Частичное приложение - это возможность применить функцию к некоторым аргументам, давая новую функцию для остальных аргументов.

Легко запомнить, если вы просто думаете, что каррирование - это преобразование, связанное с кортежами.

В языках, которые по умолчанию заданы по умолчанию (например, Haskell), разница очевидна - вы должны действительно что-то делать, чтобы передавать аргументы в кортеже. Но большинство других языков, включая Scala, по умолчанию не используются - все аргументы передаются как кортежи, поэтому curry/uncurry гораздо менее полезны и менее очевидны. И люди даже в конечном итоге думают, что частичное приложение и каррирование - одно и то же - только потому, что они не могут легко представлять карри-функции!

Ответ 3

Многовариантная функция:

def modN(n: Int, x: Int) = ((x % n) == 0)

Currying (или карриная функция):

def modNCurried(n: Int)(x: Int) = ((x % n) == 0)

Таким образом, это не частично применяется функция, сопоставимая с карри. Это многопараметрическая функция. То, что сопоставимо с частично применяемой функцией, является результатом вызова функции curried, которая является функцией с тем же списком параметров, что и частично примененная функция.

Ответ 4

Только для уточнения последнего пункта

Дополнение: Поскольку @asflierl правильно указал, Scala не кажется чтобы иметь возможность вывести тип при частичном применении "нормального" Функции:

Scala может выводить типы, если все параметры являются подстановочными знаками, но не тогда, когда некоторые из них указаны, а некоторые из них не являются.

scala> modN(_,_)
res38: (Int, Int) => Boolean = <function2>

scala> modN(1,_)
<console>:13: error: missing parameter type for expanded function ((x$1) => modN(1, x$1))
       modN(1,_)
              ^