Funktory - traktowanie obiektów jak funkcje w Common Lisp

Funktorem nazywamy obiekt, który może być wywoływany jak funkcja. Często chcemy też, by wywołanie takiego funktora przypominało składnią wywołanie normalnej funkcji w danym języku programowania. W niektórych językach (np. w C++, o czym niedawno pisałem) funktorów używa się do naprawienia braku naturalnego wsparcia dla funkcji first-class. Jednak Common Lisp takie wsparcie ma, więc zasadnicze pytanie brzmi: po co nam funktory w języku, w którym funkcja jest takim samym typem danych jak każdy inny? Zadajmy sobie inne pytanie - co robimy z funktorami?
  1. Przekazujemy je do funkcji wyższego rzędu, takich jak map czy reduce.
  2. Ponieważ są to obiekty, możemy trzymać w nich dodatkowe informacje lub umożliwiać wykonywanie na nich dodatkowych operacji.
Wyobraźmy sobie, że piszemy bibliotekę do obliczania minimum zadanej funkcji matematycznej*. Niektóre metody potrzebują jedynie znać wartości naszej funkcji w kilku punktach. Inne potrzebują także pochodnych lub gradientu. Chcielibyśmy takie informacje trzymać "razem" z naszą funkcją, móc uzyskać do nich dostęp w razie potrzeby (na tym etapie nie zastanawiamy się nad sposobem ich wyliczenia czy przechowywania). Nasza matematyczna funkcja powinna więc być obiektem. Z drugiej strony, chcemy mieć możliwość użycia jej jak normalnej funkcji w języku programowania - choćby po to, by nie mnożyć dziwnej składni tylko dlatego, że chcemy "na boku" przechowywać dodatkowe informacje. W C++ przeładowalibyśmy operator() - spełniając w ten sposób wymagania 1) i 2) naszego funktora. Deklaracja i użycie wyglądałyby na przykład tak:
class MathFunction
{
//...
public:
	MathFunction FirstDerivative(); //funkcja zwracajaca nam pierwsza pochodna
	double operator() (double x); //funkcja zwracajaca nam wartosc w punkcie
};

//uzycie
MathFunction funkcja;
double wynik = funkcja(100.0);
std::for_each(dane.begin(), dane.end(), funkcja);
Powiedzmy raz jeszcze, co chcemy uzyskać w Lispie - chcielibyśmy mieć obiekt funkcja, który dałoby się wywołać jak funkcję, pisząc: (funcall funkcja 100)**, oraz przekazać do funkcji wyższego rzędu, np.: (map 'list funkcja dane), ale równocześnie chcemy uzyskiwać z niego pewne informacje, np. (first-derivative funkcja). Rozwiązaniem jest użycie Metaobject Protocol do zmiany typu klasy z standard-class na funcallable-standard-class. Przykładowo (w Clozure Common Lisp***):
(defclass math-function ()
  () ; tutaj mozemy umieszczac sloty, czyli zmienne skladowe
  (:metaclass ccl:funcallable-standard-class))
Definiujemy również metodę zwracającą pierwszą pochodną:
(defgeneric first-derivative ()
  (:documentation "Returns analytical first derivative of a function."))

(defmethod first-derivative ((f math-function))
  (...)) ; kod metody pomijam
Od teraz obiekty klasy math-object są "funcallable" - mogą być przekazane do funcall, co oznacza, że mogą być argumentami funkcji wyższego rzędu. Spełniamy tym samym wymagania 1) i 2) naszego funktora. Pozostaje tylko pytanie, jaki kod wykona "wywołanie" naszego obiektu? Kod ten możemy ustawić za pomocą set-funcallable-instance-function. Wygodnie jest zrobić to w konstruktorze. W Lispie, konstruktor klasy tworzymy dodając metodę after o nazwie initialize-instance.
(defmethod initialize-instance :after ((f math-function) &key code)
  (set-funcallable-instance-function f code))
Dodaliśmy parametr kluczowy code, który pozwala nam tworzyć obiekty w następujący sposób:
; stworzony obiekt zapisujemy do zmiennej square
(setq square (make-instance 'math-function :code (lambda (x) (* x x))))

; uzycie:
(funcall square 3)
;wynik: 9
Jeżeli chcemy, aby nasz funktor był widziany jak funkcja globalna i używany ze składnią normalnego wywołania funkcji, możemy napisać:
(setf (fdefinition 'square) square)

; ponizszy kod teraz bedzie dzialal:
(square 3)
; wynik: 9
Wydaje się, że konieczność zaprzęgania Metaobject Protocol sprawia, iż stworzenie funktora jest czymś skomplikowanym w stosunku do np. C++. Należy jednak pamiętać, funktory w Lispie zwykle nie są potrzebne - same funkcje są first-class i można je przekazywać oraz zwracać jak każdy inny typ. Konieczność łączenia z funkcjami dodatkowych danych i operacji jest - wydaje mi się - czymś rzadkim. I nawet nie jestem pewien, czy nie znalazłaby się lepsza abstrakcja dla tego problemu. Przypisy * - problem nie jest wcale taki błahy ani wzięty z powietrza - w tym semestrze miałem na studiach cały przedmiot poświęcony znajdywaniu minimum funkcji na różne, niekiedy dość dziwne sposoby. ** - składnia (funcall funkcja argumenty) jest naturalnym sposobem wywoływania funkcji przekazanej w zmiennej w Common Lisp. Język ten, w przeciwieństwie do Scheme, pozwala funkcjom i zmiennym nosić te same nazwy - dlatego nie możemy napisać po prostu (przekazana-funkcja argumenty) (tak jak zrobilibyśmy w Scheme), gdyż Lisp będzie szukał symbolu przekazana-funkcja w przestrzeni nazw funkcji. *** - jest trochę zamieszania z Metaobject Protocol w różnych implementacjach Common Lispu. Sam symbol funcallable-standard-class znajduje się w pakiecie specyficznym dla danej implementacji. Dla Clozure Common Lisp jest to pakiet CCL (tak jak w przykładzie), w SBCL prawdopodobnie SB-MOP. Powstał projekt Closer mający na celu ujednolicenie Metaobject Protocol pomiędzy różnymi implementacjami Common Lispu, ale nie miałem okazji jeszcze z niego korzystać.