Προειδοποίηση για spoilers: Αυτό το άρθρο περιέχει spoilers για την άσκηση Grains γενικά, και ειδικότερα για την άσκηση Grains στη διαδρομή Bash. Αν δεν την έχεις ολοκληρώσει ακόμα μόνος σου και δεν θέλεις να σου δείξουν κάποιες λύσεις, γύρνα πίσω όταν την τελειώσεις!
Είναι η πρώτη σου μέρα σε μια καινούρια εταιρεία. Έχεις κάνει όλα τα χαρτιά, έχεις γνωρίσει την ομάδα, και επιτέλους ήρθε η ώρα να κάτσεις και να αρχίσεις να διαβάζεις λίγο από τον κώδικα πάνω στον οποίο θα δουλέψεις. Αρχίζεις να διαβάζεις τις διάφορες συναρτήσεις, τις κλάσεις και τα αρθρώματα, και, καθώς διαβάζεις, πιάνεις τον εαυτό σου να στραβώνει μπροστά στην οθόνη από σύγχυση. Συνεχίζεις να διαβάζεις, και μια λέξη ξεφεύγει από το στόμα σου, μόλις που ειπωμένη, σχεδόν ψιθυριστή: "Τιιιιιιι..."1 Όσο προχωράς, τόσο πιο συχνά συμβαίνει αυτό, καθώς μπερδεύεσαι όλο και περισσότερο και θυμώνεις λιγάκι.
Τι συμβαίνει σε αυτόν τον κώδικα;
Όποτε παραπάνω από ένα άτομο δουλεύει πάνω σε ένα κομμάτι κώδικα, η προσοχή και η επιμέλεια που απαιτούνται για να κρατήσεις τα πράγματα διαχειρίσιμα αυξάνονται κατά πολύ. Δεν έχεις πια την έννοια που ζει στο μυαλό σου και τον κώδικα που απλώς πρέπει να την κάνει πραγματικότητα. Τώρα, η έννοια πρέπει να ζει μέσα στον κώδικα, εκεί όπου όλοι οι συνεργάτες μπορούν να τη δουν και να την αλλάξουν αν χρειαστεί.
Το πώς υλοποιείς κάτι δεν σημαίνει και πολλά για τον τελικό χρήστη, αλλά θα έπρεπε να λέει πολλά σε κάθε μηχανικό που αγγίζει τον σχεδιασμό σου σε οποιοδήποτε σημείο. Συχνά υπάρχουν πολλοί τρόποι να πετύχεις την ίδια λειτουργικότητα, και μπορεί να φαίνεται πως οποιαδήποτε από τις επιλογές θα αρκούσε για να γίνει η δουλειά. Ωστόσο, πιστεύω πως κάθε απόφαση που παίρνεις θα πρέπει να έχει έναν λόγο (ακόμα κι αν είναι μια μικρή απόφαση με έναν μικρό λόγο), και αυτός ο λόγος θα πρέπει να επικοινωνεί έναν στόχο ή μια απαίτηση.
Η ιδέα ότι οι λεπτομέρειες της υλοποίησης θα πρέπει να βοηθούν όποιον διαβάζει τον κώδικα να διακρίνει τη σκέψη, τους στόχους και τις προτεραιότητες πίσω από αυτόν ονομάζεται σχεδιαστική πρόθεση. Το πώς ονομάζεις τις μεταβλητές σου, ποιες παραμέτρους δέχεται η συνάρτησή σου και πώς αφαιρούνται οι λεπτομέρειες είναι όλα σημεία όπου μπορεί να εκφραστεί η σχεδιαστική πρόθεση, είτε καλά είτε άσχημα.
Πιστεύω ακράδαντα ότι η σχεδιαστική πρόθεση είναι ένα από τα πιο σημαντικά πράγματα που πρέπει να λάβεις υπόψη όταν υλοποιείς έναν σχεδιασμό μηχανικού. Είναι ένα από τα πράγματα που ξεχωρίζουν τη μηχανική λογισμικού από τον προγραμματισμό.
Η μηχανική λογισμικού είναι αυτό που συμβαίνει στον προγραμματισμό όταν προσθέτεις χρόνο και άλλους προγραμματιστές.
Από τον Russ Cox
Η σχεδιαστική πρόθεση είναι διεπιστημονική
Δουλεύω ως μηχανολόγος μηχανικός, σχεδιάζοντας καλούπια έγχυσης, κυρίως για ιατρικές συσκευές. Όλα τα σχέδιά μου, μόλις τελειώσουν, βγαίνουν κατευθείαν από την πόρτα και πάνε στο μηχανουργείο, όπου αρχίζουν να φτιάχνουν όλα τα κομμάτια και να τα συναρμολογούν. Επειδή δεν ξέρουν όλα όσα πέρασαν από το μυαλό μου όσο δημιουργούσα κάθε σχέδιο, πρέπει να βρω έναν τρόπο να δείξω την πρόθεσή μου μέσα από το ίδιο το σχέδιο.
Πολλές φορές, κάποια χαρακτηριστικά είναι ιδιαίτερα κρίσιμα. Είτε ο πελάτης έχει πει ότι χρειάζεται εκεί ειδικά στενές ανοχές, είτε ο τρόπος που δένει το καλούπι απαιτεί ακραία ακρίβεια για κάποιον λόγο. Έτσι, για να βοηθήσω τους μηχανουργούς να φτιάξουν τα κομμάτια με τρόπο που να δίνει προτεραιότητα στην ακρίβεια στα σημαντικά σημεία, αφήνω σημεία που είναι σκόπιμα τετράγωνα ή που μπαίνουν εύκολα στη μέγγενη με έναν συγκεκριμένο τρόπο. Έτσι, ο πιο εύκολος δρόμος για αυτούς δίνει τα καλύτερα αποτελέσματα για μένα.
Υπάρχουν επίσης σημεία όπου οι διαστάσεις δεν είναι τόσο κρίσιμες. Για παράδειγμα, αν βάλω στο σχέδιο μια τρύπα που προορίζεται απλώς για εξαερισμό, θα την κάνω σε ένα βολικό, συνηθισμένο μέγεθος, όπως 6mm.
Όταν κατεργάζονται αυτή την τρύπα και πάνε να μετρήσουν πώς βγήκε, αν δουν έναν αριθμό σαν 5.99mm, θα σκεφτούν: "Εντάξει, μάλλον 6mm έπρεπε να είναι, άρα είμαι αρκετά κοντά", και δεν θα χρειαστεί καν να ξανακοιτάξουν τις διαστάσεις στο CAD ή στο σχέδιο προδιαγραφών. Αν αντίθετα την είχα κάνει κάτι ασυνήθιστο, όπως 5.87mm, θα την κοιτούσαν και θα είχαν αυτή την πρώτη αντίδραση:
- Ωχ, μήπως το έφτιαξα πολύ πιο μικρό απ' ό,τι έπρεπε; Μήπως έπρεπε να είναι 6mm;
- (Πάνε να δουν το CAD και βλέπουν ότι η τρύπα τους είναι καλή και ότι απλώς είναι ένα ασυνήθιστο μέγεθος.)
- Χμμ. Σίγουρα αυτή η τρύπα είναι ασυνήθιστου μεγέθους για κάποιον λόγο. Ίσως είναι πολύ σημαντική, ή ο πελάτης ζήτησε μια ειδική τρύπα εδώ. Θα πρέπει να πάω να μιλήσω με τον Ryan και να δω τι είναι τόσο σημαντικό σε αυτή την τρύπα.
- (ΜΠΑΜ! Αφήνουν το κομμάτι του αλουμινίου στο γραφείο μου με λεπτεπίλεπτη χάρη.)
- (Ανακαλύπτουν ότι δεν υπάρχει τίποτα σημαντικό σε αυτή την τρύπα, απλώς διάλεξα ένα περίεργο μέγεθος, και όλη αυτή η επιπλέον δουλειά και ανησυχία ήταν χωρίς λόγο.)
- Μα τι άνθρωπος κι αυτός ο Ryan. (μουρμούρα, βρισιά, μουρμούρα)
Όλα αυτά συμβαίνουν επειδή κάθε απόφαση του σχεδίου μου επικοινωνεί κάτι στους άλλους ανθρώπους που το κοιτάζουν και δουλεύουν πάνω του, είτε το θέλω είτε όχι. Πρέπει να δουν νόημα μέσα του, γιατί είναι η μόνη πληροφορία που έχουν για να προχωρήσουν! Άρα είναι πολύ καλύτερα αν μπορώ να αφιερώσω χρόνο ώστε να βάλω ουσιαστική, σκόπιμη πληροφορία στο σχέδιό μου.
Κόκκοι: μια εισαγωγή
Τώρα, ας μιλήσουμε για το πώς μπορεί να επικοινωνηθεί η σχεδιαστική πρόθεση στον κώδικα, χρησιμοποιώντας ένα παράδειγμα από μία από τις ασκήσεις του Exercism. Πρόσφατα δούλεψα με έναν μαθητή πάνω στη λύση του για την άσκηση Κόκκοι στη διαδρομή Bash. Οι Κόκκοι είναι μια άσκηση που ασχολείται με το πρόβλημα του σιταριού και της σκακιέρας. Με λίγα λόγια, ένας κόκκος σιταριού τοποθετείται στο πρώτο τετράγωνο μιας σκακιέρας. Δύο κόκκοι πάνε στο επόμενο τετράγωνο. Τέσσερις κόκκοι στο επόμενο. Και ούτω καθεξής, με κάθε τετράγωνο να έχει διπλάσιους κόκκους από το προηγούμενο. Οι μαθητές καλούνται να βρουν έναν τρόπο να υπολογίζουν την τιμή κάθε μεμονωμένου τετραγώνου, καθώς και τον συνολικό αριθμό κόκκων στη σκακιέρα.
Αυτός ο συγκεκριμένος μαθητής σκέφτηκε έναν αρκετά έξυπνο τρόπο να υπολογίσει το σύνολο.
bc <<< 'ibase=16;FFFFFFFFFFFFFFFF'
Το bc είναι ένας υπολογιστής γραμμής εντολών. Μπορείς να του περάσεις συμβολοσειρές με αριθμητικές πράξεις, και θα τις υπολογίσει, ακόμα και για πολύ μεγάλους ακέραιους και αριθμούς κινητής υποδιαστολής. Υπάρχουν κι άλλοι τρόποι να κάνεις υπολογισμούς χωρίς το bc στη Bash, αλλά, για λόγους απλότητας, θα δούμε πώς μπορεί να επικοινωνηθεί η πρόθεση, ή να μην επικοινωνηθεί, όταν χρησιμοποιείς το bc.
Αυτή η λύση δουλεύει επειδή όλη η άσκηση περιστρέφεται γύρω από δυνάμεις του δύο, και όπου υπάρχουν δυνάμεις του δύο υπάρχει δυαδικό σύστημα, και όπου υπάρχει δυαδικό σύστημα υπάρχει δεκαεξαδικό2!
Είναι μια έξυπνη λύση, αλλά τι μας λέει ο κώδικας; Ότι το δεκαεξαδικό είναι σημαντικό εδώ; Ότι το πρόβλημα περιστρέφεται θεμελιωδώς γύρω από το 16; Αφού ξαναδιάβασα την εκφώνηση, είναι αρκετά σαφές ότι κανένα από τα δύο δεν ισχύει. Ο μαθητής κι εγώ κάναμε καταιγισμό ιδεών για το πώς θα μπορούσαμε να επικοινωνήσουμε την πρόθεση πιο καθαρά. Να μερικά πράγματα που σκεφτήκαμε:
Πρώτη επιλογή: δυαδικό
Επειδή έχουμε ένα σωρό πράγματα που διπλασιάζονται (και άρα ένα σωρό δυνάμεις του 2), ας δούμε τι συμβαίνει στο δυαδικό σύστημα, μήπως και μας βοηθήσει.
Το πρώτο τετράγωνο έχει 1 κόκκο. Στο δυαδικό, αυτό θα ήταν το 0b1 (όπου το 0b σημαίνει απλώς "αυτός είναι ένας δυαδικός αριθμός", με τον ίδιο τον αριθμό να είναι το 1).
Το δεύτερο τετράγωνο έχει 2 κόκκους. Στο δυαδικό, 0b10. Το σύνολο μέχρι στιγμής είναι 3 (ή 0b11).
Το τρίτο τετράγωνο έχει 4 κόκκους (0b100). Σύνολο μέχρι στιγμής: 7 (0b111).
Το τέταρτο τετράγωνο έχει 8 κόκκους (0b1000). Σύνολο μέχρι στιγμής: 15 (0b1111).
Μπορείς να δεις το μοτίβο;
Κάθε τετράγωνο αντιπροσωπεύει ένα ακόμη δυαδικό ψηφίο, και το να τα προσθέσεις όλα μαζί απλώς φτιάχνει ένα σωρό άσσους.
Στη λύση του μαθητή, θα μπορούσαμε να αντικαταστήσουμε τα F με 64 άσσους (έναν για κάθε τετράγωνο)!
bc <<< "ibase=2;1111111111111111111111111111111111111111111111111111111111111111"
Πιο σκόπιμο, επειδή ταιριάζει πιο στενά με αυτό που μας δίνει το πρόβλημα. Αλλά δεν μιλάμε ρομποτικά. Μια μακριά, ουσιαστικά αμέτρητη σειρά από άσσους ίσως να μην είναι και βελτίωση.
Δεύτερη επιλογή: ο υπολογισμός με ωμή βία
Εντάξει, λοιπόν, ίσως να εγκαταλείψουμε εντελώς τα μη δεκαδικά συστήματα αρίθμησης. Γιατί δεν κάνουμε τον κώδικα να ταιριάζει με τον τρόπο που θα αθροίζαμε με το χέρι τους κόκκους σε μια σκακιέρα, μετρώντας τους κόκκους σε κάθε τετράγωνο;
total=0
current_grains=1
for square in {1..64}; do
total=$( bc <<< "$total + $current_grains" )
current_grains=$( bc <<< "$current_grains * 2" )
done
echo "$total"
Αυτό είναι πολύ πιο ευανάγνωστο και κατανοητό. Ο κώδικας δείχνει καθαρά ότι ο αριθμός των τετραγώνων στη σκακιέρα είναι καθοριστικός παράγοντας, όπως και το φαινόμενο του διπλασιασμού σε κάθε τετράγωνο. Νομίζω πως αυτό είναι καλύτερο από την αρχική λύση.
Ωστόσο.
Είναι αργό. Ο βρόχος, η πρόσθεση και οι επανειλημμένες κλήσεις σε μια εξωτερική εντολή; Όλα μαζί συσσωρεύονται σε έναν κάπως αργό χρόνο εκτέλεσης. Τώρα, είναι τόσο μεγάλο θέμα; Όχι. Αν το γράφεις ως script σε Bash, μάλλον έχεις ήδη αποφασίσει ότι δεν έχεις περιορισμούς ταχύτητας. Αλλά θα μπορούσε να είναι καλύτερο; Ναι.
Τρίτη επιλογή: άμεσος υπολογισμός
Λοιπόν, πώς τα αθροίζουμε όλα αυτά χωρίς επανάληψη;
Ας εξετάσουμε μια μικρότερη εκδοχή του ίδιου προβλήματος: μια σκακιέρα με 5 τετράγωνα3.
Τα πέντε τετράγωνα θα είχαν τον εξής αριθμό κόκκων:
---------------------
| 1 | 2 | 4 | 8 |16 |
---------------------
Και το σύνολο εδώ θα ήταν: 1 + 2 + 4 + 8 + 16 = 31. Χμ. Το 31 δεν μου φωνάζει ακόμα κάτι προφανές. Ας πάμε λίγο πιο μεγάλα.
Εντάξει, τότε τι γίνεται με μια σκακιέρα 6 τετραγώνων; Αυτή τη φορά, θα δείξω το τρέχον σύνολο κάτω από κάθε τετράγωνο για να μας βοηθήσει να το αθροίσουμε.
-------------------------
| 1 | 2 | 4 | 8 |16 |32 |
| | 3 | 7 |15 |31 |63 |
-------------------------
Και το άθροισμα: 1 + 2 + 4 + 8 + 16 + 32 = 63. Χμμ... Στην πραγματικότητα αρχίζω να βλέπω μια υποψία μοτίβου, αλλά ας κάνουμε ένα ακόμη για σιγουριά.
7 τετράγωνα:
-----------------------------
| 1 | 2 | 4 | 8 |16 |32 |64 |
| | 3 | 7 |15 |31 |63 |127|
-----------------------------
1 + 2 + 4 + 8 + 16 + 32 +64 = 127. Το βλέπεις; Σου χτυπάει κάτι με τις τιμές 31, 63, 127;
Είναι σχεδόν δυνάμεις του 2. Στην πραγματικότητα, είναι κατά ένα λιγότερες από την επόμενη δύναμη του δύο.
Ένα ακόμη παράδειγμα, για να το εμπεδώσουμε. Φαντάσου μια σκακιέρα με 12 τετράγωνα. Αυτό είναι το ένα, διπλασιασμένο 11 φορές (που, στον χώρο των μαθηματικών, είναι 2^11): 2048. Διπλασίασέ το άλλη μία φορά και παίρνεις 4096 (2^12). Οπότε... αν βρήκαμε σωστά το μοτίβο, το τρέχον σύνολο θα ήταν ένα λιγότερο από το 4096, γνωστό και ως 4095. Και αν το αθροίσουμε, αυτό ακριβώς παίρνουμε: 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256 + 512 + 1024 + 2048 = 4095.
Για να το πούμε αλλιώς, για να βρεις το σύνολο για όλα τα
nτετράγωνα, πρέπει να ανέβεις μία δύναμη του δύο και να αφαιρέσεις 1 από το αποτέλεσμα.
Ο αριθμός των κόκκων στο τετράγωνο 64 είναι 2^63 (μηδενική αρίθμηση, θυμήσου). Λοιπόοοον, αν θέλουμε να υπολογίσουμε το σύνολο των κόκκων σε όλα τα τετράγωνα μέχρι και το τετράγωνο 64, πρέπει να υπολογίσουμε το 2^64 και να αφαιρέσουμε 1.
Μπουμ.
Στη Bash, θα μοιάζει κάπως έτσι:
bc <<< "2^64 - 1"
Αυτό βγάζει νόημα όταν επιβεβαιώσεις τι συμβαίνει με το δυαδικό. Στο δυαδικό, ποιο ήταν το σύνολο και των 64 τετραγώνων;
0b1111... # 64 ones
Ποιος είναι ο αριθμός των κόκκων στο θεωρητικό 65ο τετράγωνο;
0b10000... # 1 and 64 zeros
Πώς πηγαίνεις από το 1 και 64 μηδενικά σε 64 άσσους; Αφαιρείς 1.
Και τι επιπλέον όφελος μας δίνει αυτό; Λοιπόν, τώρα έχουμε μια ωραία, ευανάγνωστη έκφραση για το σύνολο. Δεν κάνει επανάληψη, άρα η απόδοση είναι καλή. Και περιέχει τον αριθμό 64, που είναι ο αριθμός των τετραγώνων σε μια σκακιέρα, κάτι που είναι καλό παράδειγμα καλά δηλωμένης σχεδιαστικής πρόθεσης. Αν για κάποιον λόγο, σε 1000 χρόνια, ο κόσμος καθιερώσει μια σκακιέρα 7x7, αυτός ο μελλοντικός μηχανικός (χρησιμοποιώντας μάλλον Bash 6.1) θα κοιτάξει το script, θα δει τι προσπαθούσες να κάνεις, και θα αλλάξει το 64 σε 49. Μια χαρά!
Μείνε σκόπιμος, φίλε μου
Όταν επεξεργάζεσαι μια υλοποίηση, είναι εύκολο να πετάς πράγματα δεξιά κι αριστερά και να πιαστείς από την πρώτη λύση που δουλεύει. Αυτό είναι εντάξει όσο εξερευνάς το πρόβλημα, αλλά μόλις καταλάβεις πλήρως τα κρίσιμα components, αν έχεις τον χρόνο να αφιερώσεις για ένα καλό γυάλισμα, φρόντισε κάθε αλγόριθμος, κάθε όνομα μεταβλητής, ακόμα και τα κενά διαστήματά σου, να ζωγραφίζουν μια εικόνα του προβλήματος, των κρίσιμων απαιτήσεων και του πώς όλα τα κομμάτια δένουν μεταξύ τους.
-
Δες και το κόμικ του Thom Holwerda. ↩
-
Αν νιώθεις λίγο σκουριασμένος στη δυαδική και δεκαεξαδική αρίθμηση, ο @kytrinyx προτείνει το βιβλίο How to Count. Και για να κάνω μια αδιάντροπη διαφήμιση, έγραψα πρόσφατα και δυο άρθρα στο blog για το δυαδικό και το δεκαεξαδικό. ↩
-
Δεν ξέρω πώς θα δούλευε αυτό. Ίσως θα μπορούσαμε απλώς να βάλουμε τα πιόνια να κονταροχτυπηθούν μεταξύ τους. ↩