Scala curry a funkcje częściowo stosowane

82

Zdaję sobie sprawę, że jest tu kilka pytań o to, czym są funkcje curry i częściowo stosowane, ale pytam, czym się różnią. Jako prosty przykład, oto funkcja curry do znajdowania liczb parzystych:

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)

Możesz więc napisać co następuje:

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

który powraca: List(2,4,6,8). Ale odkryłem, że mogę zrobić to samo w ten sposób:

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

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

co również zwraca: List(2,4,6,8).

Więc moje pytanie brzmi: jaka jest główna różnica między nimi i kiedy użyjesz jednego nad drugim? Czy to po prostu zbyt uproszczony przykład, aby pokazać, dlaczego jeden miałby być użyty nad drugim?

Eric
źródło
W przypadku częściowego zastosowania koszty reprezentowania wersji aktualnej i niepobieralnej w pamięci mogą być różne, więc może to mieć również wpływ na wydajność środowiska wykonawczego. (To znaczy, jeśli optymalizator nie jest wystarczająco inteligentny, aby wybrać optymalną reprezentację w obu przypadkach). Nie jestem jednak wystarczająco zaznajomiony ze Scalą, aby powiedzieć, jakie są dokładne różnice.
1
Spójrz na to: stackoverflow.com/questions/8063325/…
Utaal,
Uważam, że to wyjaśnienie jest bardzo przydatne, wyjaśnia o funkcjach częściowych, częściowo zastosowanych funkcjach i curry, wszystko w jednym poście: stackoverflow.com/a/8650639/1287554
Plasty Grove
Doskonały link, @PlastyGrove. Dziękuję Ci!
Eric,
Dziękuję @Utaal za link. Każda odpowiedź samego Martina Oderskiego jest bardzo cenna. Myślę, że te koncepcje zaczynają się teraz pojawiać.
Eric,

Odpowiedzi:

88

Różnica semantyczna została dość dobrze wyjaśniona w odpowiedzi, do której odnosi się Plasty Grove .

Pod względem funkcjonalności nie wydaje się jednak dużej różnicy. Spójrzmy na kilka przykładów, aby to sprawdzić. Po pierwsze, normalna funkcja:

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

Otrzymujemy więc częściowo zastosowaną, <function1>która przyjmuje an Int, ponieważ podaliśmy już pierwszą liczbę całkowitą. Jak na razie dobrze. Teraz do curry:

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

Przy takim zapisie można by naiwnie oczekiwać, że zadziała:

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)

Więc notacja listy wielu parametrów tak naprawdę nie tworzy funkcji curried od razu (zakładając, że ma to na celu uniknięcie niepotrzebnego narzutu), ale czeka, aż wyraźnie stwierdzisz, że chcesz, aby była ona curried (notacja ma również inne zalety ):

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

To jest dokładnie to samo, co wcześniej, więc nie ma tu żadnej różnicy, z wyjątkiem notacji. Inny przykład:

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

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

To pokazuje, jak częściowe zastosowanie „normalnej” funkcji daje w wyniku funkcję, która przyjmuje wszystkie parametry, podczas gdy częściowe zastosowanie funkcji z wieloma listami parametrów tworzy łańcuch funkcji, jednej na listę parametrów, które wszystkie zwracają nową funkcję:

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.

Jak widać, ponieważ pierwsza lista parametrów programu fooma dwa parametry, pierwsza funkcja w łańcuchu curry ma dwa parametry.


Podsumowując, częściowo zastosowane funkcje nie różnią się tak naprawdę od funkcji zależnych od funkcjonalności. Można to łatwo zweryfikować, biorąc pod uwagę, że możesz przekonwertować dowolną funkcję na curried:

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

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

Post Scriptum

Uwaga: powód, dla którego Twój przykład println(filter(nums, modN(2))działa bez podkreślenia pomodN(2) wydaje się być to, że kompilator Scala po prostu zakłada, że ​​podkreślenie to jest wygodą dla programisty.


Dodanie: Jak słusznie zauważył @asflierl, Scala nie wydaje się być w stanie wywnioskować typu przy częściowym stosowaniu „normalnych” funkcji:

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

Podczas gdy te informacje są dostępne dla funkcji napisanych przy użyciu notacji listy wielu parametrów:

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

Ta odpowiedź pokazuje, jak może to być bardzo przydatne.

fresskoma
źródło
jedną ważną różnicą jest to, że wnioskowanie o typie działa inaczej: gist.github.com/4529020
Dzięki, dodałem notatkę dotyczącą twojego komentarza :)
fresskoma
19

Currying ma do czynienia z krotkami: zamienianiem funkcji, która przyjmuje argument krotki, w taką, która przyjmuje n oddzielnych argumentów i odwrotnie . Pamiętanie o tym jest kluczem do odróżnienia curry od częściowego zastosowania, nawet w językach, które nie obsługują w pełni curry.

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.

Zastosowanie częściowe to możliwość zastosowania funkcji do niektórych argumentów, dając nową funkcję dla pozostałych argumentów .

Łatwo to zapamiętać, jeśli po prostu pomyślisz, że curry jest transformacją związaną z krotkami.

W językach, które są domyślnie curried (takich jak Haskell) różnica jest oczywista - musisz coś zrobić, aby przekazać argumenty w krotce. Ale większość innych języków, w tym Scala, jest domyślnie nieuruszana - wszystkie argumenty są przekazywane jako krotki, więc curry / uncurry jest znacznie mniej użyteczne i mniej oczywiste. A ludzie nawet myślą, że częściowa aplikacja i curry to to samo - tylko dlatego, że nie mogą łatwo przedstawić funkcji curry!

Don Stewart
źródło
W pełni się zgadzam. W terminologii Scala „curry” w pierwotnym znaczeniu tego słowa to „proces przekształcania” funkcji z jedną listą parametrów w funkcję z wieloma listami parametrów. W Scali transformację tę można wykonać za pomocą ".curried". Niestety, wydaje się, że Scala trochę przeładowała znaczenie tego słowa, ponieważ pierwotnie byłoby raczej nazywane „.curry” zamiast „.curried”.
Fatih Coşkun
2

Funkcja wielu zmiennych:

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

Curry (lub funkcja curry):

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

Nie jest to więc funkcja stosowana częściowo, porównywalna z curry. To funkcja wielu zmiennych. To, co można porównać do funkcji częściowo zastosowanej, to wynik wywołania funkcji bieżącej, która jest funkcją z taką samą listą parametrów, jaką ma funkcja zastosowana częściowo.

lcn
źródło
0

Żeby wyjaśnić ostatni punkt

Dodanie: Jak słusznie zauważył @asflierl, Scala nie wydaje się być w stanie wywnioskować typu przy częściowym stosowaniu „normalnych” funkcji:

Scala może wywnioskować typy, jeśli wszystkie parametry są symbolami wieloznacznymi, ale nie wtedy, gdy niektóre z nich są określone, a niektóre nie.

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
źródło
0

Najlepsze wyjaśnienie, jakie udało mi się do tej pory znaleźć: https://dzone.com/articles/difference-between-currying-amp-partually-applied

Curry: rozkład funkcji z wieloma argumentami na łańcuch funkcji jednoargumentowych. Zauważ, że Scala pozwala na przekazanie funkcji jako argumentu do innej funkcji.

Częściowe zastosowanie funkcji: przekazanie do funkcji mniej argumentów niż ma w deklaracji. Scala nie zgłasza wyjątku, gdy podajesz mniej argumentów do funkcji, po prostu stosuje je i zwraca nową funkcję z resztą argumentów, które muszą zostać przekazane.

Danylo Zatorsky
źródło