"Có Không?" Functional idioms để dùng nullable values với functions như normal values (Phần 1)
Haskell
13
functional
6
White

Justin Le viết ngày 12/07/2015

Người ta nói "nullable values" là cái "billion dollar mistake". Kể từ ALGOL, chúng ta không thể dùng nullable values/references như một value bình thường.

Tôi xin bắt đầu bằng một câu hỏi, bạn có bao nhiêu lần viết cái này rồi?

if (!isNull(x)) {
  ...
}

Chắc là hơn một lần phải không :P Phải chi chúng ta có thể dùng x như thể chúng ta chắc chắn có một value x thiệt. Nhưng hiện tại, chúng ta lúc nào cũng phải sợ x có thể bị null. Làm sao để thoát được nỗi sợ này bây giờ? :O

Giới thiệu Maybe, đến từ Haskell....(và Scala, và Ocaml, với tên gọi khác "Option"...)

Maybe, Maybe not?

Khi tôi lo về nullable values, thường là tôi muốn làm gì đó với value ấy -- vấn đề là, khi interpreter/runtime chạy, nếu chẳng may value đó là null, chương trình của chúng ta sẽ fail ngay.

Vấn đề

Tôi sẽ mô tả vấn đề này bằng Haskell. Giả sử tôi có một function như sau:

f :: a -> b

(đây là một type signature với ý nghĩa là, f là một function nhận một value thuộc type a và trả lại value of type b, giống như b f(a foo); trong C, Java, etc.)

Tiếp đên, tôi có một value:

x :: a

(nghĩa là x là một value thuộc type a)

và tôi muốn apply f lên x như thế này:

f x :: b

(f x là syntax dùng để apply f với x...và kết quả là một value of type b)

Nhưng nếu x là một value null thì chắc chắn f x sẽ fail!

Giải pháp

Haskell làm thế nào để giải quyết vấn đề này? Haskell không bao giờ cho phép value "null"! Bất cứ khi nào bạn có một value x, bạn có thể chắc chắn rằng value x không phải là null. Thay vào đó, khi bạn muốn nói "value này có lẽ là null", bạn có thể dùng "Maybe type":

data Maybe a = Nothing | Just a

Đó là định nghĩa của type "Maybe". Ví dụ, khi bạn có value thuộc type Maybe Bool, bạn có thể có một trong 3 giá trị: Nothing, Just True, và Just False. Hoặc, một value of type Maybe Int thì có thể có nhiều giá trị trong đó --- Nothing, Just 0, Just 1,
Just 2, etc. Khi bạn có một value of type Maybe a, bạn không thể nói cái đó là gì --- Nothing hay Just x mà bạn chỉ có thể biết lúc runtime. Nói một cách khác, Maybe a rất giống như một nullable value (valu có thể là null object) của các ngôn ngữ lập trình khác, nhưng có type khác (IntMaybe Int là 2 type ("class". theo java, etc.) khác nhau).

x không bao giờ là null! Khi có x :: Int thì chắc chắn có một Int! Khi có x :: Maybe Int thì ko chắc nữa! Vậy là chúng ta thành công rồi, đúng không?

Vấn đề nữa

Rất tiếc là chúng ta chưa hẳn thành công. Một điều tốt khi có nullable values là chúng ta có thể dùng function bình thường với
value bình thường. Khi có f :: Int -> Bool và có x :: Int thì bạn có thể apply và có f x :: Bool. Nếu x là một value
implicitly nullable theo một ngôn ngữ khác thì chúng ta có thể dùng f với "x khi có value" và "x khi là null" luôn. Có điều gì khác đâu!

Nhưng Haskell thì sao? Khi có f :: Int -> Bool và có x :: Maybe Int, bạn có thể làm f x không? Tất nhiên là không! f chỉ biết
Int. Ai viết f thì viết f để dùng với Int chứ không biết làm gì với Maybe Int.

Giờ bạn thấy vấn đề chính không? Lúc chúng ta đổi nullable values thành Maybe, chúng ta được lợi về mặt safety nhưng chúng ta đã mất expressiveness. Giờ điều đơn giản lại trở thành phức
tạp...bạn không thể dùng f nữa --- bạn phải viết một function hoàn toàn khác để dùng f với x :: Maybe Int! Quả là một thất bại ghê gớm. Vây là chúng ta phải viết function mới để dùng với Maybe nhỉ?

Function transformers

Tốt nhất là bạn muốn dùng x :: Maybe Int như có x :: Int. Nếu bạn có thể làm như vậy thì bạn sẽ có thể dùng f với x :: Maybe Int.

Chúng ta có thể "think functionally" và đến ý này --- chúng ta phải viết một function để transform (biến đổi) f thành một function
mới, ví dụ như thế này:

onMaybe :: (a -> b) -> (Maybe a -> Maybe b)

onMaybe là một function lấy một function a -> b và cho lại một function Maybe a -> Maybe b.

Khi apply onMaybe với f thì chúng ta có gì? f là một Int -> Bool --- thì ta có:

onMaybe f :: Maybe Int -> Maybe Bool

Thật tuyệt! Với onMaybe, chúng ta có thể đổi f :: Int -> Bool với onMaybe f :: Maybe Int -> Maybe Bool. Mà chúng ta đã có x :: Maybe Int rồi đúng không? Giờ, vì chúng ta có một Maybe Int -> Maybe Bool, chúng ta có thể dùng onMaybe f với x để có Maybe Bool!!

Wow, chúng ta đã lấy lại được expressiveness. Bạn không cần viết function mới nữa đâu. Nếu chúng ta có hàm onMaybe như thế, bạn sẽ luôn có thể dùng functions của "values bình thường" với mấy cái value Maybe.

Chúng ta chỉ cần viết onMaybe trước...

onMaybe :: (a -> b) -> (Maybe a -> Maybe b)
onMaybe f = liftedFunction
  where
    liftedFunction Nothing = Nothing
    liftedFunction (Just x) = Just (f x)
ghci> let double x = x * 2
ghci> double 4
8
ghci> double (Just 4)
TYPE ERROR!
ghci> double Nothing
TYPE ERROR!
ghci> (onMaybe double) (Just 4)
Just 8
ghci> (onMaybe double) Nothing
Nothing

ở đây:

double :: Int -> Int
onMaybe double :: Maybe Int -> Maybe Int

Ý là nếu dùng onMaybe double với Nothing thì sẽ có Nothing luôn, và khi dùng onMaybe double với Just x thì sẽ có Just (double x). Không có gì ngạc nhiên phải không? Lúc nào có Nothing thì sẽ nhận Nothing lại. Lúc nào có Just (lúc có value) thì cũng sẽ nhận lại Just (cái value).

Functor

Giờ vì chúng ta có thể dùng "normal functions" (như f :: Int -> Bool) với "Maybe values" (như x :: Maybe Int), chúng ta có thể nói
dùng Maybe và không bao giờ lo về expressiveness --- khi nào có Int hay Maybe Int cũng có thể dùng f với nó. Việc phải làm chỉ là transform f trước lúc có Maybe Int.

Đến đây, tôi sẽ tiết lộ một điều, những điều trình bày ở trên thực chất là một design pattern gọi là Functor design pattern. Bạn có Maybe Int, nhưng chỉ có f :: Int -> Bool, làm sao để dùng f với Maybe? Khi bạn học Haskell, bạn sẽ thấy nhiều types như thế. Maybe, List, IO --- có nhiều types bạn sẽ muốn dùng với functions bình thường. Có [Int]nhưng chỉ có f :: Int -> Bool ư? Viết function transformer để có lại onList f :: [Int] -> [Bool]!

Trong ngôn ngữ lập trình khác, chúng ta có thể gọi Functor là một design pattern và là một "interface". Type nào là functor sẽ luôn có một function onX (vd., onMaybe). Với Haskell thì chúng ta có function polymorphic fmap --- khi bạn có một functor, bạn có thể dùng overloaded method fmap để map/apply "on" như thế này. fmap của Maybe chính xác là onMaybe của chúng ta.

Khi nào dùng functor design pattern với Maybe chúng ta có thể lợi safety và giữ lại expressiveness. Best of both worlds :)

Như vậy đã đủ chưa?

Functor/onMaybe là đủ để dùng nullable values như normal values chưa?

Với nhiều trường hợp, như thế là đủ. Nhưng khi bạn cứ tiếp tục dùng thì bạn sẽ thấy cũng có những trường hợp như thế vẫn là chưa đủ.

Ví dụ, khi bạn có g :: (a, b) -> cx :: a, y :: b, bạn có thể dùng g với xy: g (x, y) :: c. Nhưng nếu bạn có x :: Maybe ay :: Maybe b thì sao? tốt nhất là bạn có thể dùng g với đó để có được một Maybe c...nhưng với onMaybe và "Functor" thì bạn không thể làm như vậy.

Nếu bạn có h :: a -> Maybe bx :: a thì bạn có thể dùng h x để có Maybe b. Nhưng nếu bạn có x :: Maybe a, bạn không thể dùng h với đó để lấy đc Maybe b. Gần nhất là bạn có thể dùng (onMaybe h) x :: Maybe (Maybe b).

Ví dụ f muốn làm IO: f :: a -> IO b --- nghĩa là f là một function cho bạn một b bằng IO. Chúng ta có thể dùng f đó với x :: Maybe b không?

Có vẻ như đôi khi Functor/onMaybe ko đủ.

Haskell có gì trả lời cho vấn đề này không?

Tất nhiên là có chứ :P Trong các bài viết tiếp theo, tôi sẽ tiếp tục nói về 3 design patterns thường dùng để giải quyết những vấn đề này: Applicative, Monad, và Traversable. 4 cái này là design patterns để abstract over values và functions. Với 3 cái này, chúng ta có thể dùng "special values" với "normal functions".

Functor, Applicative, Monad và Traversable cho phép chúng ta dùng Maybe như là một normal value mà lại không có quá nhiều syntactic overhead (phức tạp về mặt cú pháp), nhờ có higher order functions/"function transformers". Chúng ta có thể có safety và expressiveness!


Cám ơn rất rất nhiều @viethnguyen sửa ngữ pháp của tôi :)

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Justin Le

1 bài viết.
4 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Bài viết liên quan
White
2 0
Đọc hiểu được type signature là một trong các yếu tố quyết định chuyện bạn có học Haskell được hay không. Đa số chúng ta khi mới tìm hiểu thường g...
Huy Trần viết 13 ngày trước
2 0
White
4 2
Trong (Link), tôi có đề cập đến cách bắt đầu với Haskell bằng cách cài đặt Haskell Platform. Sau một thời gian mày mò không biết bao nhiêu thời gia...
viethnguyen viết hơn 2 năm trước
4 2
White
2 0
Mở đầu Sau một thời gian tìm hiểu những khái niệm cơ bản của Haskell (khá mất thời gian), dạo gần đây tôi bắt đầu chuyển sang tìm hiểu những libra...
viethnguyen viết hơn 2 năm trước
2 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
1 bài viết.
4 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!