Каррінг Scala проти частково застосованих функцій


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

Семантична різниця досить добре пояснена у відповіді, на яку посилається Пластиковий гай .

Що стосується функціональних можливостей, то, здається, різниці немає. Давайте розглянемо кілька прикладів, щоб підтвердити це. По-перше, нормальна функція:

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має два параметри, перша функція в ланцюжку, що працює, має два параметри.


Підводячи підсумок, частково застосовані функції насправді не відрізняються від функцій, що функціонують з використанням каррі. Це легко перевірити, враховуючи те, що ви можете перетворити будь-яку функцію на функцію curry:

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>

Ці відповіді показують, як це може бути дуже корисним.


одна важлива відмінність полягає в тому, що умовивід типу працює по-різному: 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)

Отже, це не частково застосована функція, яка порівнянна з каррінгом. Це багатовимірна функція. Що можна порівняти з частково застосованою функцією, це результат виклику функції, що працює, яка є функцією з тим самим списком параметрів, що і частково застосована функція.


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,_)
              ^

0

Найкраще пояснення, яке я міг поки що знайти: https://dzone.com/articles/difference-between-currying-amp-partial-applied

Каррінг: розкладання функцій з декількома аргументами на ланцюг одноаргументних функцій. Зверніть увагу, що Scala дозволяє передавати функцію як аргумент іншій функції.

Часткове застосування функції: передати функції меншу кількість аргументів, ніж вона має у своєму оголошенні. Scala не створює виняток, коли ви надаєте меншу кількість аргументів функції, вона просто застосовує їх і повертає нову функцію з рештою аргументів, які потрібно передати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.