--- contributors: - ["Miltiadis Stouras", "https://github.com/mstou"] filename: learnhaskell-gr.hs --- Η Haskell σχεδιάστηκε για να είναι μια πρακτική, αγνή συναρτησιακή γλώσσα προγραμματισμού. Είναι διάσημη για τα monads και το σύστημα τύπων της, αλλά χρησιμοποιείται από πολλούς κυρίως για την κομψότητά της. Προσωπικά θεωρώ ότι είναι από τις πιο όμορφες, αν όχι η πιο όμορφη, γλώσσα προγραμματισμού. ```haskell -- Τα σχόλια μιας γραμμής ξεκινούν με 2 παύλες. {- Ενώ τα σχόλια πολλών γραμμών βρίσκονται μέσα σε blocks σαν αυτό -} ---------------------------------------------------- -- 1. Πρωτόγονοι Τύποι Δεδομένων (Primitive datatype) και Τελεστές ---------------------------------------------------- -- Οι αριθμοί είναι ένα primitive datatype 3 -- 3 -- Και οι τελεστές κάνουν αυτό που θα περιμέναμε 1 + 1 -- 2 8 - 1 -- 7 10 * 2 -- 20 35 / 5 -- 7.0 -- Η καθιερωμένη διαίρεση δεν είναι ακέραια 35 / 4 -- 8.75 -- Η ακέραια διαίρεση γίνεται με την συνάρτηση div 35 `div` 4 -- 8 -- Και οι boolean μεταβλητές ειναι primitives True False -- Πράξεις με booleans not True -- False not False -- True 1 == 1 -- True 1 /= 1 -- False 1 < 10 -- True -- Στα παραπάνω παραδείγματα, το `not` είναι μια συνάρτηση που παίρνει ένα όρισμα -- Στην Haskell δεν χρειάζονται παρενθέσεις για τις κλήσεις συναρτήσεων, όλες οι παράμετροι -- γράφονται με κενά αμέσως μετά την συνάρτηση. Στην γενική περίπτωση, -- η κλήση συνάρτησης μοιάζει κάπως έτσι: func arg1 arg2 arg3... -- Για το πως να ορίσετε τις δικές σας συναρτήσεις διαβάστε το κεφάλαιο των συναρτήσεων παρακάτω -- Συμβολοσειρές και χαρακτήρες "This is a string." -- συμβολοσειρά 'a' -- χαρακτήρας 'You cant use single quotes for strings.' -- error! -- δεν μπορούμε να γράψουμε συμβολοσειρές ανάμεσα από '' -- Οι συμβολοσειρές μπορούν να συννενωθούν με την χρήση του τελεστή ++ "Hello " ++ "world!" -- "Hello world!" -- Η συμβολοσειρά είναι ουσιαστικά μια λίστα χαρακτήρων ['H', 'e', 'l', 'l', 'o'] -- "Hello" "This is a string" !! 0 -- 'T' ---------------------------------------------------- -- 2. Λίστες και διατεταγμένα σύνολα (tuples) ---------------------------------------------------- -- Όλα τα στοιχεία μιας λίστας πρέπει να είναι του ίδιου τύπου -- Οι δύο παρακάτω λίστες είναι οι ίδιες: [1, 2, 3, 4, 5] [1..5] -- διάστημα ή range -- Τα διαστήματα μπορούν να χρησιμοποιηθούν και για άλλους τύπους εκτός από αριθμούς ['A'..'F'] -- "ABCDEF" -- Μπορούμε ακόμη να ορίσουμε και ένα βήμα [0,2..10] -- [0, 2, 4, 6, 8, 10] [5..1] -- [] (Το default βήμα της Haskell είναι το 1, επομένως η διπλανή λίστα είναι κενή) [5,4..1] -- [5, 4, 3, 2, 1] -- Προσπέλαση στοιχείου σε τυχαία θέση [1..10] !! 3 -- 4 (οι δείκτες των θέσεων ξεκινούν από το 0) -- Στην Haskell υπάρχουν και άπειρες λίστες! [1..] -- η λίστα των φυσικών αριθμών -- Οι άπειρες λίστες μπορούν να λειτουργούν επειδή η Haksell έχει "lazy evaluation". -- Αυτό σημαίνει ότι η Haskell κάνει υπολογισμούς μόνο όταν πραγματικά χρειάζεται! -- οπότε αν ζητήσουμε το 1000στό στοιχείο μιας άπειρης λίστας θα μας το δώσει, -- ξέρει ότι δεν χρειάζεται να υπολογίσει όλη την άπειρη λίστα πρώτα! [1..] !! 999 -- 1000 -- Στο παραπάνω παράδειγμα η Haskell υπολόγισε τα στοιχεία 1 μέχρι 1000...τα υπόλοιπα -- στοιχεία της άπειρης λίστας δεν υπάρχουν ακόμα! Η Haskell θα τα υπολογίσει -- μόνο αν κάποια στιγμή τα χρειαστεί. -- συνένωση δύο λιστών με τον τελεστή ++ (σε γραμμικό χρόνο) [1..5] ++ [6..10] -- προσθήκη στοιχείου στην αρχή της λίστας (σε σταθερό χρόνο) 0:[1..5] -- [0, 1, 2, 3, 4, 5] -- περισσότερες συναρτήσεις για τις λίστες head [1..5] -- 1 tail [1..5] -- [2, 3, 4, 5] init [1..5] -- [1, 2, 3, 4] last [1..5] -- 5 -- list comprehensions -- ένας άλλος τρόπος να ορίζουμε τις λίστες που θυμίζει πολύ τον ορισμό συνόλων στα μαθηματικά! [x*2 | x <- [1..5]] -- [2, 4, 6, 8, 10] -- list comprehension με συνθήκη [x*2 | x <- [1..5], x*2 > 4] -- [6, 8, 10] -- Κάθε στοιχείο ενός tuple μπορεί να έχει διαφορετικό τύπο, όμως το tuple έχει σταθερό μέγεθος. -- Ένα tuple: ("haskell", 1) -- προσπέλαση στοιχείων ενός ζεύγους στοιχείων (δηλαδή ενός tuple μεγέθους 2) fst ("haskell", 1) -- "haskell" snd ("haskell", 1) -- 1 -- οι παραπάνω συναρτήσεις δεν λειτουργούν σε tuples μεγαλύτερου μεγέθους snd ("snd", "can't touch this", "da na na na") -- error! ---------------------------------------------------- -- 3. Συναρτήσεις ---------------------------------------------------- -- Μια απλή συνάρτηση που παίρνει 2 μεταβλητές a, b και επιστρέφει το άθροισμά τους add a b = a + b -- Προσέξτε ότι αν χρησιμοποιείτε το διαδραστικό περιβάλλον της Haskell (ghci), δηλαδή -- τον interpreter, θα πρέπει να προσθέσετε ενα `let` πριν τον ορισμό της συνάρτησης: -- let add a b = a + b -- Κλήση της συνάρτησης add 1 2 -- 3 -- Μπορούμε να καλέσουμε την συνάρτηση και σαν τελεστή ανάμεσα στα 2 ορίσματα -- γράφοντας το όνομα της συνάρτησης μέσα σε backticks: 1 `add` 2 -- 3 -- Μπορούμε να ορίσουμε και συναρτήσεις που δεν έχουν γράμματα στο όνομά τους! -- Αυτό μας επιτρέπει να ορίσουμε δικούς μας τελεστές, όπως για παράδειγμα την ακέραια διάιρεση: (//) a b = a `div` b 35 // 4 -- 8 -- Guards: ένας εύκολος τρόπος να υλοποιήσουμε διακλαδώσεις σε μια συνάρτηση fib x | x < 2 = 1 | otherwise = fib (x - 1) + fib (x - 2) -- Το ταίριασμα προτύπων (Pattern matching) είναι παρόμοιο. -- Εδώ δίνουμε 3 διαφορετικούς ορισμούς για την συνάρτηση fib -- H Haskell θα χρησιμοποιήσει αυτόματα τον πρώτο ορισμό το οποίου οι παράμετροι -- ταιριάζουν με τις παραμέτρους της κλήσης fib 1 = 1 fib 2 = 2 fib x = fib (x - 1) + fib (x - 2) -- Pattern matching σε tuples sndOfTriple (_, y, _) = y -- η κάτω παύλα χρησιμοποιείται για να μην δίνουμε ονόματα -- σε μεταβλητές που δεν θα χρησιμοποιήσουμε και -- ταιριάζει με όλους τους τύπους -- Pattern matching σε λίστες. -- Στο παρακάτω παράδειγμα, το `x` είναι το πρώτο στοιχείο της λίστας -- και τo `xs` είναι η λίστα με τα υπόλοιπα στοιχεία myMap func [] = [] myMap func (x:xs) = func x : (myMap func xs) -- Μπορούμε να ορίσουμε και ανώνυμες συναρτήσεις (lambdas) χρησιμοποιώντας το -- backslash (που μοιάζει με λ) ακολουθούμενο από τις παραμέτρους: myMap (\x -> x + 2) [1..5] -- [3, 4, 5, 6, 7] -- χρήση της συνάρτησης fold με μία ανώνυμη συνάρτηση -- Το foldl1 είναι σαν fold από αριστερά, αλλά χρησιμοποιεί σαν αρχική τιμή του -- accumulator το πρώτο στοιχείο της λίστας. foldl1 (\acc x -> acc + x) [1..5] -- 15 ---------------------------------------------------- -- 4. Περισσότερες συναρτήσεις ---------------------------------------------------- -- Μερική κλήση: αν δεν περάσουμε όλες τις μεταβλητές σε μια συνάρτηση, -- τότε αυτή "καλείται μερικώς". Αυτό σημαίνει ότι μας επιστρέφει μια συνάρτηση -- η οποία παίρνει ως ορίσματα τις εναπομείνασες μεταβλητές add a b = a + b foo = add 10 -- η foo είναι μια συνάρτηση που περιμένει 1 αριθμό και του προσθέτει 10 foo 5 -- 15 -- Ένας άλλος τρόπος να γράψουμε το ίδιο πράγμα: foo = (10+) foo 5 -- 15 -- Σύνθεση συναρτήσεων -- Ο τελεστής `.` χρησιμοποιείται για την σύνθεση ("αλυσίδωση") συναρτήσεων. -- Για παράδειγμα, η foo παρακάτω είναι μια συνάρτηση που παίρνει ως όρισμα 1 αριθμό. -- Πρώτα προσθέτει 10 στον αριθμό που δώσαμε και μετά πολλαπλασιάζει το αποτέλεσμα με 4 foo = (4*) . (10+) -- 4*(10+5) = 60 foo 5 -- 60 -- διόρθωση προτεραιότητας -- Στην Haskell υπάρχει ο τελεστής `$`. Ο τελεστής αυτός εφαρμόζει μια συνάρτηση -- σε μία παράμετρο. Σε αντίθεση με την απλή εφαρμογή συνάρτησης, η οποία έχει -- την μεγαλύτερη πιθανή προτεραιότητα και είναι αριστερά προσεταιριστική, -- ο τελεστής `$` έχει την ελάχιστη προτεραιότητας και είναι δεξιά προσεταιριστικός. -- Λόγω της χαμηλής του προτεραιότητας, η έκφραση που βρίσκεται στα δεξιά του -- θα υπολογιστεί και θα περαστεί σαν παράμετρος στην συνάρτηση που βρίσκεται στα αριστερά του -- πριν even (fib 7) -- false -- ισοδύναμα even $ fib 7 -- false -- χρησιμοποιόντας σύνθεση συναρτήσεων even . fib $ 7 -- false ---------------------------------------------------- -- 5. Τύποι ---------------------------------------------------- -- Η Haskell έχει ένα πολύ ισχυρό σύστημα τύπων, στο οποίο κάθε έκφραση έχει έναν τύπο -- Κάποιο βασικοί τύποι: 5 :: Integer "hello" :: String True :: Bool -- Και οι συναρτήσεις έχουν κάποιο τύπο -- Η συνάρτηση`not` παίρνει ένα boolean και επιστρέφει ένα boolean: -- not :: Bool -> Bool -- Παρακάτω βλέπετε μια συνάρτηση που παίρνει 2 ορίσματα: -- add :: Integer -> Integer -> Integer -- Όταν ορίζουμε μια συνάρτηση ή μεταβλητή, είναι καλή πρακτική να γράφουμε -- και τον τύπο της: double :: Integer -> Integer double x = x * 2 ---------------------------------------------------- -- 6. Έλεγχος ροής και συνθήκες ---------------------------------------------------- -- if-expressions haskell = if 1 == 1 then "awesome" else "awful" -- haskell = "awesome" -- τα if-expressions μπορούν να πιάνουν και πολλές γραμμές -- αλλά η στοίχιση είναι σημαντική! haskell = if 1 == 1 then "awesome" else "awful" -- case expressions: Με τον παρακάτω τρόπο θα μπορούσαμε να κάνουμε parse -- command line arguments case args of "help" -> printHelp "start" -> startProgram _ -> putStrLn "bad args" -- Η Haskell δεν έχει βρόχους επανάληψης; αντιθέτως, χρησιμοποιούμε αναδρομή. -- Η συνάρτηση map εφαρμόζει μια συνάρτηση σε κάθε στοιχείο μιας λίστας map (*2) [1..5] -- [2, 4, 6, 8, 10] -- μπορούμε να κατασκευάσουμε τον βρόχο for χρησιμοποιώντας την map for array func = map func array -- και να τον χρησιμοποιήσουμε for [0..5] $ \i -> show i -- το παραπάνω θα μπορούσε να γραφτεί και έτσι: for [0..5] show -- Μπορούμε να χρησιμοποιήσουμε τις συναρτήσεις foldl και foldr -- για να υπολογίζουμε μια τιμή από μια λίστα (πχ άθροισμα ή γινόμενο) -- foldl foldl (\x y -> 2*x + y) 4 [1,2,3] -- 43 -- Η παραπάνω κλήση είναι η ίδια με: (2 * (2 * (2 * 4 + 1) + 2) + 3) -- Η foldl γίνεται από τα αριστερά ενώ η foldr από τα δεξιά foldr (\x y -> 2*x + y) 4 [1,2,3] -- 16 -- Η παραπάνω κλήση είναι τώρ: (2 * 1 + (2 * 2 + (2 * 3 + 4))) ---------------------------------------------------- -- 7. Τύποι δεδομένων ---------------------------------------------------- -- Με τον παρακάτω τρόπο μπορούμε να ορίζουμε δικούς μας τύπους -- δεδομένων στην Haskell data Color = Red | Blue | Green -- Τώρα μπορούμε να χρησιμοποιήσουμε τον τύπο μας και σε συναρτήσεις: say :: Color -> String say Red = "You are Red!" say Blue = "You are Blue!" say Green = "You are Green!" -- Οι τύποι δεδομένων μας μπορεί να είναι και παραμετρικοί, να δέχονται δηλαδή -- κάποιον τύπο ως παράμετρο data Maybe a = Nothing | Just a -- Όλες οι παρακάτω τιμές έχουν τύπο Maybe Just "hello" -- of type `Maybe String` Just 1 -- of type `Maybe Int` Nothing -- of type `Maybe a` for any `a` ---------------------------------------------------- -- 8. Haskell IO ---------------------------------------------------- -- Αν και το IO δεν μπορεί να εξηγηθεί σε βάθος χωρίς να εξηγήσουμε -- πρώτα τα monads, δεν είναι δύσκολο να το εξηγήσουμε αρκετά ώστε να μπορεί -- κάποιος να το χρησιμοποιήσει -- Όταν ένα πρόγραμμα Haskell εκτελείται, καλείται η συνάρτηση `main` -- Η συνάρτηση αυτή πρέπει να επιστρέφει τύπο `IO a` για κάποιο τύπο `a`. -- Για παράδειγμα: main :: IO () main = putStrLn $ "Hello, sky! " ++ (say Blue) -- η συνάρτηση putStrLn έχει τύπο: String -> IO () -- Είναι πιο εύκολο να χρησιμοποιήσουμε IO αν μπορούμε να γράψουμε το πρόγραμμά μας -- ως μια συνάρτηση από String σε String. Η συνάρτηση -- interact :: (String -> String) -> IO () -- παίρνει ως είσοδο ένα string, τρέχει μια συνάρτηση πάνω στην είσοδο -- και τυπώνει την έξοδο countLines :: String -> String countLines = show . length . lines main' = interact countLines -- Μπορείτε να σκεφτείτε μια συνάρτηση που επιστρέφει τιμή με τύπο `IO ()` -- ως μια ακολουθία πράξεων, περίπου όπως και σε μια imperative γλώσσα -- Μπορούμε να χρησιμοποιήσουμε το `do` και να ενώσουμε αυτές τις κλήσεις -- Για παράδειγμα: sayHello :: IO () sayHello = do putStrLn "What is your name?" name <- getLine -- η συνάρτηση αυτή διαβάζει μια γραμμή και την αναθέτει στην μετβαλήτη name putStrLn $ "Hello, " ++ name -- Δοκιμάστε να γράψετε την συνάρτηση `interact` που θα διαβάζει μια γραμμή -- Ωστόσο ο κώδικας της συνάρτησης `sayHello` δεν θα εκτελεστεί ποτέ. Η μόνη συνάρτηση -- που εκτελείται όταν κάνουμε compile ένα αρχείο haskell είναι η `main`. -- Αν θέλετε να τρέξετε την sayHello (εκτός από το να φορτώσετε τον κώδικα στο -- ghci) μπορείτε να βάλετε σε σχόλια τον προηγούμενο ορισμό της main -- και να την ορίσετε ως: -- main = sayHello -- Ας προσπαθήσουμε να καταλάβουμε πως λειτουργεί η συνάρτηση `getLine` -- Ο τύπος της είναι: -- getLine :: IO String -- Μπορείτε να φανταστείτε ότι μια τιμή με τύπο `IO a` θα παραχθεί -- από ένα πρόγραμμα που παράγει μια τιμή με τύπο `a` (ενώ παράλληλα κάνει και κάτι άλλο) -- Μπορούμε να πάρουμε και να επαναχρησιμοποιήσουμε αυτήν την τιμή χρησιμοποιώντας -- το `<-`. Μπορούμε ακόμα και να φτιάξουμε την δική μας συνάρτηση με τύπο -- `IO String`: action :: IO String action = do putStrLn "This is a line. Duh" input1 <- getLine input2 <- getLine -- Ο τύπος του `do` μπλοκ είναι εκείνος της τελευταίας γραμμής. -- Το `return` δεν είναι κάποια ειδική λέξη, αλλά απλώς μια συνάρτηση return (input1 ++ "\n" ++ input2) -- return :: String -> IO String -- Μπορούμε να χρησιμοποιήσουμε την παραπάνω συνάρτηση ακριβώς όπως την `getLine`: main'' = do putStrLn "I will echo two lines!" result <- action putStrLn result putStrLn "This was all, folks!" -- Ο τύπος `IO` είναι παράδειγμα ενός "monad". Χρησιμοποιώντας τα monads για το -- ΙΟ, η Haskell καταφέρνει να είναι αγνή συναρτησιακή γλώσσα. Κάθε συνάρτηση που -- αλληλεπιδρά με τον έξω κόσμο (δηλαδή κάνει IO), έχει το IO (ή κάποιο άλλο monad) -- στον τύπο της. Αυτό μας διευκολύνει να γνωρίζουμε ποιές συναρτήσεις είναι αγνές -- (μαθηματικές -- δεν αλληλεπιδρούν με τον έξω κόσμο ούτε αλλάζουν κάποιο state) -- και ποιες δεν είναι. -- Αυτό είναι ένα πολύ ισχυρό χαρακτηριστικό γιατί είναι πολύ εύκολο να -- εκτελούμε παράλληλα αγνές συναρτήσεις! Οπότε η παραλληλοποίηση στην Haskell -- είναι αρκετά πιο εύκολη ---------------------------------------------------- -- 9. Haskell REPL ---------------------------------------------------- -- Μπορείτε να ξεκινήσετε το διαδραστικό περιβάλλον της Haskell με την εντολή `ghci`. -- Εδώ μπορείτε να γράψετε και να εκτελέσετε κώδικα haskell. -- Κάθε νέα τιμή πρέπει να ορίζεται με το `let` let foo = 5 -- Μπορείτε να βρείτε τον τύπο μιας συνάρτησης με το `:t`: > :t foo foo :: Integer -- Οι τελεστές, όπως οι `+`, `:` και `$`, είναι επίσης συναρτήσεις. -- Μπορούμε να δούμε τον τύπο τους βάζοντας τους μέσα σε παρενθέσεις: > :t (:) (:) :: a -> [a] -> [a] -- Για περισσότερες πληροφορίες για οποιαδήποτε συνάρτηση ή τύπο, -- μπορείτε να χρησιμοποιήσετε το `:i`: > :i (+) class Num a where (+) :: a -> a -> a ... -- Defined in ‘GHC.Num’ infixl 6 + -- Μπορείτε επίσης να τρέξετε κάθε συνάρτηση με τύπο `IO ()` > sayHello What is your name? Friend! Hello, Friend! ``` Υπάρχουν πολλά ακόμα πράγματα να εξερευνήσετε στην Haskell, όπως τα typeclasses και διάφορα monads! Αυτές οι μαθηματικά ορισμένες έννοιες είναι που κάνουν την Haskell αυστηρή, αγνή και κομψή! Θα τελειώσουμε αυτήν την σύντομη περιήγηση με ένα τελευταίο παράδειγμα, η υλοποίηση της QuickSort σε Haskell: ```haskell qsort [] = [] qsort (p:xs) = qsort lesser ++ [p] ++ qsort greater where lesser = filter (< p) xs greater = filter (>= p) xs ``` Υπάρχουν 2 παραδοσιακοί τρόποι να εγκαταστήσετε την Haskell: - [Cabal-based installation](http://www.haskell.org/platform/), - [Stack-based process](https://www.stackage.org/install). Στις παρακάτω πηγές μπορείτε να βρείτε αρκετά κομψές εισαγωγές στην Haskell - [Learn you a Haskell](http://learnyouahaskell.com/), - [Happy Learn Haskell Tutorial](http://www.happylearnhaskelltutorial.com/), - [Real World Haskell](http://book.realworldhaskell.org/)