Κυριακή 29 Σεπτεμβρίου 2024

Αλγόριθμοι – Προγραμματισμός (τεύχος 1)

 

Αλγόριθμοι – Προγραμματισμός


Εισαγωγή

 

Στόχος αυτού του δοκιμίου είναι «Η ανάπτυξη της αλγοριθμικής σκέψης του παιδιού με την διαδικασία της κατασκευής προγραμμάτων σε ηλεκτρονικό υπολογιστή».

Όσο και να προσπαθούμε να πείσουμε τα παιδιά να «προγραμματίσουν» δεν θα βρούμε εύκολα ανταπόκριση.  Το πραγματικό πρόβλημα είναι η έλλειψη αφαιρετικής σκέψης. Με την αφαιρετική σκέψη το παιδί αντιλαμβάνεται το γενικό ενώ η αλγοριθμική σκέψη σπάει το πρόβλημα από το γενικό στο επιμέρους. Ο προγραμματισμός είναι η επιλογή εντολών μιας γλώσσας προγραμματισμού που εκτελεί αυτό που σε γενικές γραμμές λέει ο αλγόριθμος. Μπορούμε να δούμε τον αλγόριθμο ως το προσχέδιο του προγράμματος. Η γλώσσα προγραμματισμού αποτελεί το εργαλείο, που πραγματοποιεί τη σκέψη μας κατά την εκτέλεση στην υπολογιστική μηχανή, τον προσωπικό υπολογιστή (ορισμός από τη δεκαετία του ’80), ή ηλεκτρονικό υπολογιστή (ορισμός προ της δεκαετίας του ’80).

Για το σκοπό του δοκιμίου, την ανάπτυξη της αλγοριθμικής σκεψης του παιδιού,  θα προταθεί μια μέθοδος εκπαίδευσης, που στηρίζεται πρώτα στην αφαιρετική σκέψη, μετά στην αλγοριθμική σκέψη και τέλος στον προγραμματισμό.

Το δοκίμιο στοχεύει σε ενήλικες που θέλουν να δοκιμάσουν τρόπους μαθησιακής προσέγγισης ειδικότερα στον όρο της Αλγοριθμικής Σκέψης. Ο προγραμματισμός θα χρησιμοποιηθεί για την τεκμηρίωση της σκέψης.

Αφαιρετική Σκέψη

Κάθε παιδί την κάθε στιγμή έχει ένα βαθμό αφαιρετικής σκέψης, μια ικανότητα αφαίρεσης της λεπτομέρειας προς το γενικό, σ' αυτό που ταιριάζει. Όταν το παιδί παίζει με ένα «ψεύτικο» όπλο μάχη, φαίνεται σε μας σαν αφαίρεση, βάζοντας τον εαυτό του ως πολεμιστή, σ' αυτό που νομίζει ότι είναι πολεμιστής. Τι είναι «πολεμιστής» για το παιδί; Η απλή του σκέψη είναι ότι αυτός που έχει όπλο είναι πολεμιστής! Το παιδί δεν έχει καμία ιδέα περί εχθρού, απλά γενικά καθένας που μπαίνει στο χώρο του παιχνιδιού του μπορεί να του φανεί εχθρός!  Η αντίληψη του παιδιού περιορίζεται από την έλλειψη πληροφορίας ή και στοιχείων που του καθορίζουν την τωρινή κατάσταση.  Αυτή η έλλειψη δεν αποτελεί «Αφαίρεση». Όταν όμως ζητήσουμε από το παιδί να μας φέρει από τα παιχνίδια του να δούμε ό,τι παιχνιδι έχει με ρόδες, και αφού το κάνει, του πούμε πώς όλα αυτά τα λέμε οχήματα, τότε έχει ιδέα μιας γενικότητας, του όρου 《οχήματα, κι αυτό γίνεται άμεσα μέρος της δικής του αφαιρετικής σκέψης.

Η αφαιρετική σκέψη μαθαίνεται μέσα από τους ορισμούς και τη συμμετοχή των προσδιορισμών αυτών που δηλώνουν οι ορισμοί. Είναι ακριβώς το ανάποδο από την αλγοριθμική σκέψη, όπου θέλουμε ένα αποτέλεσμα, και για το οποίο ο τρόπος να το βρούμε απαιτεί να σκεφτούμε τα βήματα, ξεκινώντας από ορισμούς και καθορισμό της αρχικής κατάστασης.

Πρόβλημα - Παράδειγμα

Ένα απλό πρόβλημα είναι η εύρεση του αθροίσματος των ακεραίων αριθμών μεταξύ δυο ακεραίων αριθμών. Αν και τα ζητήματα δεν είναι πάντα μαθηματικά, εδώ για την ευκολία παρακολούθησης του θέματος μπήκε ένα μαθηματικό πρόβλημα.

Αφαιρετική Σκέψη περί Ακέραιων

Πρώτα πρέπει να ορίσουμε τι είναι ακέραιος αριθμός. Εδώ ο ορισμός λέει ότι οι ακέραιοι αριθμοί ανήκουν σε ένα σύνολο, όπου για κάθε Ν αριθμό υπάρχει ο Ν+1και ο Ν-1, με κεντρικό αριθμό το 0, ώστε για Ν=0 το Ν-1 είναι το -1 και το Ν+1 είναι το +1. Ο αριθμός 3 ανήκει στο σύνολο, γιατί βγαίνει με το 0+1+1+1 ή +3, ή απλά 3 (το + λέμε ότι εννοείται). Υπάρχουν αριθμοί που δεν ανήκουν στο σύνολο αυτό, όπως για παράδειγμα το μισό του 3, δηλαδή το ένα και μισό (όπου δυο φορές γίνεται: ένα και μισό και ένα και μισό άρα τρία), επειδή από το 0, για το σύνολο των ακεραίων, μπορούμε να προσθέτουμε μονάδες αλλά όχι μισή μονάδα!  Αυτός ο βαρύς, για τα παιδιά, ορισμός είναι προϋπόθεση για την κατανόηση της λύσης του προβλήματος. Η αφαίρεση εδώ είναι ότι το εκατό θα είναι 0+1+1….+1, δηλαδή εκατό φορές από το 0 αυξάνουμε κατά ένα, και δεν χρειάζεται να το αποδείξουμε, μας αρκεί το ότι δουλεύει η απόδειξη για 1, 2, 3. Επίσης, το συμπέρασμα του ορισμού είναι ότι για κάθε ξεχωριστό ακέραιο υπάρχει ο προηγούμενος κατά ένα και ο επόμενος κατά ένα, όπου και οι δυο είναι μοναδικοί! Έτσι για το 5 υπάρχει το 4 και το 6, ως προηγούμενος και επόμενος ακέραιος αντίστοιχα.

Για την επίλυση του προβλήματος, το άθροισμα των ακεραίων μεταξύ δυο ακεραίων, πρέπει να σκεφτούμε ότι από τον Α ακέραιο στον Β ακέραιο, εφόσον ο Α είναι μικρότερος του Β, θα υπάρχουν τόσοι ακέραιοι όσες μονάδες πρέπει να βάλουμε στον Α για να φτάσουμε στον Β χωρίς την τελευταία μονάδα. Αν ο Α είναι μεγαλύτερος του Β, τότε μπορούμε να αλλάξουμε τις τιμές τους και να έχουμε τον Α μικρότερο του Β.

Αλγοριθμική Σκέψη - Σχεδιασμός

Ένας τρόπος να ξεμπερδεύουμε γρήγορα με τον αλγοριθμικό σχεδιασμό είναι να κάνουμε μικρά βήματα ελέγχου, πριν ολοκληρώσουμε τη σκέψη μας. Αυτά τα βήματα ελέγχου γίνονται με την επιλογή των αρχικών συνθηκών. Έστω ότι έχουμε Α=4 και Β=5, πόσοι ακέραιοι είναι μεταξύ Α και Β (χωρίς τα Α και Β); Λέμε ότι από το Α στο Β χρειαζόμαστε μόνο μια μονάδα, και το σκεπτικό μας είναι να αφαιρέσουμε την τελευταία μονάδα, άρα το Α+1 είναι το Β, άρα οι μονάδες έχουν σύνολο 1 (είναι μια) και αφαιρούμε ένα, άρα έχουμε 0 ακέραιους. Έστω ότι έχουμε Α=2 και Β=5, τότε θα έχουμε τους 2+1, 2+1+1 ή 3 και 4 μεταξύ των 2 και 5, και με τον υπολογισμό μας θα έχουμε από το Α στο Β συνολικά Α+1+1+1 ή τρεις μονάδες και αφαιρούμε μια, άρα έχουμε δυο ακέραιους (3 και 4).

Τώρα μας ενδιαφέρει να προσθέσουμε τους ακέραιους. Στο προηγούμενο παράδειγμα για  Α=2 και Β=5 βρίσκουμε διαδοχικά τα 3 και 4 και κάθε φορά τα προσθέτουμε στη ΣΟΥΜΑ. Η ΣΟΥΜΑ έχει αρχική τιμή 0 (δεν έχουμε αθροίσει τίποτα). Κάθε φορά που βγαίνει ένας ακέραιος (από όσους υπάρχουν μεταξύ Α και Β) θα τον προσθέτουμε στη ΣΟΥΜΑ, δηλαδή στη ΣΟΥΜΑ βάζουμε νέα τιμή το άθροισμα ΣΟΥΜΑ συν Ακέραιος.

Τέλος στην αλγοριθμική μας σκέψη πρέπει να σκεφτούμε πότε ο αλγόριθμος δεν μπορεί να εκτελεστεί! Αν έχουμε Α=Β, για παράδειγμα, Α=4 και Β=4 δεν μπορούμε να ικανοποιήσουμε τη συνθήκη Α<Β (ή αν ήταν Β<Α θα αλλάζαμε τιμές μεταξύ τους στα Α και Β για να βγουν με τη σωστή σειρά, Α<Β). Η αντίθετη της Α=Β είναι η Α<>Β (το Α διάφορο του Β, είτε μεγαλύτερο, είτε μικρότερο)

Προγραμματισμός

Τώρα έχουμε όλα τα επιμέρους, για να φτιάξουμε το πρόγραμμα. Στο επίπεδο του προγραμματισμού υπάρχουν τεχνικά θέματα, τα οποία το παιδί θα δυσκολευτεί να ακολουθήσει.

Προϋποθέσεις για την ετοιμασία του προγράμματος είναι:

  1. Να έχουμε επιλέξει τα ονόματα των μεταβλητών, δηλαδή αυτών που θα κρατάνε τιμές. Εδώ οι μεταβλητές μας θα έχουν τα ονόματα Α, Β, Σούμα και μια ακόμα Ι όπου θα τη χρειαστούμε για να πάρει τιμές διαδοχικά από το Α+1 μέχρι και το Β-1.
  2. Να έχουμε επιλέξει τι θα εμφανιστεί ως πρώτο μήνυμα. Σε αυτό το βήμα: «Εύρεση αθροίσματος ακεραίων μεταξύ δυο ακεραίων Α και Β».
  3. Να έχουμε επιλέξει τις εντολές για εισαγωγή των Α και Β, μαζί με τις λεζάντες τους, όπως «Τιμή στο Α:» και «Τιμή στο Β:».
  4. Να κάνουμε έλεγχο, αν Α=Β και τοτε αν είναι Αληθής η σύγκριση, να τυπώσουμε «Αδύνατον, δεν έχουμε διαφορετικούς ακεραίους Α και Β» και μετά να κάνουμε Έξοδο από το πρόγραμμα.
  5. Να δούμε, αν το Β<Α (το Β μικρότερο του Α) και αν είναι Αληθής η σύγκριση, τότε να αλλάξουμε τιμές στα Α και Β και να το δείξουμε με τις λεζάντες όπως στο (3).
  6. Να κάνουμε έλεγχο Α+1=Β και αν είναι Αληθής η σύγκριση, να τυπώσουμε «Δεν υπάρχουν μεταξύ <τιμής Α> και <τιμής Β> ακέραιοι.  Εδώ μπορούμε να τυπώσουμε τη Σούμα που θα είναι μηδέν. Αν είναι Ψευδής ο έλεγχος, τότε πάμε στο (7), αλλιώς τερματίζει το πρόγραμμα
  7. Στο βήμα αυτό διατρέχουμε τις τιμές από Α+1 έως Β-1 μέσω της Ι, στην εντολή: Για Ι=Α+1 έως Β-1, και βάζουμε το κάθε Ι στη Σούμα με το Σούμα+=Ι (που είναι ίδιο με το Σούμα=Σούμα+Ι).
  8. Στο τέλος βάζουμε την εντολή Τύπωσε, η οποία θα μας δώσει τη Σούμα. Επίσης ενώ δεν το ζητάει η άσκηση, εδώ έχουμε βάλει το αποτέλεσμα χωρίς να διατρέχουμε τις τιμές και να τις αθροίζουμε, απ’ ευθείας με μαθηματικό τύπο.

Επισημάνσεις

Ο υπολογισμός με τον τύπο ((Β-Α)/2+Α)*(Β-Α-1) δε λειτουργεί, αν Α=Β ή Α>Β. 

Ο υπολογισμός βασίζεται στο σκεπτικό ότι το άθροισμα όλων των ακέραιων αριθμών από 1 έως Ν είναι το Ν*(Ν+1)/2. Για παράδειγμα το άθροισμα των 1, 2, 3 είναι το 3*4/2, και σχετίζεται με το μισό εμβαδόν ενός παραλληλόγραμμου  3 Χ 4.

Ο τύπος ((Β-Α)/2+Α)*(Β-Α-1) γράφεται (Β-Α)*(Β-Α-1/2+Α*(Β-Α-1), έτσι αν Α=3 και Β=6 τότε έχουμε (6-3)*(6-3-1)/2+3*(6-3-1), ή 3*2/2+3*2 ή 3+6 ή 9, και πράγματι μεταξύ Α και Β είναι οι ακέραιοι 4 και 5, με άθροισμα το 9.

Για Α=-3 και Β=11  θα έχουμε ((11--3)/2-3)*(11--3-1), ή (14/2-3)*13 ή 4*13 ή 52. Πράγματι αν προσθέσουμε -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 θα πάρουμε το 52.

 

Κώδικας σε διερμηνευτή Μ2000

 

Ακέραιος Α, Β, Ι
Μακρύς Σούμα
Τύπωσε "Εύρεση αθροίσματος ακεραίων μεταξύ δυο ακεραίων Α και Β"
Εισαγωγή "Τιμή στο Α:", Α
Εισαγωγή "Τιμή στο Β:", Β
Αν Α=Β Τότε Τύπωσε "Αδύνατον, δεν έχουμε διαφορετικούς ακέραιους Α και Β": Έξοδος
Αν Β<Α τότε
             Άλλαξε Α, Β
             Τύπωσε "Τιμή στο Α:"+Α
             Τύπωσε "Τιμή στο Β:"+Β
Τέλος Αν
Αν Α+1=Β Τότε
             Τύπωσε "Δεν υπάρχουν μεταξύ "+Α+" και "+Β+" ακέραιοι"
Αλλιώς
             Για Ι=Α+1 έως Β-1
                         Σούμα+=Ι
             Επόμενο
Τέλος Αν
Τύπωσε "Άθροισμα:"+Σούμα
Τύπωσε "Υπολογισμός:"+(((Β-Α)/2+Α)*(Β-Α-1))

 

Κώδικας σε διερμηνευτή Python


Α = Β = Ι = Σούμα = 0
print("Εύρεση αθροίσματος ακεραίων μεταξύ δυο ακεραίων Α και Β")
Α=int(input("Τιμή στο Α:"))
Β=int(input("Τιμή στο Β:"))
if Α==Β:
    print("Αδύνατον, δεν έχουμε διαφορετικούς ακέραιους Α και Β")
    exit()
if Β<Α:
    Α, Β = Β, Α
    print("Τιμή στο Α:", Α)
    print("Τιμή στο Β:", Β)
if Α+1==Β:
    print("Δεν υπάρχουν μεταξύ", Α, "και", Β, "ακέραιοι")
else:
    for Ι in range(Α+1, Β):
        Σούμα+=Ι
print("Άθροισμα:", Σούμα)
print("Υπολογισμός:", int(((Β-Α)/2+Α)*(Β-Α-1)))

Run python program here


Ευχαριστώ την κα Ισμήνη Κουρεμαδη (Φιλόλογο) για τη βοήθειά της στην επιμέλεια της παρούσας δημοσίευσης.


Γιώργος Καρράς

Version 12, Revision 36

A small bug fixed (one line of code), so now a copy of an event object copy the enable state (which we can alter using the keywords Hold and Release)

The example bellow didn't print number a, because the hold state copied to c.

event a {
read a
function {
print a
}
}
event a hold
c=a
call event c, 4


This is an example of use of an event object as a visitor. The pointer()  (used without parameter) return the null object (is a group with type Null, which we can't add anything, and we can merge to). Using the Null object we always have pointer to point something. This example has no use of Hold or Release on event, but we can insert Event PrintAnum Hold after the Event PrintAnum New PrintThis() to see what happen.

Report {
      Tree traversal
             1
            / \
           /   \
          /    \
         2      3
        / \    /
       4   5   6
      /      / \
     7      8   9

}
Pen 15 {Print "OOP - Event object as visitor"}
Module OOP {
    Class Node {
    Public:
        x, LeftNode=pointer(), RightNode=pointer()
        Module preorder (visitor){
            T->This
            printtree(T)
            sub printtree(T as pointer)
                If T is type Null then Exit sub
                call event visitor, T=>x
                printtree(T=>LeftNode)
                printtree(T=>RightNode)
            end sub
        }
        Module inorder (visitor){
            T->This
            printtree(T)
            sub printtree(T as pointer)
                If T is type Null then Exit sub
                printtree(T=>LeftNode)
                call event visitor, T=>x
                printtree(T=>RightNode)
            end sub
        }
        Module postorder (visitor) {
            T->This
            printtree(T)
            sub printtree(T as pointer)
                If T is type Null then Exit sub
                printtree(T=>LeftNode)
                printtree(T=>RightNode)
                call event visitor, T=>x
            end sub
        }
        Module level_order (visitor){
            T->This
            Stack New {
                printtree(T)
                if empty then exit
                Read T
                Loop
            }
            sub printtree(T as pointer)
                If T is type Null else
                  call event visitor, T=>x
                  Data T=>LeftNode, T=>RightNode
                end if
            end sub
        }
        remove {
          Print "Node removed ";.x
        }
    Class:
        \\ after class:  anything exist one time,
        \\ not included in final object
        Module Node {
            Read .x
            \\ read ? for optional values
            Read ? .LeftNode, .RightNode
        }
    }
    \\ NodeTree return a pointer to a new Node
    Function NodeTree {
        \\ ![] pass currrent stack to Node()
        ->Node(![])
    }

    Tree=NodeTree(1, NodeTree(2,NodeTree(4, NodeTree(7)), NodeTree(5)), NodeTree(3, NodeTree(6, NodeTree(8), NodeTree(9))))
    Event PrintAnum {
        read x
    }
    Function PrintThis(x) {
          Print x;" ";
    }
    Event PrintAnum New PrintThis()
    printnum=lambda PrintAnum (title$) -> {
        Print
        Print title$;
        =PrintAnum
    }
    Tree=>preorder printnum("preorder:  ")
    Tree=>inorder printnum("inorder:   ")
    Tree=>postorder printnum("postorder:   ")
    Tree=>level_order printnum("level-order: ")
    Print
    Print "Removing nodes like a preorder traversal"
}
OOP


Σάββατο 28 Σεπτεμβρίου 2024

M2000 - How to Call code (part 2)

This example has an old style, the use of lables/numbers, plus a modern one, call modules from Groups, like calling members of objects.

We have two examples:

The simple example without objects. Statement Goto jump to a line number (is a numeric label not actual the line number at editor), or a named label. Statement Gosub call code and expect a Return statement. Statement Return with parameters is a different statement from simple Return, and used to return items to specific data containers/tables of databases.

The module.name$ is the name of the current module, but not the actual inner name (which we can find it using module$, and is a compact string of the name space of current scope). 


Module UseOfLineNumbers {
Print module.name$
Goto 10
Print "You can't see this..."
// we can use line numbers, like numeric labels
10 A=5
20 Gosub 1000
30 A--
40 If A>0 then 20
50 End


1000 Print "Code at line number 1000", A : return
}
Module UseOfLineLabels {
Print module.name$
Goto StartLine
Print "You can't see this..."
// we can use line numbers, like numeric labels
StartLine:
10 A=5
again:
20 Gosub mysubroutine
30 A--
40 If A>0 then goto again
50 End
mysubroutine: // only comments in same line with label
1000 Print "Code at line number 1000", A : return
}
Module UseOfLineLabels2 {
Print module.name$
Goto StartLine
Print "You can't see this..."
// we can use line numbers, like numeric labels
StartLine:
A=5
again:
Gosub mysubroutine
A--
If A>0 then goto again
End
mysubroutine: // only comments in same line with label
Print "Code at line number 1000", A : return
}
UseOfLineNumbers
UseOfLineLabels
UseOfLineLabels2



The advanced example with objects. We have the Beta object, as a Group in the current module. The modules of Group Beta ara modules of the current module (they have .beta. between the current module name and the calling module's name. The modules of Delta exist in a closed area, and for use them we have to attach the object temporary, then call, then deattach it. We can use For object { }  block and do the attach once, then we call any number of modules, then at the exit of this block the deattach perform. The attach means that the inner lists of interpreter knows the modules,, by installing like those of Beta, and deattach means that the modules erased and the inner lists restored before th For object {}. 

Also we see the deconstructor of a group, which automatic executed when the object lost the last pointer to it. Also we see that the Beta, the group without a usable pointer (has a hidden one), erased at a Clear or at the exit of Module/Function etc, without call the constructor, unless we use Clear Beta as tbe last statement for Beta,

Class Alfa {
Module UseOfLineNumbers {
Print module.name$
Goto 10
Print "You can't see this..."
// we can use line numbers, like numeric labels
10 A=5
20 Gosub 1000
30 A--
40 If A>0 then 20
50 End

1000 Print "Code at line number 1000", A : return
}
Module UseOfLineLabels {
Print module.name$
Goto StartLine
Print "You can't see this..."
// we can use line numbers, like numeric labels
StartLine:
10 A=5
again:
20 Gosub mysubroutine
30 A--
40 If A>0 then goto again
50 End
mysubroutine: // only comments in same line with label
1000 Print "Code at line number 1000", A : return
}
Module UseOfLineLabels2 {
Print module.name$
Goto StartLine
Print "You can't see this..."
// we can use line numbers, like numeric labels
StartLine:
A=5
again:
Gosub mysubroutine
A--
If A>0 then goto again
End
mysubroutine: // only comments in same line with label
Print "Code at line number 1000", A : return
}
remove {
Print "I am an Alfa, and I just deleted"
}
}
// Beta is a Group Object
// All named members exist on interpreter lists.
// this object deleted at the end of scope
Beta=Alfa()
For Beta {
.UseOfLineNumbers
.UseOfLineLabels
// .UseOfLineLabels2
}
Beta.UseOfLineLabels2
// Delta is a pointer to an object of type of Group
// This object deleted when no pointer point to it.
Delta->Alfa()
For Delta {
// at this point the object attach as Group, like Beta, but using a hidden name
.UseOfLineNumbers
.UseOfLineLabels
}
// Now object deattach from interpreter lists (for modules/variables)
// No use of . but => for pointers
// yhis call do attach/call/deattacb in one go
Delta=>UseOfLineLabels2
Print Delta is type Alfa
// Pointer() return the Null object (is an object of type Null)
Delta=Pointer()
// The previous object deleted but call the remove module (if exist any)
Print Delta is type Null
// Now Delta point to Beta, which is a Group
Delta=Pointer(Beta)
Print Delta is type Alfa
// Interpreter just change Delta=> with Beta
// So now we don't have attach/call/deattach but only call
Delta=>UseOfLineNumbers
Delta=>UseOfLineLabels
Delta=>UseOfLineLabels2
// now we erase all variables
REM :
Clear
// Beta group can't call the remove module
// We can call it using Clear Beta (but has to be the last statement for Beta)
REM : Clear Beta // hide the Clear statement above, unhide this line (press enter after :)




M2000 - How to Call code (part 1)

In this part we will see how we can call code ffor exedcution rom code. This part has the calling for modules, functions and lambda functions, plus subs and simple functions.

Also we will see the use of Variant variable. The return type of a function depend of the type of result, which return through the statement = (the sign of equal as statement is the return from function, and isn't an exit from function, we can execute more commands after the return value passed to an internal buffer. Also we can pass more times a value, and the last one survives for return value)

Just copy these lines in a module A and then press esc and write A and press enter to get the results.


module TestModule (that, num1) {


// the names ThisIsAModule, ThisIsAFunction, ThisIsALambdaFunction, A
// aren't exist before interpreter found as interpret
module ThisIsAModule (that) {
Print "("+that+")"
}
function ThisIsAFunction(that) {
=that
}
ThisIsALambdaFunction = lambda (that) -> {
=that
}
Variant A
A=that
A+="..."
Print A=that+"..."
Print @ThisIsASimpleFunction()=that+"..."
Print ThisIsAFunction(A)=that+"..."
Print ThisIsALambdaFunction(A)=that+"..."
ThisIsASubroutine()
// calling a Module we don't use parenthesis for parameter list
// parameters can be put in stack before call also
ThisIsAModule A
Push A
ThisIsAModule
// we can change type from string to number (by default 100 is a double - 8byte)
A=num1
A++
Print A=num1+1
Print @ThisIsASimpleFunction()=num1+1
Print ThisIsAFunction(A)=num1+1
Print ThisIsALambdaFunction(A)=num1+1
ThisIsASubroutine()
ThisIsAModule A
Push A
ThisIsAModule
// sub or function without {} is an END
// so interpreter at the next statement exit from module
// subs and simple functions run on same namespace as the calling namespace
// so A is not local here
// we have to use LOCAL to define local variables.
sub ThisIsASubroutine()
Print "("+A+")"
end sub

function ThisIsASimpleFunction()
=A
end function
}
ClS 5, 0
PEN 15


TestModule "ok", 100


Module ByPassDeclaration (B) {
Print "This is a ByBass:";B
}
// 50% is a 16bit integer, 50& is 32bit integer
// 50# is Currency type, 50@ is Decimal type, 50! is single
// try those too


// this call has a decoration, which tell that the call has a predefined module
// ThisIsAModule using code from ByPassDeclaration.
// The inner code for ThisIsAModule skipped at execution
// We can do the normal call after this without decoration
TestModule "George", 50%; ThisIsAModule as ByPassDeclaration

// 100&& is long long literal (64bit signed integer)
TestModule "George", 100&&


Παρασκευή 6 Σεπτεμβρίου 2024

M2000 Check the call stack depth for modules, functions (and lambda functions), subs and simple functions

M2000 is an interpreter, so calling modules and functions needs more stack space (actually there M2000 Interpreter is a big function which call other functions including the big function).

We can say about "call stack depth", when each 1 more depth is one call in M2000 level. In M2000 way to deploy a call, the original return or process stack of processor used for specific internal calls and the parameters known from M2000 Interpreter at hidden level. The parameters at the high level are passed to stack of values which isn't in return/process stack.  So the number of parameters at call at M2000 Interpreter level are not interfere to number of calls. The stack of values, uses objects from a pool of objects, which can expand from a start number of 5000 objects. A call to a module pass the so called current stack of values. A call to function when this happen in an expression uses a new stack of value, with all parameters, with the first from the left on the first object (position 1) of stack of values, so the 3rd  parameter written to 3rd object on stack of values. When we pop a value to a variable (using Read statement) M2000 place the object to pool, with one cxception: If we copy objects on top of others, for same value the interpreter just copy the pointer to object, so when we pop a value, and there are more than one pointer attach to object, the object not passed to pool. The internal objects use a counter of pointers to it, and when the counter turn to zero the object deleted. For stack of values, the objects not used more placed to pool, so never goes under 1. Using the Clear statement at immediate mode, clear all variables and set the pool to 5000 objects. 

The program has five modules.

Output:

Recursion depth calling modules using CALL:7037
Recursion depth calling function using CALL:7037
Recursion depth calling function in expression:3314
Calling functions depth (A() calling B(), B() calling A()):2297
Calling modules depth (A calling B, B calling A):8329

Program. Instructions: Type Edit A, copy this program, then press Esc to exit from editor, type A and press Enter key to run it:

global Rep$
document Rep$ ' upgrade string to document object
module global A {
global z
module rec (x){
z<=x
call rec, x+1
}
try {
call rec 1
}
Rep$ <= "Recursion depth calling modules using CALL:" + z + {
}
}
module global B {
global z
function rec(x){
z<=x
call rec(x+1)
}
try {
call rec(1)
}
Rep$ <= "Recursion depth calling function using CALL:" + z + {
}
}
module global C {
global z
function rec(x){
z<=x:=rec(x+1)
}
try {
m=rec(1)
}
Rep$ <= "Recursion depth calling function in expression:" + z + {
}
}
module global D1 {
global z
function global a(x) {
z<=x
m=b(x+1)
}
function global b(x) {
z<=x
m=a(x+1)
}
try {
m=a(1)
}
Rep$ <= "Calling functions depth (A() calling B(), B() calling A()):" + z + {
}
}
module global D2 {
global z
module global a (x, y, k) {
z<=x
b x+1, 2, 3
}
module global b (x, y, k) {
z<=x
a x+1, 2, 3
}
try {
a 1,2, 3
}
Rep$ <= "Calling modules depth (A calling B, B calling A):" + z + {
}
}
a : b : c : d1 :d2
Report Rep$
Clipboard Rep$


Recursion for Subs


There is another call type, for SUBS. There is a Recursion.Limit statement to set the limit for Subs. If we call beyond the limit we get a fatal error (not stopped from Try statement), we return to immediate mode (if the program run from that mode, else the interpreter terminate execution). This limit can be used when we didn't use blocs of statements (using curly brackets) or statements which add another level like the  block  { }, like While End While. The if then/else.if then/end if muliline statement not used block, same the one line if then or if else (but not if then else), but the if then  else then  in one line, or multiline using { }, they use blocks.
By default recursion.limit set to 10000

In the example below we set linit it to 100000 calls.

Calling subs depth (a() calling b(), b(0) calling a()):100000
global r.limit=100000
set recursion.limit r.limit


global z=0
module testme {
a(1, 2, 3)

Sub a(x, y, k)
z<=x
if z<r.limit then b(x+1, 2, 3)
End Sub
Sub b(x, y, k)
z<=x
if z<r.limit then a(x+1, 2, 3)
End Sub
}
testme
Rep$ = "Calling subs depth (a() calling b(), b(0) calling a()):" + z + {
}
report Rep$
clipboard Rep$


Another variant (we can't go to 33000 calls)

The Gosub clause is optional for calling subs


Out of limit in b()
Calling subs depth (a() calling b(), b(0) calling a()):32700

global r.limit=32700, Rep$
Document Rep$
set recursion.limit r.limit


global z=0
module testme {
a(1, 2, 3)

Sub a(x, y, k)
z<=x
if z<r.limit then
gosub b(x+1, 2, 3)
else
Rep$ <=  "Out of limit in a()"+{
}
end if
End Sub
Sub b(x, y, k)
z<=x
if z<r.limit then
gosub a(x+1, 2, 3)
else
Rep$ <= "Out of limit in b()"+{
}
end if
End Sub
}
testme
Rep$ <= "Calling subs depth (a() calling b(), b(0) calling a()):" + z + {
}
report Rep$
clipboard Rep$


For simple functions:

We can't go to r.limit, because the expression calling depth stopped to 2328 calls. This we can handle with Try { } block. Check that calling simple functions we use @ at the calling. Simple functions can't be called using Call statement. Also the stack of value are not new for each call. Interpreter pass the same stack of value and put the parameters to pop first, starting at top with the left one on the call parameter list.

Calling subs depth (a() calling b(), b(0) calling a()):2328

global r.limit=32700, Rep$
Document Rep$
set recursion.limit r.limit


global z=0
module testme {
m=@a(1, 2, 3)

Function a(x, y, k)
z<=x
if z<r.limit then
local m=@b(x+1, 2, 3)
else
Rep$ <=  "Out of limit in a()"+{
}
end if
=100
End Function
Function b(x, y, k)
z<=x
if z<r.limit then
local m=@a(x+1, 2, 3)
else
Rep$ <= "Out of limit in b()"+{
}
end if
=200
End Function
}
try {
testme
}
Rep$ <= "Calling subs depth (a() calling b(), b(0) calling a()):" + z + {
}
report Rep$
clipboard Rep$




Lambda functions are like functions.
Add this module to first program and add the call to D3 after the call of D2
We get the same value for functions:

Calling lambda functions depth (A() calling B(), B() calling A()):2297


module global D3 {
global z
global a=lambda (x) ->{
z<=x
m=b(x+1)
}
global b=lambda (x) -> {
z<=x
m=a(x+1)
}
try {
m=a(1)
}
Rep$ <= "Calling lambda functions depth (A() calling B(), B() calling A()):" + z + {
}
}


Y combinator style call:

We can call a no names function (is a lambda function) using Y combinator.

fibonacci 10th = 55

clipboard "fibonacci 10th = "+(lambda (g, x)->{=g(g, x)}(lambda (g, x)->if(x<=1->x,g(g, x-1)+g(g, x-2)), 10))
wait 10 ' give some time for system
print clipboard$


factorial of 27 = 10888869450418352160768000000

clipboard "factorial of 27 = "+(lambda (g, x)->{=g(g, x)}(lambda (g, x)->if(x=0->1, x*g(g, x-1)), 27@))
print clipboard$

Epilogue

We see all the type of calls, except some extra non ordinary, which have to do with "call back" way of call. Parameters passed by default as copies (there are rules for objects, some of them are passed as is but when we pop them we get a copy, like arrays with ( ), bit not for arrays as pointers to array objects). We can pass by reference but we have to declare we need a reference at the called function/module/sub/simple function/lambda

This is an example for call back call, and for lazy evaluation. The Link statement normally can't link twice an identifier, except for functions (don't do that with functions as members of objects of type Group, at newer version interpreter will not allowed it).
 
m=0
counter=0
document resp$
// normaly m, counter and resp$ not seen from a normal function
// nut this used as a code of this module - see later
function dummy(value_for_counter) {
m+=20
counter+=value_for_counter
resp$=format$("{0::-10} +", value_for_counter)+{
}
}


module callback_example (&func()) {
for m=1 to 10
call func(random(1, 2))
next
}
// lazy$ make a reference to dummy() for later evaluation, like it is at this level
// so m exist. The code of dummy() called like it is at this level
// but if we make some new variables or modules or functions...
// these deleted at the return of the call
callback_example lazy$(&dummy())
resp$=format$("{0::-10} =", counter)+{
m=200 is }+(m=200)+{
done
}
// Report resp$ place the text with proportional rendering which we didn't want here
// Print #-2  render like we place text in a file, but we get it in screen.
// There is no page hold function like the one for the Report statement
Print #-2, resp$
link weak lazy$(m + 500) to k()
Print k()=700
link weak lazy$(m + 600) to k()
Print k()=800
module see_this (&f()) {
Print f()=800
}
// This is a laxy evaluation
see_this lazy$(m+600)