Καλώς ήρθες στον δεύτερο θεματικό μας μήνα: τον Mechanical March. Αυτόν τον μήνα εστιάζουμε στις γλώσσες συστημάτων, σε εκείνες που μεταγλωττίζονται σε κώδικα μηχανής.
Αυτό είναι εν μέρει άρθρο και εν μέρει απομαγνητοφώνηση του βίντεο του Mechanical March. Θα σου δώσω μια σύντομη εισαγωγή στον μήνα και μετά θα ρίξουμε μια ματιά στις γλώσσες συστημάτων, στην εξέλιξή τους μέσα στην ιστορία, στα υπέρ και τα κατά της μεταγλώττισης σε κώδικα μηχανής, και σε μια γρήγορη ματιά σε καθεμία από τις επιλεγμένες γλώσσες. Μαζί μου είναι και πάλι ο Erik, ο οποίος θα μιλήσει περισσότερο στο δεύτερο μισό. Εγώ όμως θα ξεκινήσω με μερικές πρακτικές πληροφορίες.
Λοιπόν, πρώτα οι επιλεγμένες γλώσσες αυτού του μήνα. Είναι οι C, C++, D, Go, Nim, Rust, V και Zig. Για να πάρεις το badge του Mechanical March, πρέπει να ολοκληρώσεις πέντε ασκήσεις σε μία από αυτές τις γλώσσες. Το track μας για τη Go έχει ένα από τα καλύτερα syllabus στο Exercism, οπότε σου προτείνω ανεπιφύλακτα να το δοκιμάσεις. Εδώ στο Exercism είμαστε επίσης μεγάλοι φαν της Nim, καθώς είναι μια σχετικά απλή γλώσσα για να ξεκινήσεις και πολύ εύκολη στο γράψιμο, οπότε θα σου πρότεινα σίγουρα να τη δοκιμάσεις κι αυτή.
Έχουμε επίσης πέντε επιλεγμένες ασκήσεις για να δοκιμάσεις:
- Linked-list ή simple-linked-list (ανάλογα με τη γλώσσα): δέσμευση/απελευθέρωση μνήμης, δείκτες
- secret-handshake: πράξεις bitwise
- pangram:
forβρόχοι, συμβολοσειρές και χαρακτήρες - sieve: πίνακες,
forβρόχοι - binary-search: πίνακες, βρόχοι
Υπάρχει ένα νέο badge που ανακοίνωσα στο βίντεο με τις ενημερώσεις πριν από λίγες μέρες, και είναι για την ολοκλήρωση των πέντε επιλεγμένων ασκήσεων στις θεματικές γλώσσες. Οπότε για να πάρεις αυτό το badge θα χρειαστεί να ολοκληρώσεις όλες αυτές τις ασκήσεις σε μια γλώσσα συστημάτων κάποια στιγμή μέσα στη χρονιά.
Έχουμε επίσης πολλά διασκεδαστικά πράγματα στα σκαριά: συνεντεύξεις με δύο άτομα από την ομάδα Go Core, και ελπίζουμε και από τη Rust και μερικές από τις άλλες γλώσσες. Θα κάνουμε επίσης πολλά live streaming μέσα στον μήνα. Και σύντομα θα φτάσει και λίγο swag του Mechanical March!
Λοιπόν, ας εμβαθύνουμε λίγο περισσότερο στην τεχνική πλευρά.
Σε τι χρησιμοποιούνται αυτές οι γλώσσες;
Τις χρησιμοποιούμε σε όλο το Exercism. Το CLI μας είναι γραμμένο σε Go, το εσωτερικό μας εργαλείο διαχείρισης track που λέγεται configlet είναι γραμμένο σε Nim, και η βασική βιβλιοθήκη που μετράει τις γραμμές κώδικα στις λύσεις σου είναι γραμμένη σε Rust. Erik, γιατί επιλέξαμε αυτές τις γλώσσες για αυτά τα εργαλεία;
CLI:
- Νομίζω ότι χρησιμοποιήσαμε τη Go επειδή αυτή ήταν η γλώσσα με την οποία η Katrina ήταν πιο εξοικειωμένη.
- Η Go είναι εξαιρετική για τέτοια σχετικά μικρά εργαλεία γραμμής εντολών.
- Ο κώδικας της Go είναι σχετικά απλός, κάτι που κάνει τις συνεισφορές ευκολότερες
- Τα εκτελέσιμα της Go αναπτύσσονται εύκολα, καθώς δεν απαιτούν runtime
- Η Go τα πάει καλά με τη διασταυρούμενη μεταγλώττιση
Nim
- Η Nim έχει τα περισσότερα από τα ίδια πλεονεκτήματα με τη Go
- Δεν είχαμε πολλά άτομα που να ξέρουν Go και να μπορούν να συντηρήσουν το CLI, οπότε περάσαμε στη Nim
Σε ποια άλλα μέρη θα περίμενες να βρεις αυτές τις γλώσσες;
- Παντού όπου η απόδοση είναι σημαντική (οδηγοί συσκευών, παιχνίδια, λειτουργικά συστήματα, συστήματα build/μεταγλωττιστές)
- Παντού όπου οι πόροι είναι περιορισμένοι (π.χ. ενσωματωμένο λογισμικό)
- Κάθε τι που πρέπει να είναι εξαιρετικά φορητό, δηλαδή να τρέχει σε πολλές διαφορετικές πλατφόρμες
Τι είναι ο κώδικας μηχανής;
Όπως είπα νωρίτερα, αυτές οι γλώσσες του Mechanical March ξεχωρίζουν επειδή μεταγλωττίζονται σε κώδικα μηχανής. Μπορείς να εξηγήσεις λίγο τι είναι ο κώδικας μηχανής και, αντίθετα, τι είναι το bytecode;
- Ο κώδικας μηχανής είναι κώδικας που μπορεί να εκτελεστεί απευθείας στη μηχανή.
- Το bytecode, αντίθετα, απαιτεί κάποιον άλλον κώδικα για να το διερμηνεύσει ή να το μεταγλωττίσει σε κώδικα μηχανής. Επομένως, το bytecode απαιτεί ένα ενδιάμεσο βήμα πριν μπορέσει να εκτελεστεί.
Ποια είναι τα πλεονεκτήματα και τα μειονεκτήματα του κώδικα μηχανής έναντι του bytecode;
Πλεονεκτήματα:
- Γρηγορότερη εκκίνηση (χωρίς βήμα μεταγλώττισης JIT)
- Μικρότερο αποτύπωμα μνήμης (δε φορτώνεται runtime, δεν υπάρχει bytecode στη μνήμη, ιδανικό για ενσωματωμένα συστήματα)
- Η μηχανή-στόχος δε χρειάζεται να έχει εγκατεστημένο runtime (σημαντικό για να μένουν μικρά τα docker containers)
Μειονεκτήματα:
- Δεν είναι φορητός. Το bytecode είναι φορητό, αλλά ο μεταγλωττισμένος κώδικας μηχανής είναι ειδικός για κάθε πλατφόρμα
- Δεν επιτρέπει προηγμένες βελτιστοποιήσεις όπως η βελτιστοποίηση με βάση το προφίλ (καθορισμός του καλύτερου τρόπου μεταγλώττισης του (byte)code αφού εκτελεστεί για λίγο καιρό)
Σημείωση: είναι δυνατές και υβριδικές προσεγγίσεις, όπου η γλώσσα μεταγλωττίζεται σε bytecode και μετά χρησιμοποιεί ένα διαφορετικό εργαλείο για να μεταγλωττίσει αυτό το bytecode σε κώδικα μηχανής.
Η εξέλιξη του προγραμματισμού συστημάτων
Εντάξει. Ας δούμε λίγο την εξέλιξη μερικών από τις γλώσσες αυτού του μήνα. Πρώτα, ας ξεκινήσουμε από τα παλιά, από τη C, και από το πώς εξελίχθηκε η C++ από εκεί. Πες μας λίγα λόγια για αυτές τις γλώσσες.
Η C είναι μια πολύ χαμηλού επιπέδου γλώσσα. Νιώθεις σαν να βρίσκεσαι μόλις λίγο πάνω από τον κώδικα μηχανής. Αυτό την κάνει πολύ ισχυρή και εξαιρετικά βελτιστοποιήσιμη, αλλά και κάπως επιρρεπή σε bugs (π.χ. εξαιρέσεις μηδενικού δείκτη και υπερχειλίσεις buffer). Η διαχείριση μνήμης είναι εξ ολοκλήρου χειροκίνητη και άρα ευθύνη του προγραμματιστή, κάτι που μπορεί να οδηγήσει σε bugs ή/και διαρροές μνήμης. Η C++ είναι σαν τη C αλλά με υποστήριξη αντικειμενοστρεφούς προγραμματισμού. Παραμένει αρκετά χαμηλού επιπέδου και απαιτεί να κάνεις χειροκίνητη διαχείριση μνήμης. Τόσο η C όσο και η C++ σου επιτρέπουν να γράφεις ενσωματωμένο assembly (ASM)!
Και τι γίνεται με τις νεότερες γλώσσες συστημάτων; Πώς έχουν εξελιχθεί;
Όλες οι σύγχρονες γλώσσες προγραμματισμού συστημάτων υποστηρίζουν αυτόματη διαχείριση μνήμης, είτε μέσω μέτρησης αναφορών, είτε μέσω garbage collector, είτε μέσω κάποιου άλλου μηχανισμού.
Οι πρώιμες γλώσσες προγραμματισμού συστημάτων υποστηρίζουν όλες null pointers (αυτό που ο Tony Hoare ονόμασε "το λάθος του ενός δισεκατομμυρίου δολαρίων"). Αυτοί είναι διαβόητοι γιατί οδηγούν σε σφάλματα κατά τον χρόνο εκτέλεσης και σε ευπάθειες. Πολλές σύγχρονες γλώσσες καταργούν το null ή τουλάχιστον απαιτούν προσπάθεια για να το χρησιμοποιήσεις.
Μια άλλη αλλαγή είναι η μετάβαση από τις μεταβλητές τιμές ως προεπιλογή στις αμετάβλητες τιμές εξ ορισμού. Π.χ. τόσο η Rust όσο και η Vlang έχουν αμετάβλητες τιμές εξ ορισμού, και χρειάζεται να δηλώσεις ρητά ότι θέλεις μεταβλητότητα.
Όλες οι νεότερες γλώσσες υποστηρίζουν διαλειτουργικότητα με τη C (ή τη C++), καθώς έχει γραφτεί πάρα πολύς κώδικας σε αυτές τις γλώσσες.
Ένα άλλο ενδιαφέρον σημείο είναι ότι μερικές από τις νεότερες γλώσσες δεν μεταγλωττίζονται απευθείας σε κώδικα μηχανής, αλλά χρησιμοποιούν άλλα εργαλεία για να το κάνουν. Π.χ. η Rust και η Zig χρησιμοποιούν το LLVM, ενώ η Nim επιτρέπει να χρησιμοποιήσεις μια μεγάλη ποικιλία μεταγλωττιστών. Αυτό είναι γνωστό ως transpiling.
Και τι γίνεται με πράγματα όπως τα macros και ο μεταπρογραμματισμός;
Υπάρχει μια ενδιαφέρουσα διάσπαση όσον αφορά τα macros και τον μεταπρογραμματισμό. Τα macros στη C/C++ είναι ισχυρά, αλλά έχουν κάπως αμφιλεγόμενη φήμη ως δύσκολα στη χρήση. Η Rust, η Nim και η D προσφέρουν όλες ισχυρό μεταπρογραμματισμό, αλλά με πολύ πιο όμορφο τρόπο. Αντίθετα, η VLang και η Zig αναφέρουν ρητά την απουσία macros ως χαρακτηριστικό της γλώσσας τους, και η Go έχει διαφορετική προσέγγιση με το go generate.
Οι γλώσσες συστημάτων έχουν τη φήμη ότι είναι αρκετά χαμηλού επιπέδου. Ισχύει ακόμα αυτό;
Οι νεότερες γλώσσες λειτουργούν όλες σε υψηλότερα επίπεδα αφαίρεσης από τη C/C++. Π.χ. η Rust, η D και η Nim επιτρέπουν επίσης έναν πολύ συναρτησιακό τρόπο γραφής κώδικα. Η Nim και η D έχουν μάλιστα την έννοια των "καθαρών" συναρτήσεων.
Εισαγωγή στις γλώσσες του μήνα
Θα ήταν λοιπόν καλό να δούμε κάθε γλώσσα με τη σειρά. Όλες οι γλώσσες έχουν ομοιότητες: είναι όλες ισχυρά και στατικά τυποποιημένες. Ας δούμε όμως πώς διαφέρουν. Να ξεκινήσουμε με τη C;
C
- Αναπτύχθηκε από τον Dennis Ritchie
- Μία από τις παλαιότερες και πιθανώς την πιο πολυχρησιμοποιημένη γλώσσα στον κόσμο
- Πάρα πολλά λογισμικά είναι γραμμένα σε C, όπως το Unix και το Linux
- Εξαιρετικά επιδραστική (σκέψου: υπάρχουν ολόκληρες κατηγορίες γλωσσών τύπου C)
- Χειροκίνητη διαχείριση μνήμης
- Εξαιρετικά αποδοτική (κοντά στο "μέταλλο")
- Τρέχει παντού
- Ιδανική για ενσωματωμένα συστήματα
- Σχετικά μικρή γλώσσα
C++
- Αναπτύχθηκε από τον Bjarne Stroustrup
- Διάδοχος της C, αλλά με προσθήκη αντικειμενοστρέφειας (η C με κλάσεις)
- Βοήθησε να γίνει δημοφιλής ο αντικειμενοστρεφής προγραμματισμός
- Περισσότερα χαρακτηριστικά υψηλού επιπέδου από τη C
- Υποστηρίζει γενικό προγραμματισμό μέσω templates
- Προσθέτει υποστήριξη ενοτήτων μέσω namespaces
- Πολλά παιχνίδια (και μηχανές παιχνιδιών) είναι γραμμένα σε C++, όπως και μεγάλα τμήματα των Windows
- Χειροκίνητη διαχείριση μνήμης
- Εξακολουθεί να εξελίσσεται, με νέα χαρακτηριστικά να προστίθενται τακτικά (μεγάλο spec)
D
- Αναπτύχθηκε από τον Walter Bright, και αργότερα προστέθηκε ο Andrei Alexandrescu
- Αρχικά θεωρήθηκε μια επανασχεδιασμένη C++ (μαθαίνοντας από τα "λάθη" της), αντλεί όμως έμπνευση από πολλές άλλες γλώσσες
- Πολυπαραδειγματική, υποστηρίζει προστακτικό/αντικειμενοστρεφή/συναρτησιακό προγραμματισμό
- Εύκολη διαλειτουργικότητα με C/C++
- Ενιαία σύνταξη κλήσης συνάρτησης
- Αξιολόγηση συνάρτησης σε χρόνο μεταγλώττισης (π.χ. δημιουργία μηχανής καταστάσεων Regex σε χρόνο μεταγλώττισης)
- Υποστηρίζει συναρτησιακό προγραμματισμό και "καθαρές" συναρτήσεις
- Πολλά χαρακτηριστικά ασφάλειας
- Ασφάλεια μνήμης μέσω @safe
- Συμβόλαια (προ/μετα-συνθήκες, αναλλοίωτες συνθήκες)
- Καθαρές συναρτήσεις
- Έμφαση στα unit tests, με τα tests δίπλα στον πηγαίο κώδικα που ελέγχουν (αυτό απαίτησε μια εξαίρεση στον ιστότοπο του Exercism :))
- Unicode
Rust
- Αναπτύχθηκε από τον υπάλληλο της Mozilla Research Graydon Hoare, υιοθετήθηκε επίσημα από τη Mozilla και τώρα αποτελεί μέρος του Rust Foundation
- Πολυπαραδειγματική, υποστηρίζει αντικειμενοστρεφή (αλλά με ξεκάθαρες απόψεις, π.χ. χωρίς κληρονομικότητα), προστακτικό και συναρτησιακό προγραμματισμό (τύποι Option/Result, pattern matching)
- Πολλά νέα εργαλεία είναι γραμμένα σε Rust (π.χ. το SWC, αλλά και το Gleam, και είναι η δεύτερη υποστηριζόμενη γλώσσα στον πυρήνα του Linux· εξαρτόμαστε από αυτή για τον μετρητή γραμμών κώδικα)
- Έμφαση στην αξιοπιστία και την απόδοση
- Χτισμένη πάνω στο LLVM
- Η πιο αγαπημένη γλώσσα στην έρευνα του StackOverflow τα τελευταία 7 χρόνια
- Γρήγορη, εν μέρει λόγω του ελάχιστου πυρήνα και της τυπικής βιβλιοθήκης
- Ασφαλής, τόσο ως προς τη μνήμη όσο και ως προς τα νήματα, μέσω ownership και lifetimes, αμετάβλητη εξ ορισμού
- Ισχυρό σύστημα τύπων, που πιάνει πολλά bugs σε χρόνο μεταγλώττισης (ειδικά όσα σχετίζονται με τη μνήμη). Ο μεταγλωττιστής βγάζει πραγματικά χρήσιμα σφάλματα
- Όλα μέσα στο πακέτο: μεταγλωττιστής, εργαλείο build, formatter, διαχειριστής πακέτων, ενσωματώσεις IDE
- Υπέροχη τεκμηρίωση (υπάρχει ακόμη και ένα τεράστιο έγγραφο για το πώς λειτουργεί ο μεταγλωττιστής)
- Φορητή: μεταγλωττίζεται σε ένα μόνο στατικό εκτελέσιμο και δεν απαιτεί εγκατεστημένο runtime
- Εύκολη διαλειτουργικότητα με κώδικα C
- Αφαιρέσεις μηδενικού κόστους
- Ταυτοχρονισμός χωρίς φόβο
- Macros
Nim
- Αναπτύχθηκε από τον Andreas Rumpf (αρχικά ονομαζόταν Nimrod)
- Η σύνταξη είναι εμπνευσμένη από την Python
- Πολυπαραδειγματική
- Χρησιμοποιείται στο Exercism, στο configlet
- Εξαιρετική απόδοση: iterators χωρίς επιβάρυνση, προτίμηση στη δέσμευση στη στοίβα για τύπους βασισμένους σε τιμές
- Σύγχρονο, εκφραστικό σύστημα τύπων: εξαγωγή τύπων, tuples, generics, sum types, async/await
- Garbage collection, αλλά με υποστήριξη ντετερμινιστικής διαχείρισης μνήμης (πολλαπλές επιλογές διαχείρισης μνήμης)
- Εκτέλεση κώδικα σε χρόνο μεταγλώττισης
- Ενιαία σύνταξη κλήσης
- Macros: εύκολη επέκταση της γλώσσας
- Σύστημα effects: κωδικοποίηση των παρενεργειών στο σύστημα τύπων
Go
- Αναπτύχθηκε από τους Robert Griesemer, Rob Pike και Ken Thompson στη Google
- Κυρίως προστακτική/διαδικαστική, υποστηρίζει μια προσέγγιση τύπου αντικειμενοστρέφειας (αλλά χωρίς κληρονομικότητα)
- Χρησιμοποιείται σε μεγάλα έργα όπως το Docker και το Kubernetes. Είναι επίσης εξαιρετική για backends και CLIs (π.χ. το esbuild)
- Στοχεύει να είναι αρκετά απλή ώστε να τη χωράει το κεφάλι σου (λίγη σύνταξη)
- Ασφάλεια μνήμης μέσω garbage collector
- Γρήγορη: γρήγορη μεταγλώττιση, γρήγορα tests και γρήγορος χρόνος εκτέλεσης. Ενσωματωμένη υποστήριξη για τη συγγραφή benchmarks!
- Με ξεκάθαρες απόψεις: μεγάλη προσπάθεια να επηρεάσει το στυλ του κώδικα Go: λίγη σύνταξη, μορφοποίηση κώδικα μέσω go fmt, εργαλεία που ελέγχουν τον κώδικα για ιδιωματική χρήση, σφάλμα εκεί που άλλες γλώσσες βγάζουν προειδοποίηση (π.χ. αχρησιμοποίητες μεταβλητές), τεκμηρίωση που απαριθμεί τα ιδιώματα της Go που πρέπει να ακολουθείς
- Ελαφρύ σύστημα τύπων που κάνει τη Go πολύ ευέλικτη (υποστηρίζει εξαγωγή τύπων)
- Φορητή: μεταγλωττίζεται σε ένα μόνο στατικό εκτελέσιμο και δεν απαιτεί εγκατεστημένο runtime. Εύκολη διασταυρούμενη μεταγλώττιση. Ταυτοχρονισμός μέσω goroutines (ελαφριών) και επικοινωνία μέσω channels
- Δομική τυποποίηση μέσω interfaces (παρόμοια με το duck typing, αλλά με στατικό έλεγχο)
- Χειρισμός σφαλμάτων: η γλώσσα σε ενθαρρύνει να ελέγχεις και να χειρίζεσαι τα σφάλματα
VLang
- Αναπτύχθηκε από τους Alexander Medvednikov και Delyan Angelov
- Εμπνευσμένη από τη Go:
- Ίδια στρατηγική "ένας μόνο τρόπος να κάνεις τα πράγματα"
- Ίδια στρατηγική "ελάχιστης σύνταξης"
- Coroutines
- Διαφέρει από τη Go:
- Χωρίς nil/null, χρησιμοποιεί όμως result type
- Αμετάβλητη εξ ορισμού
- Sum types (συναρτησιακά)
- Παρεμβολή συμβολοσειρών
- Μικρότερο runtime/μικρότερα εκτελέσιμα
- Pattern matching
- Προσπαθούν να κάνουν τον GC προαιρετικό (autofree)
- Διαλειτουργικότητα με τη C μηδενικού κόστους
- Δημιουργία τεκμηρίωσης από τον κώδικα
- Γρήγορος μεταγλωττιστής που χρησιμοποιεί λίγη μνήμη
Zig
- Αναπτύχθηκε από τον Andrew Kelley
- Σχετικά λίγη σύνταξη (αρχείο γραμματικής PEG 500 γραμμών)
- Εκτέλεση κώδικα και reflection σε χρόνο μεταγλώττισης
- Στοχεύει να είναι "προφανής", χωρίς κρυφή ροή ελέγχου, κρυφές δεσμεύσεις μνήμης ή macros/μεταπρογραμματισμό
- Χειροκίνητη δέσμευση μνήμης
- Υποστηρίζει διαφορετικούς allocators
- Οι συναρτήσεις της τυπικής βιβλιοθήκης που δεσμεύουν μνήμη έχουν παράμετρο allocator
- Το πλαίσιο δοκιμών μπορεί να εντοπίσει διαρροές μνήμης
- Ασφάλεια:
- Τα σφάλματα είναι τιμές και πρέπει να τα χειρίζεσαι
- Χωρίς null, χρησιμοποιεί optional type
- Τα tests μπορούν να γραφτούν στο ίδιο αρχείο με τον πηγαίο κώδικα (όπως στη D)
- Χρησιμοποιεί το LLVM ως backend
- Μπορεί να μεταγλωττίσει κώδικα C/C++
- Εύκολη διαλειτουργικότητα με τη C
- Εύκολη διασταυρούμενη μεταγλώττιση
Επίλογος
Θα το αφήσουμε εδώ, γιατί φαντάζομαι ότι τα μυαλά όλων είναι αρκετά γεμάτα σε αυτό το σημείο.
Ελπίζω αυτή να ήταν μια χρήσιμη και διασκεδαστική εισαγωγή στις επιλεγμένες γλώσσες αυτού του μήνα. Ελπίζω να περάσεις υπέροχα εξερευνώντας αυτές τις γλώσσες. Τόσο ο Erik όσο και εγώ θα θέλαμε πολύ να ακούσουμε ποιες θα διαλέξεις και πώς σου φάνηκαν, οπότε γράψε σίγουρα στα σχόλια ή στο φόρουμ!
Ευχαριστώ που το είδες!