Scala currying против частично применяемых функций

82

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

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

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

Эрик
источник
При частичном применении затраты на представление каррированной и не каррированной версии в памяти могут быть разными, поэтому это также может повлиять на производительность во время выполнения. (То есть, если оптимизатор недостаточно умен, чтобы выбрать оптимальное представление в обоих случаях.) Однако я недостаточно знаком со Scala, чтобы сказать, каковы точные различия.
1
Взгляните на это: stackoverflow.com/questions/8063325/…
Utaal
Я нашел это объяснение очень полезным, он объясняет частичные функции, частично применяемые функции и каррирование в одном посте: stackoverflow.com/a/8650639/1287554
Plasty Grove
Отличная ссылка, @PlastyGrove. Спасибо!
Эрик
И спасибо @Utaal за ссылку. Любой ответ самого Мартина Одерского очень ценится. Я думаю, что эти концепции сейчас начинают появляться.
Эрик

Ответы:

88

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

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

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

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

scala> def modNCurried(n: Int)(x: Int): Boolean = ((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)

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

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

Пост скриптум

Примечание . Причина, по которой ваш пример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>

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

Fresskoma
источник
одно важное отличие состоит в том, что вывод типа работает иначе: gist.github.com/4529020
Спасибо, я добавил примечание по поводу вашего комментария :)
fresskoma
19

Каррирование связано с кортежами: превращение функции, которая принимает аргумент кортежа, в функцию, которая принимает 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 гораздо менее полезен и менее очевиден. И люди даже в конечном итоге думают, что частичное приложение и каррирование - это одно и то же - просто потому, что они не могут легко представить каррированные функции!

Дон Стюарт
источник
Я полностью согласен. В терминах Scala «каррирование» в исходном значении этого слова - это «процесс преобразования» функции с одним списком параметров в функцию с несколькими списками параметров. В Scala это преобразование можно выполнить с помощью ".curried". К сожалению, Scala, похоже, немного перегрузила значение этого слова, поскольку изначально оно скорее называлось «.curry», чем «.curried».
Фатих Джошкун
2

Многопараметрическая функция:

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

Каррирование (или каррированная функция):

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

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

lcn
источник
0

Просто чтобы уточнить последний пункт

Дополнение: как правильно указал @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,_)
              ^
Sud
источник
0

Лучшее объяснение, которое я смог найти до сих пор: https://dzone.com/articles/difference-between-currying-amp-parhibited-applied

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

Частичное применение функции: передать функции меньше аргументов, чем указано в ее объявлении. Scala не генерирует исключение, когда вы предоставляете функции меньше аргументов, он просто применяет их и возвращает новую функцию с остальными аргументами, которые необходимо передать.

Данил Заторский
источник