Αναδρομή#
Οι subs μπορούν να καλούν τον εαυτό τους, η μία την άλλη, ή - με λίγη τελετουργία - να αντικαθιστούν το δικό τους πλαίσιο κλήσης και να περνούν τον έλεγχο αλλού χωρίς να μεγαλώνει η στοίβα. Αυτή η σελίδα καλύπτει τους τέσσερις μηχανισμούς: απλή αναδρομή, αμοιβαία αναδρομή, κλήσεις ουράς goto &fn, και __SUB__ για ανώνυμη αυτο-αναφορά. Καλύπτει επίσης το όριο βάθους στο οποίο θα φτάσετε αν τα αγνοήσετε.
Απλή αναδρομή#
sub factorial {
my ($n) = @_;
return 1 if $n <= 1;
return $n * factorial($n - 1);
}
factorial(10); # 3628800
Κάθε αναδρομική κλήση προσθέτει ένα νέο πλαίσιο στη στοίβα κλήσεων της Perl. Η στοίβα κοστίζει μνήμη και ένα σταθερό κόστος ανά κλήση· για ρηχή αναδρομή αυτό είναι αμελητέο, για βαθιά αναδρομή είναι το σημείο συμφόρησης.
Η Perl εκπέμπει μια προειδοποίηση «Deep recursion on subroutine X» όταν μια sub αναδράμει πέρα από 100 επίπεδα βάθους (κατώφλι μεταγλώττισης, PERL_SUB_DEPTH_WARN). Η προειδοποίηση ανήκει στην κατηγορία recursion - η no warnings 'recursion' την αποσιωπά. Το μόνο σκληρό όριο είναι η στοίβα του OS, τυπικά 8 MiB εξ ορισμού, ρυθμιζόμενο με ulimit -s.
Για βαθιούς, δομικά αναδρομικούς φόρτους εργασίας, προτιμήστε ένα από τα:
Μια ρητή ουρά εργασίας (επαναδιατύπωση σε επαναληπτική μορφή).
Αναδρομή ουράς μέσω
goto &fn(βλέπε παρακάτω).Ένα trampoline (βλέπε παρακάτω).
Αμοιβαία αναδρομή#
Δύο subs που καλούν η μία την άλλη:
sub is_even {
my ($n) = @_;
return 1 if $n == 0;
return is_odd($n - 1);
}
sub is_odd {
my ($n) = @_;
return 0 if $n == 0;
return is_even($n - 1);
}
is_even(10); # 1
Η αμοιβαία αναδρομή δεν έχει ειδική μεταχείριση από τον χρόνο εκτέλεσης - είναι απλώς δύο subs που τυχαίνει να καλούν η μία την άλλη. Ισχύει το ίδιο όριο βάθους.
Αν η is_even ορίζεται πρώτη και αναφέρεται στην is_odd πριν υπάρξει η is_odd, θα δείτε «Bareword not allowed» ή «Called undefined subroutine» ανάλογα με τη μορφή κλήσης. Προδηλώστε για να το διορθώσετε:
sub is_odd; # forward declaration
sub is_even { ... is_odd($n-1) }
sub is_odd { ... is_even($n-1) }
goto &fn - προώθηση κλήσης ουράς#
Η μορφή goto &SUBROUTINE αντικαθιστά το τρέχον πλαίσιο με μια κλήση σε άλλη sub. Το τρέχον πλαίσιο εξαφανίζεται - δεν βρίσκεται στη στοίβα - οπότε η κλήση δεν μεγαλώνει τη στοίβα:
sub api_v2 {
my @args = @_;
push @args, deprecated => 1;
@_ = @args;
goto &api_v1; # tail-call: api_v1 sees the modified @_
}
Αυτό είναι το σωστό εργαλείο όταν:
Μια sub-περιτύλιγμα χρειάζεται να προωθήσει σε πραγματική υλοποίηση χωρίς να αφήσει πλαίσιο στοίβας πίσω. Decorators, shims καταγραφής, AUTOLOAD dispatchers.
Μια αναδρομική sub μπορεί να εκφράσει την αναδρομή της ως «κάνε κάποια δουλειά, μετά γίνε αυτή η άλλη κλήση». Κλασική αναδρομή ουράς:
sub factorial_tail { my ($n, $acc) = @_; $acc //= 1; return $acc if $n <= 1; @_ = ($n - 1, $n * $acc); goto &factorial_tail; } factorial_tail(20); # 2432902008176640000, no stack growth
Η μηχανική: το @_ γίνεται ο πίνακας ορισμάτων της καλούμενης sub, το τρέχον πλαίσιο ξετυλίγεται, ο έλεγχος πηδά. Ο καλών της τρέχουσας sub θα δει το αποτέλεσμα της sub στην οποία έγινε goto, ποτέ αυτής που κάλεσε.
Η goto &fn είναι η μόνη μορφή goto που εκτελεί κλήση ουράς. Η μορφή βασισμένη σε ετικέτα (goto LABEL) είναι άσχετη και πολύ πιο περιορισμένη.
Δείτε goto για πλήρη περιγραφή της δεσμευμένης λέξης και τους περιορισμούς μορφής.
__SUB__ - ανώνυμη αυτο-αναφορά#
use feature 'current_sub'; # or use v5.16
my $factorial = sub {
my ($n) = @_;
return 1 if $n <= 1;
return $n * __SUB__->($n - 1);
};
$factorial->(5); # 120
Η __SUB__ αποτιμάται σε αναφορά κώδικα προς την τρέχουσα εκτελούμενη sub. Είναι ο μόνος τρόπος να κάνετε αναδρομή σε ανώνυμη sub χωρίς να την αποθηκεύσετε πρώτα σε ονομαστική μεταβλητή (και χωρίς να διαρρέει ισχυρή αναφορά προς αυτή μέσα από τον εαυτό της, η κλασική μορφή διαρροής μνήμης).
Για ανώνυμες subs που δεν χρειάζεται να κάνουν αναδρομή, προτιμήστε την απλή ανώνυμη μορφή. Η __SUB__ υπάρχει ειδικά για την περίπτωση αναδρομής.
Trampolines#
Όταν θέλετε αναδρομή με οριοθετημένη στοίβα αλλά η goto &fn είναι δύσχρηστη (επειδή η αναδρομή είναι αμοιβαία, ή διακλαδίζεται σε πολλές υπο-περιπτώσεις), το μοτίβο trampoline λειτουργεί:
sub trampoline {
my $next = shift;
while (ref $next eq 'CODE') {
$next = $next->();
}
return $next;
}
sub even {
my ($n) = @_;
return $n == 0 ? 1 : sub { odd($n - 1) };
}
sub odd {
my ($n) = @_;
return $n == 0 ? 0 : sub { even($n - 1) };
}
trampoline(even(10000)); # 1, no stack growth
Κάθε «αναδρομική» κλήση επιστρέφει ένα closure για να γίνει η επόμενη κλήση, αντί να καλεί τον εαυτό της απευθείας. Το trampoline οδηγεί τα closures σε βρόχο. Η στοίβα παραμένει σε βάθος 2 για πάντα.
Τα trampolines είναι βαρύτερα από τη goto &fn (μία δέσμευση closure ανά «κλήση»), αλλά γενικεύονται σε αυθαίρετη ροή ελέγχου - συμπεριλαμβανομένων continuations και κώδικα τύπου CPS. Για Perl παραγωγής, η goto &fn είναι η συνηθισμένη επιλογή· τα trampolines εμφανίζονται όταν η δομή της αναδρομής κάνει τη goto δυσεπίτακτη.
Το όριο βάθους, στην πράξη#
sub recurse {
my ($n) = @_;
return $n if $n <= 0;
return recurse($n - 1);
}
recurse(2000); # warning: Deep recursion on subroutine
# (default limit 1000)
Η προειδοποίηση «Deep recursion» είναι ενημερωτική, όχι μοιραία - εκπέμπεται μία φορά ανά sub. Θα εξαντλήσετε τον πραγματικό χώρο στοίβας πολύ πριν φτάσετε κάποιο πρακτικό όριο στις περισσότερες πλατφόρμες. Αν δείτε αυτή την προειδοποίηση, η απάντηση είναι σχεδόν πάντα αναδιαμόρφωση (επαναληπτικός βρόχος, goto &fn, ή trampoline), όχι αύξηση του ορίου.
Αν έχετε ήδη επαληθεύσει ότι η αναδρομή είναι οριοθετημένη και απλώς θέλετε να αποσιωπήσετε τον θόρυβο, περιορίστε την κατηγορία προειδοποίησης:
no warnings 'recursion';
Αυτό αποσιωπά εντελώς την προειδοποίηση· δεν αλλάζει το όριο στοίβας του OS, το οποίο είναι αυτό που τελικά τερματίζει μια ανεξέλεγκτη αναδρομή ανεξάρτητα από τις προειδοποιήσεις.
Δείτε επίσης#
goto- η δεσμευμένη λέξη πίσω από τηgoto &fn.caller- λειτουργεί μέσα από πλαίσια μετά από goto με την ίδια αρίθμηση όπως τα κανονικά πλαίσια.Ορίσματα και
@_- σημασιολογία@_υπόgoto &fn.Δήλωση - ανώνυμες subs, το υπόστρωμα για
__SUB__και trampolines.Εμβέλεια - closures, το υπόστρωμα για trampolines.