Haskell #1.5 Tuple

 

Tuple

Ở mức độ nào đó, tuple cũng giống như List - đó là một cách lưu giữ nhiều giá trị vào một giá trị. Tuy nhiên có một vài điểm khác biệt. List số là một List chỉ chứa số. Đó là type của List này và nó không ảnh hưởng gì nếu chứa có một hay vô hạn số. Còn tuple được dùng khi bạn đã biết chắc chắn có bao nhiêu giá trị cần kết hợp lại với nhau và kiểu của tuple phụ thuộc vào số lượng phần tử (thành phần) trong nó và type của từng phần tử. Tuple được kí hiệu bằng cặp ngoặc tròn () và các component được ngăn cách bởi dấu phẩy.

Một điểm khác biệt cơ bản là tuple có thể là kết hợp nhiều kiểu dữ liệu khác nhau. Hãy hình dung cách ta biểu thị vector hai chiều trong Haskell. Một cách làm là dùng List. Có vẻ như cách này được. Vậy sẽ ra sao nếu ta muốn đưa nhiều vector vào trong một List để biểu diễn các điểm trong không gian phẳng (hai chiều)? Ta có thể viết [[1,2],[8,11],[4,5]]. Vấn đề là với cách này là ta cũng có thể lỡ may viết sai, chẳng hạn [[1,2],[8,11,5],[4,5]], điều này không gây ra lỗi cú pháp vì [8,11,5] vẫn là List số nhưng thực ra chúng đã mất ý nghĩa toán học. Còn một tuple với kích thước bằng hai (cặp) thì chính là kiểu riêng của nó. Vì vậy ta hãy dùng tuple. Thay vì bao quanh vector bởi cặp ngoặc vuông, ta dùng cặp ngoặc tròn: [(1,2),(8,11),(4,5)]. Vậy sẽ ra sao nếu ta thử tạo một hình kiểu như [(1,2),(8,11,5),(4,5)]? Ồ, ta sẽ gặp lỗi này:

* Couldn't match expected type `(a, b)' with actual type `(Integer, Integer, Integer)'.
* In the expression: (8, 11, 5)
    In the expression: [(1, 2), (8, 11, 5), (4, 5)]
    In an equation for `t': t = [(1, 2), (8, 11, 5), (4, 5)]
* Relevant bindings include
    t :: [(a, b)] (bound at <interactive>:18:1)

Nó báo cho ta biết rằng ta đã dùng một tuple kiểu đôi và một tuple kiểu ba trong cùng List, điều này không được phép xảy ra. Bạn cũng không thể tạo List kiểu [(1,2),("One",2)] vì phần tử đầu trong List là một cặp số còn phần tử thứ hai là một cặp gồm string và số. Tuple cũng có thể được dùng để biểu diễn nhiều loại dữ liệu khác nhau. Chẳng hạn, nếu ta muốn biểu diễn tên và tuổi của một người, trong Haskell ta có thể dùng tuple ba phần tử: ("Christopher", "Walken", 55).

Như ở ví dụ này, ta thấy được tuple có thể còn chứa List. Dùng tuple khi bạn đã biết trước có bao nhiêu phần tử mà một đối tượng cần chứa. So với List thì tuple cứng nhắc hơn vì tuple kích thước khác nhau thì khác nhau, vì vậy bạn không thể viết một hàm tổng quát để thêm một phần tử vào một tuple - bạn phải viết một hàm để bổ sung vào một cặp, một hàm khác để bổ sung vào một tuple ba phần tử, một hàm khác nữa để bổ sung vào tuple bốn phần tử, …

Trong khi có List chứa một phần tử, lại không có tuple nào như vậy (thực ra khái niệm đó vô nghĩa). Tuple một phần tử chính là phần tử đó.

Cũng như List, hai tuple có thể so sánh với nhau được nếu các phần tử của chúng có thể so sánh được. Bạn chỉ không thể so sánh được hai tuple khác kích thước, nhưng hai List khác kích thước lại so sánh được. Có hai hàm có ích khi thao tác với cặp: fst nhận vào một cặp và trả về phần tử thứ nhất của nó.

ghci> fst (8,11)
8
ghci> fst ("Wow", False)
"Wow"

snd nhận vào một cặp và trả về phần tử thứ hai của nó. Thật ngạc nhiên!

ghci> snd (8,11)
11
ghci> snd ("Wow", False)
False

Lưu ý: các hàm này chỉ có tác dụng đối với cặp. Ta không dùng được với các tuple ba, tuple tứ, tuple năm, v.v. Sau này ta sẽ đề cập đến cách khác để trích thông tin từ tuple.

image info

Một hàm rất tuyệt để tạo ra một List các cặp: zip. Nó nhận vào hai List rồi kết hợp chúng lại [thử liên tưởng đến hình ảnh phéc-mơ-tuya, cũng vì vậy mà tên hàm này là zip] bằng cách ghép các phần tử có cùng só thứ tự trong hai List thành các cặp. Đây là một hàm thực sự đơn giản nhưng có vô vàn ứng dụng. Nó đặc biệt có ích khi bạn muốn kết hợp hai List theo cách nào đó, hoặc đồng thời duyệt theo hai List. Sau đây là ví dụ sử dụng.

ghci> zip [1,2,3,4,5] [5,5,5,5,5]
[(1,5),(2,5),(3,5),(4,5),(5,5)]
ghci> zip [1 .. 5] ["one", "two", "three", "four", "five"]
[(1,"one"),(2,"two"),(3,"three"),(4,"four"),(5,"five")]

Hàm này ghép đôi các phần tử lại và tạo ra một List mới. Phần tử thứ nhất đi với phần tử thứ nhất, phần tử thứ hai đi với phần tử thứ hai, v.v. Lưu ý rằng vì cặp có thể có kiểu khác nhau, nên zip có thể nhận vào hai List có kiểu khác nhau rồi kết lại. Tuy nhiên, nếu chiều dài của các List này không bằng nhau thì sao?

ghci> zip [5,3,2,6,2,7,2,5,4,6,6] ["im","a","turtle"]
[(5,"im"),(3,"a"),(2,"turtle")]

List dài hơn sẽ được cắt bớt đi để bằng List ngắn. Vì Haskell có tính lazy, nên ta có thể zip List hữu hạn với List vô hạn:

ghci> zip [1..] ["apple", "orange", "cherry", "mango"]
[(1,"apple"),(2,"orange"),(3,"cherry"),(4,"mango")]

Sau đây là một bài toán kết hợp tuple với List comprehension: Tam giác vuông nào có các cạnh là số nguyên và các cạnh đều dài bằng hoặc ngắn hơn 10 đồng thời có chu vi bằng 24? Trước hết, ta hãy thử phát sinh tất cả tam giác với cạnh dài bằng hoặc ngắn hơn 10:

ghci> let triangles = [ (a,b,c) | c <- [1..10], b <- [1..10], a <- [1..10] ]

Ta chỉ việc rút ba số nguyên từ ba List và hàm kết quả làm nhiệm vụ kết hợp chúng thành một tuple có ba phần tử. Nếu bạn đánh giá hàm này bằng cách gõ triangles trong GHCI, bạn sẽ nhận được một List các tam giác với ba cạnh đều không dài quá 10. Tiếp theo, ta sẽ thêm vào một điều kiện để buộc chúng là tam giác vuông. Ta cũng sẽ thay đổi hàm này bằng cách tính đến cạnh b không lớn hơn cạnh huyền còn cạnh a thì không lớn hơn cạnh b.

ghci> let rightTriangles = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2].

Ta gần xong việc rồi. Bây giờ, chỉ cần sửa lại hàm này bằng cách nói rằng ta muốn những tam giác nào có chu vi bằng 24.

ghci> let rightTriangles' = [ (a,b,c) | c <- [1..10], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a+b+c == 24]
ghci> rightTriangles'
[(6,8,10)]

image info

Và đó là đáp số! Trên đây là một dạng thông dụng của lập trình hàm. Bạn đem một tập các giá trị (nghiệm) khởi đầu rồi áp dụng các phép chuyển đổi cho những nghiệm đó rồi lọc đi đến khi nhận được nghiệm đúng.