Lập trình · Ngôn ngữ lập trình
Hướng dẫn học lập trình Haskell cơ bản cho người mới
Trả lời nhanh
Haskell là ngôn ngữ hàm thuần túy, kiểu tĩnh, có suy luận kiểu và đánh giá lười. Chương trình được xây dựng từ các hàm thuần túy, kiểu dữ liệu đại số và khớp mẫu, còn hiệu ứng phụ được theo dõi trong kiểu như IO. Trang này bao gồm cài GHC, kiểu dữ liệu, hàm và guard, danh sách và hàm bậc cao, kiểu dữ liệu, type class, Maybe và Either, cùng một ví dụ hoàn chỉnh.
Haskell là gì và dùng để làm gì
Haskell là ngôn ngữ lập trình hàm thuần túy với hệ thống kiểu tĩnh mạnh, suy luận kiểu và đánh giá lười. Nó được một nhóm nhà nghiên cứu thiết kế và xác định lần đầu năm 1990; chuẩn hiện hành là Haskell 2010, và GHC (Glasgow Haskell Compiler) là bản triển khai chính. Trong Haskell, hàm không có hiệu ứng phụ trừ khi kiểu của nó cho biết, nên chương trình dễ suy luận và kiểm thử hơn.
Haskell được dùng trong tài chính và fintech, trình biên dịch và công cụ cho ngôn ngữ lập trình (Pandoc và trình linter ShellCheck được viết bằng Haskell), các dự án blockchain, backend web và nghiên cứu. Nhiều người cũng học nó vì các ý tưởng như kiểu dữ liệu đại số, type class và hàm thuần túy đã ảnh hưởng đến Rust, Swift, Scala, Kotlin và TypeScript.
Cài GHC và chạy Hello, world
Trình cài đặt được khuyên dùng là GHCup, cài GHC, các công cụ build Cabal và Stack, cùng Haskell Language Server cho trình soạn thảo. Mọi chương trình đều có một action main kiểu IO ().
-- hello.hs
main :: IO ()
main = putStrLn "Hello, world!"runghc hello.hs # run without compiling
# Hello, world!
ghc hello.hs && ./hello # compile to a native executable
ghci # interactive REPL; load a file with :load hello.hsChú thích bắt đầu bằng --. Trong GHCi, :type expression (hoặc :t) hiển thị kiểu của bất kỳ biểu thức nào, đây là một trong những cách học tốt nhất.
Giá trị, kiểu và suy luận kiểu
Tên chỉ được gắn một lần và không bao giờ thay đổi: không có biến khả biến. Khai báo kiểu viết bằng :: và là tùy chọn vì trình biên dịch tự suy luận, nhưng viết chúng cho các định nghĩa cấp cao nhất là thói quen tốt.
name :: String
name = "Haskell"
year :: Int
year = 1990
ratio :: Double
ratio = fromIntegral year / 1000 -- convert Int to Double explicitly
main :: IO ()
main = do
putStrLn (name ++ " " ++ show year) -- Haskell 1990
print ratio -- 1.99
print (7 `div` 2, 7 `mod` 2) -- (3,1)
print (2 ^ 64 :: Integer) -- 18446744073709551616| Kiểu | Ví dụ | Ghi chú |
|---|---|---|
Int | 42 | số nguyên kích thước cố định (64 bit với GHC trên các nền tảng phổ biến) |
Integer | 2 ^ 100 | số nguyên độ chính xác tùy ý |
Double | 3.14 | số thực dấu phẩy động 64 bit |
Bool | True, False | các constructor viết hoa chữ cái đầu |
Char | 'h' | một ký tự Unicode |
String | "text" | danh sách Char, tức [Char]; Data.Text nhanh hơn |
show chuyển một giá trị thành String, và print là putStrLn . show. Số không bao giờ được chuyển đổi ngầm; hãy dùng fromIntegral, round hoặc truncate.
Hàm, guard và khớp mẫu
Lời gọi hàm viết bằng dấu cách chứ không dùng ngoặc: add 2 3. Mỗi hàm nhận một đối số và trả về một hàm khác cho phần còn lại, tính năng này gọi là currying, nên add 2 tự nó là một hàm. Khớp mẫu và guard thay thế hầu hết các chuỗi if.
add :: Int -> Int -> Int
add x y = x + y
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
grade :: Int -> Char
grade score
| score >= 90 = 'A'
| score >= 70 = 'B'
| otherwise = 'C'
describe :: [a] -> String
describe [] = "empty"
describe [_] = "one item"
describe _ = "many items"
main :: IO ()
main = do
print (add 2 3) -- 5
print (map (add 10) [1, 2, 3]) -- [11,12,13] (partial application)
print (factorial 20) -- 2432902008176640000
print (grade 75) -- 'B'
putStrLn (describe "hi") -- many items
print (if even 4 then "even" else "odd") -- "even"if là một biểu thức và luôn cần else. Không có vòng lặp: việc lặp được thực hiện bằng đệ quy hoặc các hàm bậc cao như map, filter và foldr.
Danh sách, tuple và hàm bậc cao
Danh sách chứa các giá trị cùng một kiểu và là collection phổ biến nhất. Tuple nhóm một số lượng cố định các giá trị có thể khác kiểu. List comprehension, khoảng giá trị và toán tử hợp thành hàm . giúp xử lý dữ liệu ngắn gọn.
import Data.List (sortOn)
main :: IO ()
main = do
let nums = [5, 3, 8, 1] :: [Int]
print (head nums, length nums, sum nums) -- (5,4,17)
print (0 : nums) -- [0,5,3,8,1] (prepend)
print (filter even nums) -- [8]
print (foldr (+) 0 [1 .. 10]) -- 55
print [x * x | x <- [1 .. 5], odd x] -- [1,9,25]
print (take 5 [1, 3 ..]) -- [1,3,5,7,9] (lazy infinite list)
let people = [("Ann", 31), ("Bo", 25)] :: [(String, Int)]
print (map fst (sortOn snd people)) -- ["Bo","Ann"]
print ((length . filter (> 2)) nums) -- 3
print (zip [1 :: Int ..] "abc") -- [(1,'a'),(2,'b'),(3,'c')]Vì Haskell lười, giá trị chỉ được tính khi cần. Đó là lý do take 5 [1, 3 ..] chạy được trên danh sách vô hạn. Tính lười cũng có thể tích tụ những biểu thức lớn chưa được đánh giá; hãy ưu tiên foldl' trong Data.List hơn foldl khi tính tổng danh sách dài.
Kiểu dữ liệu đại số và type class
Bạn tự định nghĩa kiểu bằng data. Một kiểu có thể có nhiều constructor (kiểu tổng), mỗi constructor có các trường (kiểu tích). Các type class như Show, Eq, Ord và Functor mô tả hành vi dùng chung, tương tự interface; deriving tự động sinh các instance phổ biến.
data Shape = Circle Double | Rect Double Double
deriving (Show, Eq)
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rect w h) = w * h
data User = User { userName :: String, userAge :: Int }
deriving Show
class Describable a where
describe :: a -> String
instance Describable Shape where
describe (Circle _) = "a circle"
describe (Rect _ _) = "a rectangle"
main :: IO ()
main = do
print (map area [Circle 1, Rect 2 3]) -- [3.141592653589793,6.0]
putStrLn (describe (Rect 1 1)) -- a rectangle
let u = User { userName = "Ann", userAge = 31 }
print (u { userAge = 32 }) -- User {userName = "Ann", userAge = 32}Maybe, Either và IO
Haskell không có null và không có ngoại lệ không kiểm tra trong mã thuần túy. Giá trị có thể không tồn tại có kiểu Maybe a (Just x hoặc Nothing), và phép tính có thể thất bại kèm lý do có kiểu Either e a (Left err hoặc Right x). Các hiệu ứng phụ như đọc đầu vào nằm trong kiểu IO, và cú pháp do sắp xếp chúng theo trình tự.
import Text.Read (readMaybe)
safeDiv :: Int -> Int -> Either String Int
safeDiv _ 0 = Left "division by zero"
safeDiv x y = Right (x `div` y)
parseAge :: String -> Maybe Int
parseAge s = case readMaybe s of
Just n | n >= 0 -> Just n
_ -> Nothing
main :: IO ()
main = do
print (safeDiv 10 2) -- Right 5
print (safeDiv 1 0) -- Left "division by zero"
print (parseAge "42") -- Just 42
print (parseAge "abc") -- Nothing
print (fmap (+ 1) (Just 41)) -- Just 42
putStrLn "Your name?"
n <- getLine -- an IO action that returns a String
putStrLn ("Hi, " ++ n)Một chương trình hoàn chỉnh nhỏ: tần suất từ
Chương trình này đọc văn bản từ đầu vào chuẩn, đếm tần suất từ bằng Map và in ba từ phổ biến nhất. Nó dùng hợp thành hàm, Data.Map từ gói containers đi kèm GHC, và sắp xếp.
import Data.Char (isAlpha, toLower)
import Data.List (sortBy)
import Data.Ord (comparing, Down (..))
import qualified Data.Map.Strict as Map
wordFreq :: String -> [(String, Int)]
wordFreq =
sortBy (comparing (Down . snd) <> comparing fst)
. Map.toList
. Map.fromListWith (+)
. map (\w -> (w, 1))
. words
. map (\c -> if isAlpha c then toLower c else ' ')
main :: IO ()
main = do
text <- getContents
mapM_ (\(w, n) -> putStrLn (w ++ ": " ++ show n)) (take 3 (wordFreq text))echo "The cat and the dog. The dog sat, and the cat ran." | runghc wordfreq.hs
# the: 4
# and: 2
# cat: 2Học tiếp ở đâu
- Trang tài liệu tại haskell.org, có liên kết tới hướng dẫn, sách và GHC User's Guide.
- Hackage, kho gói, và Hoogle, công cụ tìm kiếm hàm theo tên hoặc theo kiểu.
- Learn You a Haskell for Great Good! và Real World Haskell, hai cuốn sách đọc miễn phí.
- Haskell 2010 Language Report cho định nghĩa chính xác của ngôn ngữ.
Câu hỏi thường gặp
Haskell có khó học không?
Nó mang lại cảm giác khác vì không có biến khả biến hay vòng lặp và dùng hệ thống kiểu giàu biểu đạt. Người mới học đệ quy, khớp mẫu và kiểu từng bước thường thấy phần lõi của ngôn ngữ nhỏ gọn và nhất quán.
Hàm thuần túy nghĩa là gì?
Hàm luôn trả về cùng một kết quả cho cùng một đầu vào và không thay đổi trạng thái hay thực hiện nhập xuất. Hiệu ứng vẫn có thể xảy ra, nhưng được thể hiện trong các kiểu như IO.
Monad trong Haskell là gì?
Monad là một type class cho các kiểu như Maybe, Either và IO, hỗ trợ nối chuỗi các phép tính bằng >>=. Cú pháp do là cách viết cho việc nối chuỗi đó.
Đánh giá lười là gì?
Biểu thức chỉ được đánh giá khi cần đến giá trị của nó. Điều này cho phép danh sách vô hạn và tránh công việc không cần thiết, nhưng có thể khiến mức dùng bộ nhớ khó dự đoán hơn.
Nên dùng Stack hay Cabal?
Cả hai đều build dự án Haskell và đều được GHCup cài đặt. Cabal là công cụ build tiêu chuẩn; Stack bổ sung các snapshot gói được chọn lọc. Với người mới, cái nào cũng được.