Trong bài viết này, chúng ta sẽ nói về các khái niệm liên quan tới định kiểu dữ liệu Type
. Những khái niệm mới mà chúng ta sẽ đề cập ở đây không hẳn thuộc về Functional Programming
mà là những khái niệm phổ biến trong mảng lập trình nói chung. Đó là Kiểu Biến Thiên hay Biến Định Kiểu Type Variable
và Lớp Định Kiểu Type Class
.
Type Variable
Khái niệm Type Variable
xuất phát từ môi trường của các ngôn ngữ định kiểu tĩnh static-typing
, khi người ta bắt đầu nghĩ tới khả năng sử dụng một kiểu dữ liệu trừu tượng a
để biểu trưng cho nhiều kiểu dữ liệu khác nhau. Như vậy, khi một biến được định kiểu với kiểu a
sẽ có thể lưu trữ được một giá trị thuộc bất kỳ kiểu dữ liệu nào.
Do Type Variable
đã được đề cập trước đó trong một bài viết của Sub-Series Declarative
nên ở đây mình sẽ chỉ trích dẫn lại phần nội dung đã thực hiện để chúng ta có thể di chuyển nhanh tới khái niệm tiếp theo.
Các ngôn ngữ thuần
Declarative
hầu hết đều được xây dựng với một tinh thần chung – đó là
khả năng định kiểu rất mạnh mẽ và nghiêm ngặtstrong-typing
. Và ở đây chúng ta cóElm
là một trong số đó.
1 2 3 | <span class="token punctuation">(</span><span class="token builtin">round</span> <span class="token number">1.0</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">2.0</span> <span class="token comment">-- Error : Int + Float</span> |
Cụ thể là thông báo lỗi như ví dụ phép tính
+
giữa một giá trịInt
và một giá trịFloat
mà
chúng ta đã nhìn thấy ở trên. Mặc dù trình biên dịchcompiler
củaElm
đã
có đủ thông tin về các giá trị nhận được trước khi thực hiện phép tính, tuy nhiên chỉ đơn giản là
Elm
không hỗ trợ tự động chuyển đổi kiểu ngầm định trong trường hợp này. Và chúng ta sẽ
cần phải thực hiện việc chuyển kiểu dữ liệu trong code của mình –
1 2 3 4 5 6 | <span class="token punctuation">(</span><span class="token builtin">toFloat</span> <span class="token punctuation">(</span><span class="token builtin">round</span> <span class="token number">1.0</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">2.0</span> <span class="token comment">-- 3 : Float</span> <span class="token number">1</span> <span class="token operator">+</span> <span class="token number">2.0</span> <span class="token comment">-- 3 : Float</span> |
Ồ thế nhưng tại sao phép tính
1 + 2.0
lại không có thông báo lỗi ?
Giá trị
1
trả về bởiround
có thông tin định kiểu chính xác làInt
. Còn giá trị1
mà chúng ta viết trực tiếp vào trong tệp code
thì lại chưa được định kiểu cố định, do đó nênElm
sẽ xem là một giá trị thuộc kiểu biến thiênnumber
.
Khái niệm kiểu biến thiên
Type Variable
, có thể hiểu đơn giản là một kiểu dữ liệu bất kỳ mà trình
biên dịchcompiler
không tìm thấy thông tin định kiểu rõ ràng trong code. Và sẽ cố gắng tìm ra
một logic xử lý thành công phù hợp nhất.
Chính xác thì một kiểu biến thiên
a
được hiểu là một kiểuUnion
bao gồm tất cả các kiểu dữ liệu
mà trình biên dịch thu thập được trong code định nghĩa của toàn bộ chương trìnhproject
. Tuy
nhiên,Elm
cũng có tạo ra một vàiType Variable
với khả năng hữu hạn hơn so vớia
. Đó là –
number
– là một giá trị số học; Vì vậy nên có thể làFloat
hoặcInt
.comparable
– là một giá trị có thể so sánh được bằng chương trìnhcompare
; Bao gồmInt
,Float
,Char
,String
, vàList/Tupple
của các kiểu đó.appendable
– là một giá trị có thể thực hiện các thao tác nối ghép nội dung; Vì vậy nên có thể làString
hoặcList
.compappend
– là một giá trị vừa thuộccomparable
và vừa thuộcappendable
.
Như vậy khi trình biên dịch đọc phép tính
1 + 2.0
thì giá trị2.0
đã có đủ thông tin định kiểu
rõ ràng làFloat
do có dấu phẩy thập phân.
; Còn giá trị1
thì chưa có thông tin định kiểu
cụ thể nên sẽ lànumber
. Logic thành công phù hợp nhất làFloat + Float
và chúng ta có
logic được biên dịch là1.0 + 2.0
. Phép tính được thực hiện và không có thông báo lỗi.
Type Class
Khái niệm Type Class
hay còn được gọi ngắn gọn là Class
cũng là một khái niệm phổ biến trong lập trình nói chung và không thuộc về riêng bất kỳ ngôn ngữ hay mô hình lập trình nào. Tuy nhiên cũng giống Type Variable
, khái niệm Class
được hiểu và triển khai trong các mô hình lập trình có phần khác nhau do bối cảnh áp dụng và nền móng tư duy khởi điểm.
Đối với tư duy Lập Trình Hướng Đối Tượng OOP
thì một Class
được sử dụng để biểu trưng cho các giá trị thuộc các kiểu dữ liệu trong cùng dòng kế thừa tính từ chính Class
đó cho tới các Class
mở rộng được định nghĩa sau. Tức là nếu như chúng ta có Class Person
được sử dụng để định kiểu cho một biến someone : Person
thì biến đó sẽ có thể lưu thực thể dữ liệu tạo ra từ chính Class Person
hoặc bất kỳ Class
nào khác ví dụ như Worker
hay Teacher
kế thừa từ Class Person
.
Còn trong tư duy Lập Trình Hàm Functional Programming
thì các Class
lại được sử dụng để nhóm các kiểu dữ liệu khác nhau với logic giống với CSS Class
. Tức là các kiểu dữ liệu được nhóm lại có thể không liên quan tới nhau về mặt logic quản lý dữ liệu của chương trình, mà có cùng giao diện chức năng. Ví dụ đơn cử như trường hợp của comparable
ở phần trước của bài viết có thể được xem là một Class
bởi bất kỳ kiểu dữ liệu nào thuộc comparable
cũng sẽ có thể tham gia vào các phép so sánh.
Như vậy chúng ta cũng có thể hiểu Class
là một Kiểu Tổ Hợp Union Type
biểu trưng cho nhiều kiểu dữ liệu khác nhau, nhưng được sử dụng ở khía cạnh mô tả chức năng tham gia vào một logic xử lý nào đó. Và ở điểm này thì Class
của Functional
lại có thể được hiểu là có chức năng tương đương với Interface
trong OOP
– tức là công cụ được sử dụng để tạo ràng buộc cho các kiểu dữ liệu cụ thể khi tham gia vào Class
sẽ phải triển khai những chức năng mà Class
đó đã khai báo.
Elm
không có cú pháp hỗ trợ khai báo Class
và chúng ta sẽ tham khảo một code ví dụ từ Haskell
– ngôn ngữ được xem là superset
của Elm
. Ở đây chúng ta sẽ có Class YesNo
được khai báo là những kiểu dữ liệu thuộc Class
này sẽ có thể tham gia vào các phép kiểm tra tính tương đương ==
và /=
. Lưu ý trước khi đọc code ví dụ là Haskell
sử dụng ký hiệu ::
để định kiểu thay vì một dấu hai chấm :
như Elm
.
1 2 3 | <span class="token keyword">class</span> <span class="token constant">YesNo</span> <span class="token hvariable">a</span> <span class="token keyword">where</span> <span class="token hvariable">yesno</span> <span class="token operator">::</span> <span class="token hvariable">a</span> <span class="token operator">-></span> <span class="token constant">String</span> |
Định nghĩa này có thể được đọc là chúng ta có class YesNo
được khai báo là bất kỳ kiểu dữ liệu a
nào thuộc class
này cũng đều phải định nghĩa logic chi tiết cho hàm yesno
nhận vào một giá trị thuộc kiểu đó và trả về một chuỗi nhận định là "Yes"
hoặc "No"
. Như vậy sau khi tự định nghĩa xong một kiểu dữ liệu nào đó, chúng ta có thể thực hiện khai báo để kiểu dữ liệu đó thuộc class YesNo
và cung cấp logic xử lý chi tiết cho hàm nhận định yesno
.
1 2 3 4 | <span class="token keyword">instance</span> <span class="token constant">YesNo</span> <span class="token constant">Int</span> <span class="token keyword">where</span> <span class="token hvariable">yesno</span> <span class="token number">0</span> <span class="token operator">=</span> <span class="token string">"No"</span> <span class="token hvariable">yesno</span> <span class="token hvariable">_</span> <span class="token operator">=</span> <span class="token string">"Yes"</span> |
Code ví dụ trên đã tạo ra một thực thể instance
từ class YesNo
để trình biên dịch nạp vào thông tin rằng kiểu Int
được khai báo thuộc class YesNo
và có logic nhận định yesno
là nói "No"
với giá trị 0
và nói "Yes"
với tất cả các giá trị khác 0
.
1 2 3 4 5 6 | > yesno 0 "No" : String > yesno 9 "Yes" : String |
Oh.. như vậy là Type Class
là công cụ để hỗ trợ tạo ra các tên hàm phổ biến dùng chung cho nhiều kiểu dữ liệu khác nhau. Và mặc dù các phiên bản của các hàm này được định nghĩa chi tiết ở các module
riêng nhưng chúng ta sẽ có thể sử dụng cú pháp gọi hàm chung mà không cần tham chiếu từ tên của các module
.
Ví dụ như khi chúng ta tạo ra tên hàm show
, hay image
, hay toString
để chuyển các giá trị bất kỳ có thể có cấu trúc đơn giản hay phức tạp thành chuỗi mô tả. Chúng ta sẽ không cần phải gọi các hàm này theo cú pháp List.show [0, 1]
, Int.show 9
, v.v… mà chỉ đơn giản là show [0, 1]
hay show 9
.
Elm
không hỗ trợ Type Class
ở cấp độ cú pháp của ngôn ngữ, tuy nhiên chúng ta có thể nhìn thấy rằng mục đích của Type Class
là tạo ra giao diện sử dụng chung cho các hàm xuyên suốt các module
. Thêm vào đó là với định hướng tiếp cận người viết code có căn bản JavaScript
nên các module
của Elm
đang sử dụng quy ước đặt tên theo kiểu OOP
để chúng ta sử dụng cú pháp gọi hàm tham chiếu từ tên module
.
Như vậy chúng ta nên thuận theo quy ước sẵn có của Elm
và nghĩ đến một phương thức tạo ràng buộc để trình biên dịch có thể đưa ra thông báo nhắc nhở nếu một module
nào đó khi áp dụng một class
nào đó mà chưa viết code triển khai chi tiết cho các hàm được khai báo trong class
đó. Cụ thể là chúng ta có thể lưu các hàm vào các record
mô phỏng class
.
1 2 3 4 5 | <span class="token keyword">module</span> <span class="token constant">Class</span> <span class="token keyword">exposing</span> <span class="token punctuation">(</span><span class="token constant">YesNo</span><span class="token punctuation">)</span> <span class="token keyword">type</span> <span class="token keyword">alias</span> <span class="token constant">YesNo</span> <span class="token hvariable">a</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token hvariable">yesno</span> <span class="token operator">:</span> <span class="token hvariable">a</span> <span class="token operator">-></span> <span class="token constant">String</span> <span class="token punctuation">}</span> |
Sau đó ở các module
muốn tạo ràng buộc triển khai class
này, chúng ta có thể tạo một hàm instance...
và trả về một record
với định kiểu a
được chỉ định cụ thể và các thuộc tính được gắn với các hàm được định nghĩa trong module
đó.
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">module</span> <span class="token constant">Book</span> <span class="token keyword">exposing</span> <span class="token punctuation">(</span><span class="token constant">Book</span><span class="token punctuation">,</span> <span class="token hvariable">yesno</span><span class="token punctuation">)</span> <span class="token import-statement"><span class="token keyword">import</span> Class <span class="token keyword">exposing</span> </span><span class="token punctuation">(</span><span class="token constant">YesNo</span><span class="token punctuation">)</span> <span class="token keyword">type</span> <span class="token keyword">alias</span> <span class="token constant">Book</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token hvariable">title</span> <span class="token operator">:</span> <span class="token constant">String</span> <span class="token punctuation">,</span> <span class="token hvariable">author</span> <span class="token operator">:</span> <span class="token constant">String</span> <span class="token punctuation">,</span> <span class="token hvariable">rating</span> <span class="token operator">:</span> <span class="token constant">Float</span> <span class="token punctuation">}</span> <span class="token hvariable">instanceYesNo</span> <span class="token operator">:</span> <span class="token constant">Class.YesNo</span> <span class="token constant">Book</span> <span class="token hvariable">instanceYesNo</span> <span class="token operator">=</span> <span class="token constant">Class.YesNo</span> <span class="token hvariable">yesno</span> |
Do các thuộc tính của record ClassYesNo
đều được định nghĩa không sử dụng Maybe
nên các hàm instance...
ở các module
buộc sẽ phải được truyền vào đầy đủ các tham số. Trong trường hợp chúng ta quên chưa viết code triển khai chi tiết cho một hàm nào đó ở một module
bất kỳ có instance...
thì trình biên dịch sẽ đưa ra thông báo lỗi.
1 2 3 4 5 6 7 | <span class="token keyword">module</span> <span class="token constant">Main</span> <span class="token keyword">exposing</span> <span class="token punctuation">(</span><span class="token hvariable">main</span><span class="token punctuation">)</span> <span class="token import-statement"><span class="token keyword">import</span> Book <span class="token keyword">exposing</span> </span><span class="token punctuation">(</span><span class="token operator">..</span><span class="token punctuation">)</span> <span class="token import-statement"><span class="token keyword">import</span> Html <span class="token keyword">exposing</span> </span><span class="token punctuation">(</span><span class="token constant">Html</span><span class="token punctuation">,</span> <span class="token hvariable">text</span><span class="token punctuation">)</span> <span class="token hvariable">main</span> <span class="token operator">:</span> <span class="token constant">Html</span> <span class="token hvariable">message</span> <span class="token hvariable">main</span> <span class="token operator">=</span> <span class="token hvariable">text</span> <span class="token string">"There must be an error message."</span> |
1 2 3 4 | elm reactor Go to http://localhost:8000 to see your project dashboard. |
Hàm instance...
sẽ không cần phải được gọi ở bất kỳ đâu, chúng ta chỉ định nghĩa hàm này để kích hoạt thông báo lỗi nếu code bên trong module
chưa viết định nghĩa cho hàm yesno
khai báo tại ClassYesNo
. Như vậy là chúng ta đã có công cụ tạo ràng buộc triển khai code Type Class
để sử dụng trong Elm
. Trước khi kết thúc bài viết thì chúng ta sẽ thêm định nghĩa yesno
cho module Book
để bỏ thông báo lỗi ở trên.
1 2 3 4 5 6 7 8 | <span class="token comment">-- instance...</span> <span class="token hvariable">yesno</span> <span class="token operator">:</span> <span class="token constant">Book</span> <span class="token operator">-></span> <span class="token constant">String</span> <span class="token hvariable">yesno</span> <span class="token hvariable">abook</span> <span class="token operator">=</span> <span class="token keyword">if</span> <span class="token hvariable">abook</span><span class="token punctuation">.</span><span class="token hvariable">rating</span> <span class="token operator"><</span> <span class="token number">9.0</span> <span class="token keyword">then</span> <span class="token string">"No"</span> <span class="token keyword">else</span> <span class="token string">"Yes"</span> |
Và code sử dụng
Book.yesno
tại Main
.1 2 3 4 5 6 7 8 9 | <span class="token keyword">module</span> <span class="token constant">Main</span> <span class="token keyword">exposing</span> <span class="token punctuation">(</span><span class="token hvariable">main</span><span class="token punctuation">)</span> <span class="token import-statement"><span class="token keyword">import</span> Book <span class="token keyword">exposing</span> </span><span class="token punctuation">(</span><span class="token operator">..</span><span class="token punctuation">)</span> <span class="token import-statement"><span class="token keyword">import</span> Html <span class="token keyword">exposing</span> </span><span class="token punctuation">(</span><span class="token constant">Html</span><span class="token punctuation">,</span> <span class="token hvariable">text</span><span class="token punctuation">)</span> <span class="token hvariable">main</span> <span class="token operator">:</span> <span class="token constant">Html</span> <span class="token hvariable">message</span> <span class="token hvariable">main</span> <span class="token operator">=</span> <span class="token keyword">let</span> <span class="token hvariable">tell</span> <span class="token operator">=</span> <span class="token hvariable">text</span> <span class="token operator"><<</span> <span class="token hvariable">Book.yesno</span> <span class="token keyword">in</span> <span class="token hvariable">tell</span> <span class="token punctuation">(</span><span class="token constant">Book</span> <span class="token string">"Yoga Sutra"</span> <span class="token string">"Patanjali"</span> <span class="token number">9.9</span><span class="token punctuation">)</span> <span class="token comment">-- "Yes"</span> |