Czy funkcja lub makro może określać ostrzeżenia kompilatora bajtowego?

15

Piszę funkcję, która w zasadzie przyjmuje dowolną liczbę argumentów. W praktyce należy jednak podawać parzystą liczbę argumentów, w przeciwnym razie przyniosą niepożądane rezultaty.

Oto fałszywy przykład kontekstu:

(defun my-caller (&rest args)
  (while args
    (call-other-function (pop args) (pop args))))

Gdy plik elisp jest kompilowany bajtowo, kompilator bajtów generuje ostrzeżenie, gdy widzi funkcję wywoływaną z niewłaściwą liczbą argumentów. Oczywiście, nigdy tak się nie stanie my-caller, ponieważ jest zdefiniowane, aby przyjmować dowolną liczbę.

Może jednak istnieje właściwość symbolu, którą mogę ustawić, lub (declare)formę, którą mogę dodać do jej definicji. Coś, co powiadomi użytkownika, że ​​ta funkcja powinna mieć tylko parzystą liczbę argumentów.

  1. Czy istnieje sposób poinformowania kompilatora bajtów o tym ograniczeniu?
  2. Jeśli nie, czy jest to możliwe za pomocą makra zamiast funkcji?
Malabarba
źródło
„... gdy widzi funkcję wywoływaną z niewłaściwą liczbą argumentów”?
itsjeyd

Odpowiedzi:

13

EDYCJA : Lepszym sposobem na zrobienie tego w ostatnich Emacsach jest zdefiniowanie makra kompilatora w celu sprawdzenia liczby argumentów. Moja oryginalna odpowiedź przy użyciu normalnego makra została zachowana poniżej, ale makro-kompilator jest lepsze, ponieważ nie zapobiega przekazaniu funkcji do funcalllub applyw czasie wykonywania.

W najnowszych wersjach Emacsa możesz to zrobić, definiując makro kompilatora dla swojej funkcji, która sprawdza liczbę argumentów i wyświetla ostrzeżenie (lub nawet błąd), jeśli się nie zgadza. Jedyną subtelnością jest to, że makro kompilatora powinno zwrócić oryginalny formularz wywołania funkcji bez zmian w celu oceny lub kompilacji. Odbywa się to za pomocą &wholeargumentu i zwracania jego wartości. Można to osiągnąć w następujący sposób:

(require 'cl-lib)

(defun my-caller (&rest args)
  (while args
    (message "%S %S" (pop args) (pop args))))

(define-compiler-macro my-caller (&whole form &rest args)
  (when (not (cl-evenp (length args)))
    (byte-compile-warn "`my-caller' requires an even number of arguments"))
  form)

(my-caller 1 2 3 4)
(my-caller 1 2)
(funcall #'my-caller 1 2 3 4)       ; ok
(apply #'my-caller '(1 2))          ; also ok
(my-caller 1)                       ; produces a warning
(funcall #'my-caller 1 2 3)         ; no warning!
(apply #'my-caller '(1 2 3))        ; also no warning

Należy pamiętać, że funcalli applymogą być teraz używane, ale obejście kontroli argument makra kompilatora. Pomimo swojej nazwy, kompilator makr również wydają się być rozszerzona w toku „interpretować” poprzez ocenę C-xC-e, M-xeval-buffertak dostaniesz błędy w zakresie oceny, jak również na przygotowanie tego przykładem.


Oryginalna odpowiedź brzmi:

Oto, w jaki sposób możesz wdrożyć sugestię Jordona, aby „użyć makra, które zapewni ostrzeżenia w czasie ekspansji”. Okazuje się, że jest bardzo łatwe:

(require 'cl-lib)

(defmacro my-caller (&rest args)
  (if (cl-evenp (length args))
      `(my-caller--function ,@args)
    (error "Function `my-caller' requires an even number of arguments")))

(defun my-caller--function (&rest args)
  ;; function body goes here
  args)

(my-caller 1 2 3 4)
(my-caller 1 2 3)

Próba skompilowania powyższego w pliku nie powiedzie się (żaden .elcplik nie zostanie wygenerowany), z ładnym klikalnym komunikatem o błędzie w dzienniku kompilacji, informującym:

test.el:14:1:Error: `my-caller' requires an even number of arguments

Można również wymienić (error …)z (byte-compile-warn …)produkować ostrzeżenie zamiast błędu, pozwalając kompilacja, aby kontynuować. (Podziękowania dla Jordona za wskazanie tego w komentarzach).

Ponieważ makra są rozszerzane w czasie kompilacji, z tym sprawdzeniem nie wiąże się żadna kara w czasie wykonywania. Oczywiście nie możesz powstrzymać innych osób dzwoniących my-caller--functionbezpośrednio, ale możesz przynajmniej reklamować ją jako funkcję „prywatną”, stosując konwencję podwójnego łącznika.

Istotną wadą używania makra do tego celu jest to, że my-callernie jest on już funkcją pierwszej klasy: nie można przekazać go do funcalllub applyw czasie wykonywania (lub przynajmniej nie zrobi tego, czego oczekujesz). Pod tym względem to rozwiązanie nie jest tak dobre, jak możliwość deklarowania ostrzeżenia kompilatora dla prawdziwej funkcji. Oczywiście użycie applyi tak uniemożliwiłoby sprawdzenie liczby argumentów przekazywanych do funkcji w czasie kompilacji, więc być może jest to akceptowalny kompromis.

Jon O.
źródło
2
Ostrzeżenia dotyczące kompilacji są tworzone za pomocąbyte-compile-warn
Jordon Biondo,
Zastanawiam się teraz, czy można to bardziej skutecznie osiągnąć poprzez zdefiniowanie makra kompilatora dla funkcji. Wyeliminowałoby to wadę polegającą na braku dostępu do opakowania makra applylub funcalljego braku . Spróbuję i zmienię odpowiedź, jeśli zadziała.
Jon O.
11

Tak, możesz użyć byte-defop-compilerdo określenia funkcji, która kompiluje twoją funkcję, byte-defop-compilerma pewne wbudowane drobiazgi, które pomogą ci określić, że twoje funkcje powinny generować ostrzeżenia oparte na posiadaniu wielu argumentów.

Dokumentacja

Dodaj formularz kompilatora dla FUNKCJI. Jeśli funkcja jest symbolem, to zmienna „byte-SYMBOL” musi określać używany kod operacji. Jeśli funkcja jest listą, pierwszy element jest funkcją, a drugi element jest symbolem kodu bajtowego. Drugi element może być zerowy, co oznacza, że ​​nie ma kodu operacji. COMPILE-HANDLER to funkcja używana do kompilacji tego bajtu-operacji, lub mogą to być skróty 0, 1, 2, 3, 0-1 lub 1-2. Jeśli jest zero, to program obsługi to „bajt-kompilacja-SYMBOL.


Stosowanie

W twoim konkretnym przypadku możesz użyć jednego ze skrótów, aby zdefiniować, że twoja funkcja powinna mieć dwa argumenty.

(byte-defop-compiler my-caller 2)

Teraz Twoja funkcja wyświetli ostrzeżenia, gdy zostanie skompilowana z czymkolwiek innym niż 2 argumenty.

Jeśli chcesz podać bardziej szczegółowe ostrzeżenia i napisać własne funkcje kompilatora. Sprawdź byte-compile-one-argi inne podobne funkcje w bytecomp.el w celach informacyjnych.

Zauważ, że nie tylko określasz jakąś funkcję do obsługi sprawdzania poprawności, ale tak naprawdę kompilację. Ponownie funkcje kompilacji w bytecomp.el zapewnią ci dobre referencje.


Bezpieczniejsze trasy

Nie jest to coś, co widziałem udokumentowane lub omówione online, ale ogólnie powiedziałbym, że jest to odradzana droga do wyboru. Prawidłowa trasa (IMO) polegałaby na napisaniu defuntu za pomocą podpisów opisowych lub użyciu makra, które zapewni ostrzeżenia w czasie ekspansji, sprawdzania długości argumentów i używania byte-compile-warnlub errorpokazywania błędów. Korzystne może być również skorzystanie z funkcji eval-when-compilesprawdzania błędów.

Będziesz także musiał zdefiniować swoją funkcję, zanim zostanie ona kiedykolwiek użyta, a wywołanie to byte-defop-compilerbędzie musiało nastąpić, zanim kompilator przejdzie do rzeczywistych wywołań twojej funkcji.

Ponownie wydaje się, że tak naprawdę nie zostało to udokumentowane ani wskazane na podstawie tego, co widziałem (może się mylić), ale wyobrażam sobie, że wzorzec, który należy zastosować tutaj, to określenie pewnego rodzaju pliku nagłówkowego dla twojego pakietu, który jest pełen wielu pustych odrzuceń i wzywa do byte-defop-compiler. Zasadniczo byłby to pakiet wymagany przed skompilowaniem prawdziwego pakietu.

Opinia: Na podstawie tego, co wiem, co nie jest wiele, ponieważ właśnie się o tym wszystkim dowiedziałem, radziłbym wam nigdy tego nie robić. zawsze

Jordon Biondo
źródło
1
Powiązane: Istnieje bytecomp-simplify, który uczy kompilator bajtów dodatkowych ostrzeżeń.
Wilfred Hughes