JavaScript curry: jakie są praktyczne zastosowania?


172

Chyba jeszcze nie żałowałem curry. Rozumiem, co to robi i jak to zrobić. Po prostu nie mogę wymyślić sytuacji, w której bym go użył.

Gdzie używasz curry w JavaScript (lub gdzie używają go główne biblioteki)? Mile widziane są przykłady manipulacji DOM lub ogólnych przykładów programowania aplikacji.

Jedna z odpowiedzi wspomina o animacji. Funkcje takie jak slideUp, fadeInpobierają element jako argument i zwykle są funkcją curried zwracającą funkcję wyższego rzędu z domyślną wbudowaną funkcją animacji. Dlaczego jest to lepsze niż tylko stosowanie funkcji wyższego poziomu z niektórymi domyślnymi ustawieniami?

Czy są jakieś wady korzystania z niego?

Zgodnie z prośbą, oto kilka dobrych zasobów na temat curry JavaScript:

Dodam więcej, gdy pojawią się w komentarzach.


Tak więc, zgodnie z odpowiedziami, curry i ogólnie częściowe stosowanie są wygodnymi technikami.

Jeśli często „udoskonalasz” funkcję wysokiego poziomu, wywołując ją z tą samą konfiguracją, możesz użyć funkcji wyższego poziomu (lub użyć częściowej Resiga), aby utworzyć proste, zwięzłe metody pomocnicze.


czy możesz dodać link do zasobu opisującego, czym jest curry JS? samouczek lub wpis na blogu byłby świetny.
Eric Schoonover,

2
svendtofte.com ma długie wieści, ale jeśli pominiesz całą sekcję z „Szybkiego kursu w ML” i zaczniesz od nowa od „Jak pisać curried JavaScript”, stanie się świetnym wprowadzeniem do curry w js.
danio,

1
To dobry punkt wyjścia do zrozumienia, czym naprawdę jest curry i częściowa aplikacja: slid.es/gsklee/functional-programming-in-5-minutes
gsklee

1
Odsyłacz do strony svendtofte.comwygląda na martwą - znaleziono go na maszynie WayBack pod adresem web.archive.org/web/20130616230053/http://www.svendtofte.com/ ... Przepraszamy, blog.morrisjohns.com/javascript_closures_for_dummies wydaje się być niedostępny też
phatskat

1
BTW, wersja Resiga częściowa jest wadliwa (z pewnością nie „na pieniądze”), ponieważ prawdopodobnie zakończy się niepowodzeniem, jeśli jeden z wstępnie zainicjowanych („curried”) argumentów otrzyma wartość undefined . Każdy zainteresowany dobrą funkcją curry powinien otrzymać oryginał z funcitonal.js Olivera Steele'a , ponieważ nie ma tego problemu.
RobG

Odpowiedzi:


35

@Hank Gay

W odpowiedzi na komentarz EmbiggensTheMind:

Nie przychodzi mi do głowy przypadek, w którym curry - samo w sobie - byłoby przydatne w JavaScript; jest to technika konwersji wywołań funkcji z wieloma argumentami do łańcuchów wywołań funkcji z jednym argumentem dla każdego wywołania, ale JavaScript obsługuje wiele argumentów w jednym wywołaniu funkcji.

W JavaScript - i zakładam, że większość innych rzeczywistych języków (nie rachunek lambda) - jest jednak powszechnie kojarzona z częściowym zastosowaniem. John Resig wyjaśnia to lepiej , ale istota jest taka, że ​​ma pewną logikę, która zostanie zastosowana do dwóch lub więcej argumentów, a znasz tylko wartości niektórych z tych argumentów.

Możesz użyć częściowej aplikacji / currying, aby naprawić te znane wartości i zwrócić funkcję, która akceptuje tylko niewiadome, do wywołania później, gdy faktycznie masz wartości, które chcesz przekazać. Zapewnia to sprytny sposób na uniknięcie powtarzania się, gdy w kółko wywoływałbyś te same wbudowane funkcje JavaScript z tymi samymi wartościami z wyjątkiem jednej. Aby ukraść przykład Johna:

String.prototype.csv = String.prototype.split.partial(/,\s*/);
var results = "John, Resig, Boston".csv();
alert( (results[1] == "Resig") + " The text values were split properly" );


6
To naprawdę zła odpowiedź. Currying nie ma nic wspólnego z częściową aplikacją. Currying umożliwia komponowanie funkcji. Kompozycja funkcji umożliwia ponowne wykorzystanie funkcji. Ponowne użycie funkcji zwiększa łatwość utrzymania kodu. To jest takie proste!

2
@ftor sir, jesteś bardzo złą odpowiedzią. Currying ma oczywiście na celu uatrakcyjnienie funkcji. Wyraźnie przegapiłeś punkt.
Callat

112

Oto interesujące I praktyczne zastosowanie curry w JavaScript, które używa domknięć :

function converter(toUnit, factor, offset, input) {
    offset = offset || 0;
    return [((offset + input) * factor).toFixed(2), toUnit].join(" ");
}

var milesToKm = converter.curry('km', 1.60936, undefined);
var poundsToKg = converter.curry('kg', 0.45460, undefined);
var farenheitToCelsius = converter.curry('degrees C', 0.5556, -32);

milesToKm(10);            // returns "16.09 km"
poundsToKg(2.5);          // returns "1.14 kg"
farenheitToCelsius(98);   // returns "36.67 degrees C"

Opiera się to na curryrozszerzeniu Function, chociaż jak widać, używa tylko apply(nic nadzwyczajnego):

Function.prototype.curry = function() {
    if (arguments.length < 1) {
        return this; //nothing to curry with - return function
    }
    var __method = this;
    var args = toArray(arguments);
    return function() {
        return __method.apply(this, args.concat([].slice.apply(null, arguments)));
    }
}

5
To jest świetne! Widzę to podobnie do cytatu lisp, który mówi: „Lisp jest programowalnym językiem programowania”
santiagobasulto

2
Ciekawe, ale ten przykład nie działa. offset+inputbędzie undefined + 1.60936na twoim milesToKmprzykładzie; co skutkuje NaN.
Nathan Long,

3
@Nathan - przesunięcie nie może być nieokreślone - domyślnie
wynosi

6
Z tego, co przeczytałem (przed chwilą), „curry” nie jest zwykle częścią zestawu sztuczek funkcji, chyba że używasz biblioteki Prototype lub dodajesz ją samodzielnie. Ale bardzo fajnie.
Roboprog

11
To samo można osiągnąć za pomocą metody bind () ES5. Bind tworzy nową funkcję, która po wywołaniu wywołuje oryginalną funkcję z kontekstem jej pierwszego argumentu i kolejną sekwencją argumentów (poprzedzającą każdą przekazaną do nowej funkcji). Możesz więc zrobić ... var mileToKm = converter.bind (this, 'km', 1.60936); lub var farenheitToCelsius = converter.bind (this, 'stopnie C', 0,5556, -32); Pierwszy argument, kontekst, to, jest tutaj nieistotny, więc możesz po prostu przekazać undefined. Oczywiście musiałbyś rozszerzyć podstawowy prototyp funkcji o własną metodę wiązania dla rezerwy innej niż ES5
hacklikecrack

7

Znalazłem funkcje, które przypominają Pythona functools.partialbardziej przydatne w JavaScript:

function partial(fn) {
  return partialWithScope.apply(this,
    Array.prototype.concat.apply([fn, this],
      Array.prototype.slice.call(arguments, 1)));
}

function partialWithScope(fn, scope) {
  var args = Array.prototype.slice.call(arguments, 2);
  return function() {
    return fn.apply(scope, Array.prototype.concat.apply(args, arguments));
  };
}

Dlaczego chcesz go używać? Typowa sytuacja, w której chcesz tego użyć, ma miejsce, gdy chcesz powiązać thisfunkcję z wartością:

var callback = partialWithScope(Object.function, obj);

Teraz, gdy wywoływane jest wywołanie zwrotne, thiswskazuje na obj. Jest to przydatne w sytuacjach zdarzeń lub w celu zaoszczędzenia miejsca, ponieważ zwykle skraca kod.

Currying jest podobna do częściowej z tą różnicą, że funkcja, którą zwraca currying przyjmuje tylko jeden argument (o ile to rozumiem).


7

Zgadzanie się z Hankiem Gayiem - jest niezwykle przydatne w niektórych prawdziwych funkcjonalnych językach programowania - ponieważ jest to niezbędna część. Na przykład w Haskell po prostu nie możesz wziąć wielu parametrów do funkcji - nie możesz tego zrobić w czystym programowaniu funkcyjnym. Bierzesz jeden parametr na raz i rozbudowujesz swoją funkcję. W JavaScript jest to po prostu niepotrzebne, pomimo wymyślnych przykładów, takich jak „konwerter”. Oto ten sam kod konwertera, bez potrzeby curry:

var converter = function(ratio, symbol, input) {
    return (input*ratio).toFixed(2) + " " + symbol;
}

var kilosToPoundsRatio = 2.2;
var litersToUKPintsRatio = 1.75;
var litersToUSPintsRatio = 1.98;
var milesToKilometersRatio = 1.62;

converter(kilosToPoundsRatio, "lbs", 4); //8.80 lbs
converter(litersToUKPintsRatio, "imperial pints", 2.4); //4.20 imperial pints
converter(litersToUSPintsRatio, "US pints", 2.4); //4.75 US pints
converter(milesToKilometersRatio, "km", 34); //55.08 km

Żałuję, że Douglas Crockford w „JavaScript: The Good Parts” nie wspomniał o historii i faktycznym wykorzystaniu curry niż o swoich bezmyślnych uwagach. Przez długi czas po przeczytaniu tego byłem zdumiony, aż zacząłem studiować programowanie funkcjonalne i zdałem sobie sprawę, że to stąd pochodzi.

Po dłuższym namyśle stwierdzam, że istnieje jeden ważny przypadek użycia curry w JavaScript: jeśli próbujesz pisać przy użyciu czystych technik programowania funkcjonalnego przy użyciu JavaScript. Wydaje się jednak, że jest to rzadki przypadek użycia.


2
Twój kod jest znacznie łatwiejszy do zrozumienia niż Prisoner Zero i rozwiązuje ten sam problem bez curry ani niczego skomplikowanego. Masz dwa kciuki do góry, a on ma prawie 100. Idź do figury.
DR01D

2

To nie jest żadna magia ani nic ... po prostu przyjemny skrót do anonimowych funkcji.

partial(alert, "FOO!") jest równa function(){alert("FOO!");}

partial(Math.max, 0) koresponduje z function(x){return Math.max(0, x);}

Wywołania częściowe ( terminologia MochiKit . Myślę, że niektóre inne biblioteki nadają funkcjom metodę .curry, która robi to samo) wyglądają nieco ładniej i mniej hałaśliwie niż funkcje anonimowe.


1

Jeśli chodzi o biblioteki, które go używają, zawsze istnieje Functional .

Kiedy jest przydatny w JS? Prawdopodobnie w tym samym czasie jest przydatny w innych współczesnych językach, ale jedyny raz, kiedy go używam, to w połączeniu z częściowym zastosowaniem.


Dzięki, Hank - czy możesz rozwinąć, kiedy jest to ogólnie przydatne?
Dave Nolan

1

Powiedziałbym, że najprawdopodobniej cała biblioteka animacji w JS używa curry. Zamiast przekazywania dla każdego wywołania zestawu elementów, na które ma to wpływ, i funkcji, opisującej, jak element powinien się zachowywać, do funkcji wyższego rzędu, która zapewni wszystkie czynności związane z synchronizacją, jest to ogólnie łatwiejsze dla klienta do wydania, ponieważ niektóre publiczne API funkcja taka jak "slideUp", "fadeIn", która przyjmuje tylko elementy jako argumenty, a które są po prostu jakąś funkcją curried zwracającą funkcję wyższego rzędu z wbudowaną domyślną funkcją animacji.


Dlaczego lepiej jest curry funkcji higherup niż po prostu wywoływać ją z pewnymi wartościami domyślnymi?
Dave Nolan

1
Ponieważ jest o wiele bardziej modularna możliwość wykonania "operacji doMath" z dodawaniem / mnożeniem / kwadratem / modułem / innymi obliczeniami, niż wyobrażanie sobie wszystkich "domyślnych", które może obsługiwać wyższa funkcja.
gizmo

1

Oto przykład.

Instrumentuję kilka pól za pomocą JQuery, aby zobaczyć, co robią użytkownicy. Kod wygląda następująco:

$('#foo').focus(trackActivity);
$('#foo').blur(trackActivity);
$('#bar').focus(trackActivity);
$('#bar').blur(trackActivity);

(W przypadku użytkowników innych niż JQuery mówię, że za każdym razem, gdy kilka pól jest aktywowanych lub traconych, chcę wywołać funkcję trackActivity (). Mógłbym również użyć funkcji anonimowej, ale musiałbym ją zduplikować 4 razy, więc wyciągnąłem go i nadałem mu nazwę.)

Teraz okazuje się, że z jednym z tych pól trzeba postępować inaczej. Chciałbym móc przekazać parametr w jednym z tych wywołań do naszej infrastruktury śledzenia. Z curry mogę.


1

W innym języku funkcjonalnym funkcje JavaScript nazywane są lamda. Może być używany do tworzenia nowego interfejsu API (bardziej wydajnej lub złożonej funkcji) w oparciu o proste dane wejściowe innego programisty. Curry to tylko jedna z technik. Możesz go użyć do utworzenia uproszczonego interfejsu API w celu wywołania złożonego interfejsu API. Jeśli jesteś programistą, który używa uproszczonego interfejsu API (na przykład używasz jQuery do wykonywania prostych operacji), nie musisz używać curry. Ale jeśli chcesz stworzyć uproszczone API, curry jest twoim przyjacielem. Musisz napisać framework javascript (jak jQuery, mootools) lub bibliotekę, wtedy możesz docenić jego moc. Napisałem ulepszoną funkcję curry o godz http://blog.semanticsworks.com/2011/03/enhanced-curry-method.html. Nie potrzebujesz metody curry, aby robić curry, po prostu pomaga to robić, ale zawsze możesz to zrobić ręcznie, pisząc funkcję A () {}, aby zwrócić inną funkcję B () {}. Aby było ciekawiej, użyj funkcji B (), aby zwrócić inną funkcję C ().


1

Znam jego stary wątek, ale będę musiał pokazać, jak jest używany w bibliotekach javascript:

Wykorzystam bibliotekę lodash.js, aby konkretnie opisać te koncepcje.

Przykład:

var fn = function(a,b,c){ 
return a+b+c+(this.greet || '); 
}

Częściowe zastosowanie:

var partialFnA = _.partial(fn, 1,3);

Curry:

var curriedFn = _.curry(fn);

Wiążący:

var boundFn = _.bind(fn,object,1,3 );//object= {greet: ’!'}

stosowanie:

curriedFn(1)(3)(5); // gives 9 
or 
curriedFn(1,3)(5); // gives 9 
or 
curriedFn(1)(_,3)(2); //gives 9


partialFnA(5); //gives 9

boundFn(5); //gives 9!

różnica:

po curry otrzymujemy nową funkcję bez żadnych parametrów.

po częściowym zastosowaniu otrzymujemy funkcję, która jest związana z pewnymi parametrami.

w powiązaniu możemy powiązać kontekst, który zostanie użyty do zastąpienia 'this', jeśli nie jest powiązany, domyślną funkcją będzie zasięg okna.

Porada: nie ma potrzeby odkrywania na nowo koła. Częściowe zastosowanie / wiązanie / curry są bardzo powiązane. Różnicę widać powyżej. Używaj tego znaczenia wszędzie, a ludzie rozpoznają, co robisz, bez problemów ze zrozumieniem, a ponadto będziesz musiał używać mniej kodu.


0

Zgadzam się, że czasami chciałbyś wprawić w ruch, tworząc pseudo-funkcję, która zawsze będzie miała wypełnioną wartość pierwszego argumentu. Na szczęście trafiłem na zupełnie nową bibliotekę JavaScript o nazwie jPaq (h ttp: // jpaq.org/ ), który udostępnia tę funkcję. Najlepszą rzeczą w bibliotece jest to, że możesz pobrać własną kompilację, która zawiera tylko potrzebny kod.



0

Chciałem tylko dodać trochę zasobów dla Functional.js:

Wykład / konferencja wyjaśniająca niektóre aplikacje http://www.youtube.com/watch?v=HAcN3JyQoyY

Zaktualizowana biblioteka Functional.js: https://github.com/loop-recur/FunctionalJS Kilku fajnych pomocników (przepraszam, nowa tutaj, brak reputacji: p): / loop-recur / PreludeJS

Ostatnio często używam tej biblioteki, aby zmniejszyć liczbę powtórzeń w bibliotece pomocniczej klientów IRC js. To świetna rzecz - naprawdę pomaga oczyścić i uprościć kod.

Ponadto, jeśli wydajność stanie się problemem (ale ta biblioteka jest dość lekka), łatwo jest po prostu przepisać za pomocą funkcji natywnej.


0

Możesz użyć natywnego wiązania do szybkiego rozwiązania w jednej linii

function clampAngle(min, max, angle) {
    var result, delta;
    delta = max - min;
    result = (angle - min) % delta;
    if (result < 0) {
        result += delta;
    }
    return min + result;
};

var clamp0To360 = clampAngle.bind(null, 0, 360);

console.log(clamp0To360(405)) // 45


0

Kolejne uderzenie w to, od pracy z obietnicami.

(Uwaga: JS noob, pochodzący ze świata Pythona. Nawet tam curry nie jest używane zbyt często, ale czasami może się przydać. Więc połączyłem funkcję curry - zobacz linki)

Najpierw zaczynam od wywołania Ajax. Muszę wykonać pewne określone czynności w przypadku sukcesu, ale w przypadku niepowodzenia chcę tylko dać użytkownikowi informację zwrotną, że wywołanie czegoś spowodowało jakiś błąd . W moim rzeczywistym kodzie wyświetlam informacje o błędzie w panelu ładowania początkowego, ale używam tutaj tylko logowania.

Zmodyfikowałem mój adres URL na żywo, aby to się nie udało.

function ajax_batch(e){
    var url = $(e.target).data("url");

    //induce error
    url = "x" + url;

    var promise_details = $.ajax(
        url,
        {
            headers: { Accept : "application/json" },
            // accepts : "application/json",
            beforeSend: function (request) {
                if (!this.crossDomain) {
                    request.setRequestHeader("X-CSRFToken", csrf_token);
                }
        },
        dataType : "json",
        type : "POST"}
    );
    promise_details.then(notify_batch_success, fail_status_specific_to_batch);
}

Teraz, aby poinformować użytkownika, że ​​partia nie powiodła się, muszę zapisać tę informację w module obsługi błędów, ponieważ wszystko, co otrzymuje, to odpowiedź z serwera.

Wciąż mam informacje dostępne tylko w czasie kodowania - w moim przypadku mam kilka możliwych partii, ale nie wiem, która z nich nie powiodła się, analizując odpowiedź serwera o nieudanym adresie URL.

function fail_status_specific_to_batch(d){
    console.log("bad batch run, dude");
    console.log("response.status:" + d.status);
}

Zróbmy to. Dane wyjściowe konsoli to:

konsola:

bad batch run, dude utility.js (line 109) response.status:404

Teraz zmieńmy trochę rzeczy i użyj ogólnej procedury obsługi błędów wielokrotnego użytku, ale także takiej, która jest pobierana w czasie wykonywania z zarówno kontekstem wywołania znanego w czasie kodu, jak i informacjami w czasie wykonywania dostępnymi ze zdarzenia.

    ... rest is as before...
    var target = $(e.target).text();
    var context = {"user_msg": "bad batch run, dude.  you were calling :" + target};
    var contexted_fail_notification = curry(generic_fail, context); 

    promise_details.then(notify_batch_success, contexted_fail_notification);
}

function generic_fail(context, d){
    console.log(context);
    console.log("response.status:" + d.status);
}

function curry(fn) {
     var slice = Array.prototype.slice,
        stored_args = slice.call(arguments, 1);
     return function () {
        var new_args = slice.call(arguments),
              args = stored_args.concat(new_args);
        return fn.apply(null, args);
     };
}

konsola:

Object { user_msg="bad batch run, dude. you were calling :Run ACL now"} utility.js (line 117) response.status:404 utility.js (line 118)

Mówiąc bardziej ogólnie, biorąc pod uwagę, jak powszechne jest użycie wywołań zwrotnych w JS, currying wydaje się być całkiem przydatnym narzędziem.

https://javascriptweblog.wordpress.com/2010/04/05/curry-cooking-up-tastier-functions/ http://www.drdobbs.com/open-source/currying-and-partial-functions-in- javasc / 231001821? pgno = 2

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.