Chuyên mục lưu trữ: Haskell

Haskell – Function

[Đọc bài này mất trung bình 3 phút]

Có thể bạn không biết, nhưng từ phần đầu tiên đến giờ chúng ta liên tục dùng hàm (Function). Chẳng hạn như dấu * là hàm thực hiện phép nhân 2 số, và chúng ta đặt 2 số cần nhân với nhau ở 2 bên trái phải của dấu *. Tuy nhiên hầu hết các hàm không nhận các tham số là số nguyên thì lại được gọi bằng cách đặt các tham số vào bên phải của tên hàm.

Cách gọi hàm

Thông thường trong các ngôn ngữ mệnh lệnh thì chúng ta hay gọi hàm bằng cách gọi tên, sau đó là cặp dấu (), bên trong là các tham số, ngăn cách nhau bới dấu phẩy.

Trong Haskell thì chúng ta gọi hàm bằng cách ghi tên hàm ra, sau đó là các tham số, cách nhau bởi 1 dấu khoảng trống. Ví dụ:

ghci> succ 8
9

Hàm succ có chức năng trả về một giá trị tiếp theo của giá trị được truyền vào, chẳng hạn như succ 8 = 9, succ 'a' = 'b', succ 1.1 = 2.1

Truyền nhiều tham số thì chúng ta chỉ việc ghi các tham số cách nhau bằng 1 dấu khoảng trống. Ví dụ:

ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
ghci> max 101 101
101

Hàm min lấy số nhỏ nhất giữa 2 số, hàm max lấy số lớn nhất giữa 2 số.

Các lệnh gọi hàm bao giờ cũng có độ ưu tiên cao hơn các câu lệnh thường, tức là trong một câu lệnh có nhiều lệnh, thì các lệnh gọi hàm sẽ được thực thi đầu tiên. Ví dụ:

ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1
16

Trong đoạn code trên thì succ 9max 5 4 sẽ được gọi trước rồi mới cộng kết quả của 2 hàm này lại và + 1. Cả 2 câu lệnh có dùng () và không dùng () đều có độ ưu tiên như nhau.

Nếu chúng ta muốn thực hiện các lệnh sau trước thì chúng ta phải bọc các lệnh đó trong cặp dấu ngoặc (). Ví dụ:

ghci> max 5 3 * 2
10
ghci> max 5 (3 * 2)
6

Nếu một hàm nhận 2 tham số thì chúng ta có thể viết 2 tham số này ở 2 bên trái và phải của tên hàm. Tuy nhiên với cách gọi này, chúng ta phải ghi tên hàm trong cặp dấu `` vì Haskell không biết tham số nào truyền vào trước và tham số nào truyền vào sau. Ví dụ:

ghci> 5 `max` 3
5
ghci> max 5 3
5

Định nghĩa hàm

Để định nghĩa hàm thì đầu tiên chúng ta ghi giống như phần gọi hàm, sau đó thêm dấu = và bắt đầu ghi các câu lệnh bên trong hàm đó. Ví dụ:

ghci> doubleMe x = x + x

Ở đoạn code trên chúng ta định nghĩa hàm doubleMe, nhận vào 1 tham số tên là x. Hàm này sẽ x2 tham số này. Sau đó chúng ta có thể dùng như bình thường:

ghci> doubleMe 9
18
ghci> doubleMe 8.3
16.6

Chúng ta có thể dùng hàm trên bên trong phần định nghĩa của hàm khác. Ví dụ:

ghci> tripleMe x = doubleMe x + x
ghci> tripleMe 3
9

Haskell – Lệnh rẽ nhánh

[Đọc bài này mất trung bình < 1 phút]

Câu lệnh rẽ nhánh trong Haskell khá đơn giản và giống hầu hết các ngôn ngữ khác. Cú pháp là:

if <biểu_thức> then

<Các câu lệnh>

else if <biểu_thức> then

<Các câu lệnh>

...

else

<Các câu lệnh>

Ví dụ:

main :: IO ()
main = do    
    let v = 10    
    if v `rem` 2 == 0 then
        putStrLn "Even number"    
    else
        putStrLn "Odd number"   

Trong đoạn code trên, chúng ta định nghĩa biến v có giá trị là 10. Sau đó kiểm tra xem v là số lẻ (odd nunber) hay số chẵn (even number).

Ở đây chúng ta có dùng từ khóa do. Chúng ta sẽ tìm hiểu về từ khóa này sau.

Ngoài ra Haskell có 2 hàm để kiểm tra xem 1 số có phải số lẻ hay số chẵn không, đó là oddeven. Ví dụ:

main :: IO ()
main = do
    let v = 9
    if v == 0 then
        putStrLn "Zero is neither odd nor even"
    else if even v then
        putStrLn "Even number"
    else if odd v then
        putStrLn "Odd number"
    else 
        putStrLn "Unknown"

Haskell – Các toán tử cơ bản

[Đọc bài này mất trung bình 3 phút]

Chúng ta sẽ tìm hiểu các toán tử cơ bản của Haskell, đa phần là giống với các ngôn ngữ lập trình thông thường.

Comment

Là phần không được biên dịch, chỉ để ghi chú cho coder dễ hiểu. Để comment một dòng nào đó thì chúng ta đặt 2 dấu - phía trước.

n :: Int
n = 3
-- n = 5 

Và để comment nhiều dòng thì chúng ta đặt trong cặp dấu {- -}

{-
main :: IO()
main = return()
-}

Các phép toán đại số

Ví dụ:

-- Phép cộng
p1 = 2 + 5    -- KQ = 7

-- Phép trừ
p2 = 10 - 6   -- KQ = 4

-- Phép nhân
p3 = 10 * 10  -- KQ = 100

-- Phép chia
p4 = 10 / 2   -- KQ = 2.0. Lưu ý là kết quả là kiểu số thực.

-- Phép chia lấy phần dư
p5 = 10 `rem` 2 -- KQ = 0. 10 chia 2 được 5 và dư 0

-- Phép lũy thừa
p6 = 10 ^ 10  -- KQ = 100
p7 = 2 ^ 3    -- KQ = 8.0. Kết quả là kiểu số thực
p8 = 6 ** 6   -- KQ = 46656.0. Kết quả là kiểu số thực  

Các phép toán thao tác bit

Trước khi thực hiện các phép toán này chúng ta phải dùng đến module Data.Bits bằng cách đặt câu lệnh import Data.Bits trước, chúng ta sẽ tìm hiểu thêm về module sau. Ví dụ:

import Data.Bits

-- AND
255 .&. 170    -- KQ = 170
-- OR
255 .|. 170    -- KQ = 255
-- XOR
255 `xor` 170  -- KQ = 85
-- Ngịch đảo tất cả các bit
complement 255 -- KQ = -256
-- Dịch bit trái 
shiftL 6 2     -- KQ = 24, dịch các bit của số 6 sang trái 2 bit
-- Dịch bit phải
shiftR 6 1     -- KQ = 3, dịch các bit của số 6 sang phải 1 bit

Các phép so sánh

Ví dụ:

-- Lớn hơn
2 > 3   -- False
-- Lớn hơn hoặc bằng
2 >= 3  -- False
-- Bé hơn
2 < 3   -- True
-- Bé hơn hoặc bằng
2 <= 3  -- True
-- Bằng nhau
2 == 3  -- False
-- Khác nhau
2 /= 3  -- True

Haskell – Kiểu dữ liệu

[Đọc bài này mất trung bình 4 phút]

Trong phần này chúng ta sẽ tìm hiểu các kiểu dữ liệu có trong Haskell.

Khai báo kiểu

Đầu tiên chúng ta phải biết cách khai báo kiểu dữ liệu cho một biến đã. Cú pháp khai báo là:

<tên_biến> :: <tên_kiểu>

Ví dụ:

main :: IO()
a :: Int
b :: Bool
main = return()

Các kiểu cơ bản

Haskell cũng có một số kiểu như trong các ngôn ngữ khác.

Bool

Mang giá trị đúng / sai (True / False). Ví dụ:

main :: IO()

a :: Bool
a = False

b :: Bool 
b = True

main = return()

Int

Đây là kiểu dữ liệu lưu trữ số nguyên có dấu, miền giá trị từ [-229, 229-1].

main :: IO()

i :: Int
i = 2020

j :: Int
j = -99999999
main = return()

Integer

Đây cũng là kiểu dữ liệu lưu trữ số nguyên, nhưng có thể lưu những con số rất lớn, thậm chí có thể dùng hết bộ nhớ trong máy để lưu.

main :: IO()
bi :: Integer
bi = 94919521698492921921919551295194198981621984623...
main = return()

Char

Đây là kiểu dữ liệu lưu trữ kí tự. Ví dụ:

main :: IO()
c :: Char
c = 'a'
main = return()

Lưu ý là chúng ta chỉ có thể bọc các kí tự trong cặp dấu nháy đơn ', không phải cặp dấu nháy kép ".

String

Cũng lưu trữ kí tự nhưng là một chuỗi nhiều kí tự.

main :: IO()
c :: String
c = "Phocode - Haskell"
main = return()

Khác với Char, chúng ta phải bọc các chuỗi String trong cặp dấu nháy đôi ".

Float/Double

Đây là kiểu dữ liệu lưu trữ số thập phân, điểm khác biệt là Float chỉ lưu 32 bit còn Double là 64 bit. (Tuy nhiên có nguồn cho biết trong các phiên bản Haskell hiện đại, cả Float và Double đều dùng 64 bit để lưu trữ).

main :: IO()

f :: Float
f = 3.1415

d :: Double
d = 9.9999999999

main = return()

List

Đây là kiểu danh sách. Cũng giống như mảng trong C++ hay Java, list trong Haskell chỉ lưu những phần tử có chung kiểu. Để khai báo một list thì chúng ta dùng cú pháp [tên_kiểu], ví dụ:

main :: IO()
list :: [Int]  -- List số nguyên
list = [1, 2, 3, 4, 5]
main = return()

Tuple

Nếu bạn đã từng lập trình Python thì bạn sẽ quen với kiểu tuple này, chúng ta có thể coi nó đơn giản như là một list nhưng có thể lưu nhiều phần tử với nhiều kiểu dữ liệu khác nhau. Để khai báo tuple thì chúng ta dùng cú pháp (tên_kiểu_1, tên_kiểu_2,.... tên_kiểu_n), khác với list là chúng ta khai báo bao nhiêu kiểu trong tuple thì phải gán chính xác bấy nhiêu phần tử, ví dụ:

main :: IO()
list :: (Int, Int, String)
list = (16, 5, "phocode.com")
-- list = ('a', 5, "Hello") --- Sai vì phần tử đầu tiên sai kiểu
-- list = (1, 2, "Hello", 4, 5) -- Lỗi vì số phần tử là 5 trong khi khai báo chỉ có 3
main = return()

Haskell có thể gán kiểu động

Để linh hoạt hơn trong khai báo biến, chúng ta có thể không cần dùng câu lệnh khai báo kiểu dữ liệu (bằng 2 dấu ::), mà cứ gán dữ liệu rồi Haskell sẽ tự động gán kiểu dữ liệu cho biến đó giùm chúng ta.

main :: IO()
num = 3 -- Int
char = 'a' -- Char
str = "Phocode" -- String
fl = 3.14 -- Float
main = return()

Biến trong Haskell là bất biến

Tức là không thể thay đổi giá trị của biến đó được.

main :: IO()
num = 3
num = 4 -- Lỗi Multiple declaration of 'num'
main = return()

Haskell – Hello World với VS Code

[Đọc bài này mất trung bình 4 phút]

Trong phần trước chúng ta đã cài đặt GHCI và sử dụng trong Command Prompt. Trong phần này chúng ta sẽ dùng Visual Studio Code làm IDE để có thể viết và quản lý code dễ dàng hơn.

VS Code

Đầu tiên chúng ta tải và cài đặt VS Code tại địa chỉ https://code.visualstudio.com/.

Cài Extension trên VS Code

Chúng ta cài các extension sau đây:

  • Haskell Syntax Highlighting: hiển thị màu chữ các keyword, comment, biến…v.v
  • haskell-linter: kiểm tra cú pháp và đề xuất cách viết clean code

Chúng ta chỉ cần 2 extension đó là đủ rồi, không cần quá nhiều.

Cài hlint

Ở thời điểm hiện tại, haskell-linter chỉ là một wrapper của phần mềm hlint, nghĩa là extension này thực chất sẽ sử dụng hlint để thực hiện các công việc của một phần mềm linter. Vì vậy sau khi cài, extension này sẽ báo lỗi không tìm thấy hlint.

Nên chúng ta phải cài hlint trước, chúng ta có thể cài thông qua Cabal – đây là bộ công cụ giúp quản lý các gói phần mềm dành cho Haskell, đi kèm khi chúng ta cài Haskell Platform.

Đầu tiên chúng ta chạy lệnh cabal update:

C:>cabal update
Downloading the latest package list from package.haskell.org

Tiếp theo chúng ta cài hlint bằng lệnh cabal install hlint:

C:>cabal install hlint
Resolving dependencies...
...

Cabal sẽ tải và cài đặt hlint, việc này có thể tốn tầm 5-10 phút.

Sau khi đã cài xong, chúng ta có thể kiểm tra bằng cách chạy lệnh hlint -V để xem phiên bản hlint hiện tại:

C:>hlint -V
HLint v3.1.1, Copyright Neil Mitchell 2006-2020

Tạo project

Chúng ta tạo một thư mục nào đó, rồi mở thư mục đó trong VS Code, sau đó tạo một file, đặt tên chẳng hạn như main.hs, *.hs là đuôi của file Haskell. Viết đoạn code sau vào file này:

main :: IO()
main = putStrLn "Hello World"

Để chạy thì chúng ta mở Terminal trong VS Code lên, biên dịch bằng lệnh ghc main.hs:

Trình ghc sẽ biên dịch và link file main.hs và tạo ra file main.exe. Để chạy thì chúng ta chỉ việc gọi file này:

Ngoài ra chúng ta cũng có thể dùng lệnh runhaskell main.hs, lệnh này sẽ sẽ làm công việc thông dịch, nghĩa là không biên dịch ra file .exe mà chỉ chạy từng lệnh như các ngôn ngữ Python, Perl, Ruby…v.v

Lưu ý

Khi chúng ta viết một đoạn code Haskell thì trình biên dịch sẽ luôn luôn tìm một biến có tên là main và có kiểu là IO(). Ngoài ra quy tắc của Haskell là khi đã khai báo một biến thì phải gán dữ liệu cho biến đó. Ở đây dòng main :: IO() là dòng khai báo kiểu, còn dòng main = putStrLn "Hello World" tiếp theo là dòng gán, nếu không có dòng đầu và dòng thứ 2 thì trình biên dịch sẽ luôn luôn báo lỗi.

Ở đây để ví dụ nên chúng ta viết dòng in ra màn hình. Trên thực tế chúng ta có thể viết như sau:

main :: IO()
main = return() --- Không làm gì cả

Haskell – Cài đặt

[Đọc bài này mất trung bình 3 phút]

Lưu ý: phần này hướng dẫn cách cài đặt Haskell phiên bản 8.10.1 trên Windows 10. Phiên bản mới hơn hoặc các hệ điều hành khác có thể có cách cài khác.

Haskell có 3 bộ cài đặt là Minimal Installers, StackHaskell Platform. Chúng ta sẽ cài bộ Haskell Platform.

Chúng ta phải sử dụng trình Powershell dưới quyền Administrator trên Windows thì mới có thể cài được. Để mở Powershell thì chúng ta có thể bấm phím Windows+X và chọn Windows Powershell (Admin).

Cài đặt Chocolatey

Chocolatey là một trình quản lý phần mềm dành cho Windows. Đầu tiên chúng ta phải chạy lệnh sau:

Set-ExecutionPolicy Bypass -Scope Process

Lệnh này cho phép Powershell bỏ qua các bước kiểm tra độ an toàn của các phần mềm không rõ nguồn gốc. Dĩ nhiên ở đây chúng ta chỉ cài những thứ mà chúng ta biết rõ là an toàn, nên chỉ cần gõ A hoặc Y rồi Enter là xong.

Tiếp theo chúng ta chạy lệnh sau để cài Chocolatey:

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

Nếu khi cài mà cũng được hỏi thì chỉ cần gõ A hoặc YEnter.

Cài Haskell Platform

Sau khi đã cài xong Chocolatey, chúng ta chạy lệnh sau để cài Haskell Platform

choco install haskell-dev

Quá trình cài có thể mất vài phút.

Cuối cùng chúng ta chạy lệnh sau là xong.

refreshenv

Kiểm tra

Để kiểm tra xem Haskell Platform đã được cài đặt thành công chưa thì chúng ta chạy lệnh ghci. Nếu ra tương tự như hình dưới đây là thành công.

GHCI (Glasgow Haskell Compiler) là trình biên dịch dành cho ngôn ngữ Haskell. Khi chạy lệnh này thì chúng ta sẽ được sử dụng trình shell riêng của Haskell. Để thoát trình shell này thì chúng ta gõ :quit rồi Enter, lưu ý phải có dấu 2 chấm : trước quit.

Từ đây chúng ta có thể dùng ghci ở Powershell hay trình Command Prompt truyền thống đều được.

Haskell – Lập trình mệnh lệnh vs lập trình khai báo

[Đọc bài này mất trung bình 2 phút]

Có một câu nói về 2 khái niệm này: Lập trình mệnh lệnh (Imperative programming) nhấn mạnh tới việc chúng ta làm như thế nào, còn lập trình khai báo (Declarative programming) nhấn mạnh việc chúng ta làm cái gì.

Có thể lấy ví dụ thực tế như sau, một cặp vợ chồng đến nhà hàng, người bồi bàn muốn hỏi họ ngồi ở đâu, họ sẽ trả lời:

  • Hướng mệnh lệnh (Imperative, như thế nào): tôi thấy bàn kia còn trống và sát cửa sổ, nơi có view nhìn đẹp, nên tôi và chồng tôi sẽ tới đó ngồi.
  • Hướng khai báo (Declarative, cái gì): cho tôi bàn 2 người.

Chúng ta có thể để ý thấy là khi đưa ra phép khai báo, thông thường đã có phép mệnh lệnh ẩn bên trong. Ví dụ như khi gọi bàn 2 người, thì cặp vợ chồng này mặc nhiên cho rằng bồi bàn biết được nên chọn bàn nào và hướng dẫn khách của mình tới bàn đó như thế nào.

Ví dụ SQL và HTML

SELECT * FROM Users WHERE Country='Vietnam';
<article>
    <header>
        <h1>Declarative Programming</h1>
    </header>
</article>

Chúng ta có thể thấy, trong cả 2 ngôn ngữ trên thì chúng ta quan tâm nhiều hơn đến kết quả sẽ cho ra cái gì, chứ không quan tâm đến việc chúng làm thế nào để ra kết quả đó.

Lập trình khai báo vs lập trình chức năng

Lập trình chức năng (Functional programming) có thể coi là một nhánh con của lập trình khai báo, trong lập trình chức năng thì chúng ta cũng đưa ra bài toán cho máy tính giải quyết (cái gì), nhưng đồng thời cũng đưa ra cách giải quyết (như thế naào), chỉ có điều là chúng ta không hề làm bất cứ thứ gì để thay đổi giá trị/trạng thái của các biến.

Chúng ta sẽ làm quen dần với mô hình này trong các phần sau.

Ngôn ngữ tiêu biểu của từng loại

  • Ngôn ngữ mệnh lệnh (Imperative): Fortran, Algol, Pascal, C/C++, Java
  • Ngôn ngữ khai báo (Declarative):
    • Ngôn ngữ Logic: Prolog
    • Ngôn ngữ chức năng (Functional): LISP, APL, ML, FP, Haskell, F#

Haskell – Giới thiệu

[Đọc bài này mất trung bình 3 phút]

Haskell là một ngôn ngữ lập trình chức năng (functional programming) và lười (lazy):))) (chúng ta sẽ tìm hiểu sau) được tạo ra vào năm 1980 bởi một hội đồng các nhà học thuật. Vào thời đó cũng có một số ngôn ngữ lập trình chức năng, tuy nhiên mỗi người một ý không ai giống ai. Vì thế cuối cùng họ tập hợp lại và cùng xây dựng nên một ngôn ngữ lập trình mới với những tính năng tốt nhất của loại lập trình chức năng, và Haskell đã ra đơi từ đó.

Mô hình lập trình chức năng

Tên tiếng Anh là Functional Programming, chúng ta cũng có thể gọi là lập trình hàm, hoặc bạn cũng có thể gọi nó theo cách của bạn.

Không có ai đưa ra định nghĩa chính thức cho thuật ngữ Lập trình chức năng. Tuy nhiên khi nói Haskell là một ngôn ngữ lập trình chức năng, thì chúng ta thường nghĩ đến 2 thứ sau:

  • HÀM LÀ TRÊN HẾT, nghĩa là các hàm cũng có giá trị như các loại biến khác.
  • Lập trình trong Haskell thường xoay quanh việc đánh giá các câu lệnh biểu thức hơn là thực thi các biểu thức đó.

Tham chiếu minh bạch

Tính chất này có thể được giải thích:

  • Tất cả mọi thứ từ biến đến cấu trúc dữ liệu đều không thể thay đổi. Nếu bạn để ý thì lớp String trong Java cũng có đặc tính này.
  • Các biểu thức không có tác dụng phụ, chẳng hạn như thay đổi giá trị của các biến toàn cục hoặc hiển thị output ra màn hình.
  • Một hàm được gọi nhiều lần như nhau và được truyền tham số như nhau thì luôn cho ra kết quả giống nhau.

Nghe qua thì có thể khó hiểu. Luật Leibniz giải thích tính chất này như sau: phép toán bằng gán '=' có thể thay thế tất cả mọi thứ ở bất cứ đâu. Ví dụ, nếu x được định nghĩa bởi phương trình x=42, thì cái tên x và số 42 có thể được dùng ở bất cứ đâu mà vì chúng “tuy 2 mà 1”.

Tính lười – Lazy

Lười ở đây nghĩa là, nếu kết quả của một biểu thức không được dùng ở bất cứ đâu, thì Haskell sẽ không thực thi biểu thức đó. Cũng vì thế mà nó có một số hệ quả (hoặc hậu quả) sau:

  • Việc định nghĩa một cấu trúc điều khiển là vô cùng dễ dàng.
  • Định nghĩa vô số các cấu trúc dữ liệu là một điều khả thi.
  • Việc đánh giá độ phức tạp bộ nhớ và thời gian là vô cùng… phức tạp 🙂

Kiểu dữ liệu tĩnh

Tất cả các biểu thức trong Haskell đều có kiểu, và Haskell KHÔNG kiểm tra kiểu trong quá trình biên dịch. Các đoạn code có lỗi liên quan đến kiểu còn không được Haskell biên dịch, do đó khiến việc chạy chương trình nhanh hơn.

Hàm thứ bậc cao

Hàm thức bậc cao (Higher-Order Function) cũng là một tính chất có trong ngôn ngữ Javascript, cho phép một hàm có thể được dùng làm tham số truyền vào một hàm khác, và cũng có thể được dùng làm giá trị trả về của một hàm nào đó.

Haskell – Lập trình chức năng với Haskell

[Đọc bài này mất trung bình < 1 phút]

Series này giới thiệu về ngôn ngữ lập trình Haskell cùng với các đặc tính của mô hình lập trình chức năng. Series này nhắm tới những bạn đã có kiến thức lập trình với các ngôn ngữ mệnh lệnh phổ biến như C++, C#, Java, Javascript, PHP…

Series này tham khảo giáo trình của khóa học CIS 194 – Đại học Pennsylvania và khóa học 5491V – Đại học Passau.

Mục lục