Skip to main content

Bài 03 - Điều kiện và cấu trúc trợ giúp

TÓM TẮT
  • Biểu thức if-then-else.
  • Guards
  • Biểu thức let
  • BIểu thức where
  • Nên sử dụng let hay where?
  • Những điều cần lưu ý
Video bài giảng
Chúng tôi đang dịch thuyết minh bài giảng sang tiếng Việt

Biểu thức if-then-else

Thông thường trong mã code của bạn, bạn phải đưa ra lựa chọn. Có một số cách để thể hiện điều kiện. Trong Haskell, chúng ta thường sử dụng các biểu thức if-then-else nhất :

if <Condition> 
then <Expesssion1>
else <Expesssion2>

Condition là biểu thức logic mang lại True hoặc FalseExpression1 biểu thức được sử dụng nếu Condition là True và Expression2 biểu thức được sử dụng nếu Condition là False. Hàm checkLocalHost bên dưới kiểm tra xem đối số có phải là localhost hay không và báo cáo cho người dùng.

checkLocalhost :: String -> String
checkLocalhost ip =
-- True or False?
if ip == "127.0.0.1"
-- When the condition is True the answer is
then "It's localhost!"
-- Otherwise the condition is False and the answer is
else "No, it's not localhost."

checkLocalhost "127.0.0.1"

"It's localhost!"

Hàm checkLocalhost được áp dụng cho một đối số loại String và trả về một giá trị khác của loại String. Đối số là một chuỗi ip chứa địa chỉ IP và hàm sẽ kiểm tra xem chuỗi đó có bằng "127.0.0.1". Nếu kiểm tra thành công thì hàm trả về "It's localhost!", nếu không thì trả về "No, it's not localhost."

Mặc dù trong các ngôn ngữ lập trình mệnh lệnh, thì else không bắt buộc, nhưng trong Haskell, nó là như vậy! Đó là bởi vì, trong Haskell, mọi hàm đều phải trả về một giá trị. Vì vậy, chúng ta có nghĩa vụ cung cấp kết quả cùng loại cho cả trường hợp then và else.

Guards

Bây giờ, hãy tưởng tượng rằng chúng ta muốn thực hiện một kiểm tra phức tạp hơn. Giống như kiểm tra xem sinh nhật năm nay có ý nghĩa đặc biệt nào không. Chúng ta có thể sử dụng các câu lệnh if-else lồng nhau như sau:

specialBirthday :: Int -> [Char]
specialBirthday age =
if age == 1
then "First birthday!"
else
if age == 18
then "You're an adult!"
else
if age == 60
then "Finally, I can stop caring about new lingo!"
else "Nothing special"

Hãy thử sử dụng Guards

Bộ Guards hoạt động tương tự như câu lệnh if-else, nhưng bạn có thể có nhiều điều kiện:

func arg
| <Condition1> = <Result1>
| <Condition2> = <Result2>
| <Condition3> = <Result3>
...

Chúng ta sử dụng biểu tượng | để chỉ sự khởi đầu của mỗi Guards.

Lưu ý rằng không có dâu = nào sau đối số func! Đó là một cạm bẫy phổ biến khi viết Guards. Đừng thêm dấu = vào!

Với Guards, chúng ta có thể viết specialBirthday hàm như thế này:

specialBirthday :: Int -> [Char]
specialBirthday age
| age == 1 = "First birthday!"
| age == 18 = "You're an adult!"
| age == 60 = "Finally, I can stop caring about new lingo!"
| True = "Nothing special"

Sử dụng otherwise

Điều cuối cùng True là có một điều kiện nắm bắt tất cả. Một điều kiện luôn được đánh giá là True bởi vì nó theo nghĩa đen là True.

Kiểu thêm ký tự cuối cùng True vào lớp Guards cuối cùng phổ biến đến mức Haskell đi kèm với một biến có tên là otherwise nó bằng True (otherwise = True) để tạo ra một lớp Guards dễ đọc hơn:

specialBirthday :: Int -> [Char]
specialBirthday age
| age == 1 = "First birthday!"
| age == 18 = "You're an adult!"
| age == 60 = "Finally, I can stop caring about new lingo!"
| otherwise = "Nothing special"

specialBirthday 60

"Finally, I can stop caring about new lingo!"

Bây giờ bạn có thể dễ dàng hiểu ý nghĩa của biểu thức này trong nháy mắt!

OK, đó là về đánh giá có điều kiện. Bây giờ hãy xem cách chúng ta có thể nâng cấp trò chơi cú pháp hàm của mình với let và where !

let where

Chúng ta sử dụng let và where để lưu trữ kết quả tính toán trung gian và liên kết các biến cục bộ.

Hãy bắt đầu với let !

Biểu thức let

let có thể liên kết các biểu thức với các biến cục bộ theo cách sau:

func arg =
let <BIND_1>
<BIND_2>
in <EXPR that uses BIND_1 and/or BIND_2>

Các ràng buộc cục bộ có thể truy cập với <BIND_X> trong toàn bộ biểu thức let.

Bây giờ, hãy tạo một hàm lấy hai nhiệt độ—một tính bằng độ C và một tính bằng độ F—và trả về nhiệt độ nóng hơn nhưng tính bằng Kelvin. Đó là khá nhiều chuyển đổi, phải không?

Để chuyển từ độ F sang độ C, trước tiên chúng ta phải trừ 32 rồi nhân với 5/9, như sau:

$tC = (tF - 32) * 5/9$

Để chuyển từ độ C sang độ Kelvin, chúng ta chỉ cần thêm 273,16 như sau:

$tK = tC + 273.16$

Vì vậy, nếu chúng ta muốn tạo một hàm duy nhất thực hiện tất cả những điều đó, chúng ta có thể tạo một cái gì đó như thế này:

hotterInKelvin :: Double -> Double -> Double
hotterInKelvin c f = if c > (f - 32) * 5 / 9 then c + 273.16 else ((f - 32) * 5 / 9) + 273.16

hotterInKelvin 40 100

313.16

Nó đa hoạt động, nhưng đó là cách mà tôi viết khi tôi chưa biết gì về mã hai tuần trước.

Một cách tiếp cận tốt hơn là sử dụng let các liên kết cho các biểu thức trung gian và viết biểu thức kéo mọi thứ lại với nhau tại một in phần:

hotterInKelvin' :: Double -> Double -> Double
hotterInKelvin' c f =
let fToC t = (t - 32) * 5 / 9
cToK t = t + 273.16
fToK t = cToK (fToC t)
in if c > fToC f then cToK c else fToK f

hotterInKelvin' 40 100

313.16

Bây giờ mã của chúng ta dễ đọc hơn và không có tất cả các biểu thức lặp lại đó!

Nhưng xin chờ chút nữa! Chúng ta cũng có thể sử dụng where xây dựng!

BIểu thức where

Chúng ta có thể sử dụng where để liên kết các giá trị với các biến theo cách sau:

func arg = <EXP that uses BIND_1 and/or BIND_2>
where <BIND_1>
<BIND_2>

Vì vậy, hotterInKelvin hàm tương tự như trước đây có thể được thể hiện where như thế này:

Các ràng buộc có thể truy cập ở đâu <BIND_X> trong toàn bộ nội dung hàm.

hotterInKelvin'' :: Double -> Double -> Double
hotterInKelvin'' c f = if c > fToC f then cToK c else fToK f
where
fToC t = (t - 32) * 5 / 9
cToK t = t + 273.16
fToK t = cToK (fToC t)

hotterInKelvin'' 40 100

313.16

Ok, cả hai dường như làm điều tương tự. Vì vậy, tại sao bận tâm có cả hai? Chúng ta không thể chọn sử dụng một trong số chúng sao?

Chà, có rất nhiều trường hợp chúng có thể hoán đổi cho nhau. Trong những trường hợp đó, bạn có thể chọn cái nào bạn thích nhất. Nhưng họ cũng có những hạn chế và điểm mạnh.

Tôi nên sử dụng let hay where?

Các biểu let thức thuận tiện bất cứ khi nào chúng ta muốn tách các biểu thức phức tạp thành các khối xây dựng nhỏ hơn mà bạn kết hợp thành một biểu thức cuối cùng.

Ví dụ, hãy tưởng tượng bạn muốn tính thể tích của một ngôi nhà. Chúng ta có thể đơn giản hóa vấn đề như thế này:

Một ngôi nhà là một khối lập phương với một kim tự tháp trên đỉnh (mái nhà). Vì vậy, để tìm thể tích của nó, chúng ta cần tính thể tích của khối lập phương và thể tích của hình chóp rồi cộng chúng lại với nhau:

houseV side roofH = let cubeV = side ^ 3
pyramidV = (side ^ 2) * roofH / 3
in cubeV + pyramidV

houseV 3 1

30.0

Chúng ta tạo cubeV và các khối pyramidV bên trong khối let, sau đó chúng ta sử dụng chúng bên trong biểu thức in.

Bên cạnh sự rõ ràng của cú pháp, một ưu điểm khác là nếu biểu thức cuối cùng sau này trở nên phức tạp hơn (ví dụ: chúng ta thêm một ống khói vào nhà), chúng ta chỉ cần thêm một ràng buộc khác và sử dụng nó trong biểu thức cuối cùng!:

houseV side roofH = let cubeV = side ^ 3
pyramidV = (side ^ 2) * roofH / 3
chimneyV = (0.5 ^ 2) * roofH
in cubeV + pyramidV + chimneyV

houseV 3 1

30.25

Mặt khác, các biểu thức where thuận tiện hơn bất cứ khi nào chúng ta muốn mở rộng các ràng buộc trên một số phương trình được Guards.

Bởi vì chúng ta không thể truy cập các ràng buộc let trên tất cả các Guards, nhưng có thể làm như vậy với các ràng buộc where! Ví dụ:

analyzeCylinder :: Float -> Float -> String
analyzeCylinder diameter height
| volume < 10 = "The cylinder is a glass."
| volume < 100 = "The cylinder is a bucket."
| volume < 1000 = "The cylinder is a tank."
| otherwise = "What in the world is that huge thing?!"
where
volume = pi * diameter^2 * height / 4

analyzeCylinder 15 6

"What in the world is that huge thing?!"

Như bạn có thể thấy, chúng ta đã xác định liên kết volume bên trong khối where và sau đó chúng ta truy cập nó trên mọi biểu thức được Guards!

Và cuối cùng, sự khác biệt chính giữa hai loại này là các liên kết where là các khai báo được liên kết với một cấu trúc cú pháp xung quanh. Có nghĩa là, chúng chỉ có thể được sử dụng ở những nơi cụ thể (như bên trong thân hàm). Nhưng let giới thiệu một biểu thức, vì vậy nó có thể được sử dụng ở bất cứ nơi nào một biểu thức có thể được sử dụng. Ví dụ:

-- Seconds in a day
24 * (let seconds = 60 in seconds * 60)

-- The volume of a rectangular prism (we can separate expressions by semicolons to have them in the same line)
let s1 = 10; s2 = 20; s3 = 30; in s1*s2*s3

86400

6000

Trong tất cả các trường hợp bạn có thể sử dụng cái này hay cái kia, hãy chọn cái phù hợp với tình huống hoặc phong cách của bạn. Phải mất một số thực hành để chọn cái nào sẽ sử dụng một cách thích hợp và đó cũng là sở thích của người lập trình. Vì vậy, đừng lo lắng quá nhiều về nó.

Những điều cần lưu ý

Các biểu thức được xác định bằng where không thể truy cập bên ngoài thân hàm đó.

fToK t = 273.16 + fToC t
where fToC t = (t - 32) * 5 / 9

fToC 60

<interactive>:1:1: error:
• Variable not in scope: fToC :: t0 -> t
• Perhaps you meant ‘fToK’ (line 1)

Biểu thức được giới thiệu trong một biểu thức let chỉ tồn tại trong biểu thức let đó.

Ví dụ: hàm này lấy tên và họ của bạn và trả về tên viết tắt của bạn:

initials :: String -> String -> String
initials name lastName = if name == "" || lastName == ""
then "How was your name again?"
else let x = head name
y = head lastName
in [x] ++ "." ++ [y] ++ "."

initials "Richard" "Feynman"

"R.F."

Các biểu thức x và y chỉ khả dụng bên trong biểu thức let đó. Nếu bạn cố gắng sử dụng chúng bên trong if hoặc then, thì chúng sẽ nằm ngoài phạm vi và nó sẽ không biên dịch được.

Bản tóm tắt

Trong bài học này, chúng ta đã thảo luận:

  • Câu lệnh if-then-else và tại sao bạn luôn phải xác định trường hợp khác.
  • Cách sử dụng các bộ Guards để tránh các câu lệnh if-else lồng nhau.
  • Cách sử dụng let và where lưu trữ kết quả của các phép tính trung gian, liên kết các biến cục bộ, cho phép mã sạch hơn và tránh lặp lại chính bạn.

Nguồn bài viết tại đây


Picture