Bài 13 - Mô-đun
- import mô-đun
- Kiểm soát môi trường
- Kiểm soát không gian tên
- Tạo các Mô-đun của riêng chúng ta
- Thư viện mở đầu và tiêu chuẩn
Video bài giảng
Các bài học bằng video và bằng văn bản khác nhau vì định dạng video cho phép giải thích rõ ràng thông qua tái cấu trúc mã và bài viết phù hợp hơn cho các giải thích tuần tự.
Sử dụng cái này cho lợi thế của bạn. Nếu một cái gì đó không có ý nghĩa trong một phương tiện, có thể nó sẽ ở phương tiện khác!
Nói một cách thô thiển, một mô-đun Haskell chỉ là một tập hợp các hàm, kiểu và lớp kiểu liên quan có thể được import và sử dụng trong mã của bạn. Nhưng họ còn hơn thế nữa.
Các mô-đun cho phép chúng ta cấu trúc, sử dụng lại và duy trì mã cũng như môi trường của chúng ta.
Nhưng trước khi tìm hiểu cách tạo các mô-đun của riêng mình, hãy xem cách chúng ta có thể sử dụng các mô-đun được xác định trước.
import mô-đun
Chúng ta sẽ import nhiều mô-đun nhiều lần trong bài học này. Vì vậy, nếu bạn chạy các ô theo trình tự, bạn sẽ gặp lỗi khi không nên. Trong những trường hợp đó, hãy khởi động lại Kernel (Trong menu Kernel ở trên) để loại bỏ tất cả các lần import và chỉ chạy ô bạn đang truy cập, bỏ qua tất cả các ô trước đó.
Giả sử ứng dụng của bạn cần thao tác với tệp và thư mục. Chúng ta có thể sử dụng một mô-đun có tên System.Directory
có nhiều chức năng, hành động và loại liên quan đến thao tác tệp và thư mục.
Để import mô-đun này, chúng ta sử dụng từ khóa import
theo sau là tên của mô-đun:
import System.Directory
Điều này phải được thực hiện trước khi xác định bất kỳ chức năng nào, vì vậy việc import thường được thực hiện ở đầu tệp. Bằng cách thêm dòng mã này, chúng ta có quyền truy cập vào tất cả các chức năng, hành động, loại và kiểu của mô-đun System.Directory
. Bạn có thể kiểm tra mọi thứ đi kèm với mô-đun này tại đây (liên kết).
Một trong những hàm được cung cấp là listDirectory
:
listDirectory :: FilePath -> IO [FilePath]
Nó nhận một đường dẫn thư mục kiểu FilePath
(chỉ là một kiểu đồng nghĩa với Chuỗi) và trả về một hành động IO, khi được thực hiện, trả về một danh sách tất cả các mục import (tệp và thư mục, mọi thứ) bên trong thư mục mà chúng ta đã truyền dưới dạng tham số.
Vì vậy, nếu chúng ta sử dụng nó để kiểm tra những gì bên trong thư mục hiện tại của JupyterNotebook này, chúng ta sẽ nhận được:
import System.Directory
listDirectory "."
["23-State-Monad.ipynb","21-Reader-Monad.ipynb","24-Monadic-functions.ipynb","09-Creating-parameterized-and-recursive-types.ipynb","04- Pattern-matching.ipynb","jupyter-tutuorial.ipynb","ourProgram.o","rise.css","simpleProgram.hs","06-Recursion-and-folds.ipynb","22-Writer- Monad.ipynb","simpleProgram","ourProgram.hi","13-Bits-Bytes.ipynb","10-Creating-Type-Classes.ipynb","15-Learning-on-your-own-and- project.ipynb","16-Semigroup-and-Monoid.ipynb","03-Conditions-and-helper-constructions.ipynb","14-Handling-Errors.ipynb","19-Aeson.ipynb"," hello.hi","02-Functions-Data-Types-and-Signatures.ipynb","ourSourceCode.hi","hello.o","17-Functor.ipynb","11-Basic-IO.ipynb" ,".ipynb_checkpoints","20-Monad.ipynb","12-Pragmas-Modules-and-Cabal.ipynb","25-Monad-Transformers.ipynb","18-Applicative.ipynb","ourSourceCode.o ""01-Giới thiệu về haskell.ipynb","08-Tạo-không tham số hóa-types.ipynb","05-Cải thiện-và-kết hợp-functions.ipynb","07-Giới thiệu-to-Type -Classes.ipynb"]
Như bạn có thể thấy, thư mục hiện tại chứa các tập tin của tất cả các bài học mà chúng ta đã học cho đến nay.
Bây giờ, giả sử chúng ta muốn viết một hàm để tìm các tệp bên trong thư mục hiện tại có chứa một Chuỗi nhất định như một phần tên của chúng. Một cái gì đó như thế này:
import System.Directory
find' :: String -> IO [FilePath]
find' str = do
entry <- listDirectory "."
let found = -- filter entries
return found
Đầu tiên, chúng ta lấy danh sách tất cả các tệp và thư mục bằng cách sử dụng listDirectory, sau đó chúng ta lọc chúng.
Chúng ta có thể dễ dàng tự tạo chức năng lọc bằng một số khớp mẫu và đệ quy. Nhưng trên thực tế, đây có vẻ là một chức năng đủ phổ biến để có sẵn dưới dạng thư viện. Và nó là!!
Ngoài ra còn có một mô-đun có tên Data.List
chứa hàng tá hàm để hoạt động với danh sách.
Một trong số chúng được gọi là isInfixOf
. Nó nhận hai danh sách và trả về True
nếu danh sách đầu tiên được chứa, toàn bộ và nguyên vẹn, ở bất kỳ đâu trong danh sách thứ hai. Chính xác những gì chúng ta cần, vì vậy hãy sử dụng nó:
import System.Directory
import Data.List
find' :: String -> IO [FilePath]
find' str = do
entry <- listDirectory "."
let found = filter (str `isInfixOf`) entry
return found
find' "11"
["11-Basic-IO.ipynb"]
Tuyệt vời! Bởi vì chúng ta có quyền truy cập vào các mô-đun có mã viết sẵn, nên chúng ta không phải tự mình triển khai mọi thứ!
Ok, vậy là chức năng có vẻ tốt, nhưng nó có một cái tên lạ. Tại sao không chỉ gọi nó là tìm
mà không có '
?
Giờ đây, một lần nữa, JupyterNotebook đang thực hiện một số phép thuật để tránh lỗi. Nhưng nếu chúng ta cố đổi tên hàm thành find
mà không có '
và biên dịch mã này như một chương trình Haskell thông thường (mà bạn sẽ có thể thực hiện sau bài học này), thì chúng ta sẽ gặp phải lỗi này:
Ambiguous occurrence ‘find’
It could refer to
either ‘Data.List.find’,
imported from ‘Data.List’ at YourFileName.hs:3:1-16
(and originally defined in ‘Data.Foldable’)
or ‘YourFileName.find’, defined at YourFileName.hs:13:1
Lỗi là rõ ràng. Chúng ta có hai chức năng có cùng tên. Một trong tệp của chúng ta và một xuất hiện khi chúng ta import Data.List
. Vì vậy, trình biên dịch không biết chúng ta đang đề cập đến cái nào.
Có nhiều giải pháp cho việc này.
Kiểm soát môi trường
Tất nhiên, giải pháp đơn giản nhất là thay đổi tên hàm của chúng ta:
import System.Directory
import Data.List
findEntry :: String -> IO [FilePath]
findEntry str = do
entry <- listDirectory "."
let found = filter (str `isInfixOf`) entry
return found
findEntry "11"
["11-Basic-IO.ipynb"]
Nhưng điều này không giải quyết được vấn đề cơ bản là môi trường của chúng ta bị ô nhiễm với hàng chục hàm và loại của cả System.Directory
và Data.List
mà chúng ta không định sử dụng. Mà có thể gây ra tất cả các loại rắc rối.
Một giải pháp tốt hơn là import một chức năng hoặc loại cụ thể thay vì toàn bộ mô-đun. Chúng ta có thể dễ dàng làm điều đó như thế này:
import System.Directory (listDirectory) -- import lsitDirectory from System.Directory
import Data.List (isInfixOf) -- import isInfixOf from Data.List
find :: String -> IO [FilePath]
find str = do
entry <- listDirectory "."
let found = filter (str `isInfixOf`) entry
return found
find "11"
["11-Basic-IO.ipynb"]
Chúng ta thêm các chức năng bên trong dấu ngoặc đơn bên phải. Và nếu chúng ta cần import nhiều hơn một, chúng ta sẽ thêm nó cách nhau bằng dấu phẩy.
Ví dụ: nếu chúng ta muốn sắp xếp các mục import trước khi trả lại chúng, chúng ta có thể import hàm sort
từ Data.List
và sử dụng nó như sau:
import System.Directory (listDirectory)
import Data.List (isInfixOf, sort) -- import isInfixOf and sort from Data.List
find :: String -> IO [FilePath]
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry -- filter and sort
return found
find "Creating"
["08-Creating-non-parameterized-types.ipynb","09-Creating-parameterized-and-recursive-types.ipynb","10-Creating-Type-Classes.ipynb"]
Và chúng ta có thể tiếp tục thêm các chức năng chúng ta cần khi chúng ta cần.
Cuối cùng, nếu chúng ta cần nhiều chức năng của một mô-đun, việc import từng chức năng một có thể rất cồng kềnh. Ngoài ra, chúng ta có thể kết thúc với một danh sách khổng lồ các hàm và kiểu chứa gần như toàn bộ mô-đun.
Đối với những trường hợp đó, bạn có thể sử dụng từ khóa hidden
. Ví dụ: giả sử hàm find
của chúng ta chỉ là một trong nhiều hàm trong tệp của chúng ta. Và chúng ta cần sử dụng rất nhiều hàm do Data.List
cung cấp.
Chúng ta có thể thay đổi quá trình import như thế này:
import System.Directory (listDirectory)
import Data.List hiding (find) -- import everything from Data.List, except `find`
find :: String -> IO [FilePath]
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry
return found
find "Creating"
["08-Creating-non-parameterized-types.ipynb","09-Creating-parameterized-and-recursive-types.ipynb","10-Creating-Type-Classes.ipynb"]
Ở đây, chúng ta đang import mọi thứ từ Data.List
ngoại trừ chức năng find
.
Và, giống như trước đây, bạn có thể ẩn nhiều hàm và loại hơn bằng cách thêm chúng vào bên trong dấu ngoặc đơn được phân tách bằng dấu phẩy.
Và đó là cách bạn có thể kiểm soát môi trường của mình trong khi import mô-đun.
Nhưng còn một trường hợp nữa mà chúng ta chưa có cách giải quyết. Điều gì sẽ xảy ra nếu chúng ta import hai mô-đun có chức năng trùng tên?
Ví dụ:
import System.Directory (listDirectory)
import Data.List hiding (find)
import Data.Map
find :: String -> IO [FilePath]
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry
return found
find "Creating"
-- 👇 More code that uses `filter` from Data.Map
-- ...
<interactive>:4:22: error:
Ambiguous occurrence ‘filter’
It could refer to
either ‘Data.Map.filter’, imported from ‘Data.Map’ (and originally defined in ‘Data.Map.Internal’)
or ‘Prelude.filter’, imported from ‘Prelude’ (and originally defined in ‘GHC.List’)
Giả sử chúng ta import một mô-đun có tên Data.Map
. Mô-đun này có các loại và chức năng cho phép bạn lưu trữ các liên kết giữa các khóa và giá trị duy nhất. Hiện tại, đừng lo lắng về điều đó. Chúng ta sẽ xem xét kỹ mô-đun này trong các bài học sau.
Điều thú vị là cả Data.Map
và Prelude
(một mô-đun mà chúng ta sẽ tìm hiểu ngay sau đây) đều cung cấp một chức năng gọi là filter
. Tương tự như trước đây, chúng ta có hai hàm khác nhau có cùng tên và Haskell không biết chúng ta đang đề cập đến hàm nào.
Trong trường hợp này, chúng ta không muốn ẩn chức năng filter
của Bản đồ vì chúng ta đang sử dụng chức năng này ở một nơi khác trong mã của mình. Và chúng ta không thể thay đổi tên của chúng vì chúng ta đang import cả hai từ các mô-đun khác.
Bây giờ là lúc các không gian tên có ích!
Kiểm soát không gian tên
Không có tất cả các biệt ngữ:
Không gian tên là môi trường được tạo để chứa một nhóm tên.
Chúng ta đã nói về "môi trường của chúng ta" như một điều duy nhất. Một "không gian" chứa tất cả các hàm, kiểu và lớp kiểu của chúng ta được trộn lẫn với nhau. Nhưng nhờ các mô-đun, chúng ta có thể có các môi trường (hoặc không gian tên) khác nhau.
Và dễ dàng như thêm một từ vào phần nhập:
import qualified System.Directory (listDirectory) -- qualified import
System.Directory.listDirectory "." -- This works
listDirectory "." -- This doesn't work now
["23-State-Monad.ipynb","21-Reader-Monad.ipynb","24-Monadic-functions.ipynb","09-Creating-parameterized-and-recursive-types.ipynb","04- Pattern-matching.ipynb","jupyter-tutuorial.ipynb","ourProgram.o","rise.css","simpleProgram.hs","06-Recursion-and-folds.ipynb","22-Writer- Monad.ipynb","simpleProgram","ourProgram.hi","13-Bits-Bytes.ipynb","10-Creating-Type-Classes.ipynb","15-Learning-on-your-own-and- project.ipynb","16-Semigroup-and-Monoid.ipynb","03-Conditions-and-helper-constructions.ipynb","14-Handling-Errors.ipynb","19-Aeson.ipynb"," hello.hi","02-Functions-Data-Types-and-Signatures.ipynb","ourSourceCode.hi","hello.o","17-Functor.ipynb","11-Basic-IO.ipynb" ,".ipynb_checkpoints","20-Monad.ipynb","12-Pragmas-Modules-and-Cabal.ipynb","25-Monad-Transformers.ipynb","18-Applicative.ipynb","ourSourceCode.o ""01-Giới thiệu về haskell.ipynb","08-Tạo-không tham số hóa-types.ipynb","05-Cải thiện-và-kết hợp-functions.ipynb","07-Giới thiệu-to-Type -Classes.ipynb"]
<interactive>:1:1: error:
• Variable not in scope: listDirectory :: String -> t
• Perhaps you meant ‘IHaskellDirectory.listDirectory’ (imported from System.Directory)
Nếu bạn chạy ô trước đó, listDirectory "."
sẽ hoạt động vì môi trường của Jupyter đã import System.Directory
mà không đủ điều kiện cho nó. Nếu bạn muốn lặp lại lỗi này, bạn sẽ phải khởi động lại kernel và chạy ô trên trước.
Bằng cách thêm từ khóa qualified
sau import
, thay vì thêm listDirectory
vào môi trường của mình, chúng ta tạo một từ khóa mới có tên System.Directory
chứa nó.
Bằng cách đó, mỗi lần chúng ta muốn sử dụng listDirectory
, chúng ta phải tìm kiếm nó bên trong không gian tên System.Directory
.
Điều này giải quyết vấn đề trước đây của chúng ta:
import System.Directory (listDirectory)
import Data.List hiding (find)
import qualified Data.Map
find :: String -> IO [FilePath]
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry
return found
find "Creating"
-- 👇 More code that uses `Data.Map.filter` from Data.Map
-- ...
["08-Creating-non-parameterized-types.ipynb","09-Creating-parameterized-and-recursive-types.ipynb","10-Creating-Type-Classes.ipynb"]
Như bạn có thể thấy, chúng ta không còn lỗi nữa. Bây giờ chúng ta đủ điều kiện
import Data.Map
, chúng ta vẫn đang import mọi thứ từ mô-đun đó, bao gồm chức năng filter
. Nhưng tất cả những thứ đó đều nằm trong không gian tên Data.Map
, vì vậy nó không bị trộn lẫn với các hàm/kiểu/lớp trong môi trường chính của chúng ta.
Và, để chỉ ra cách chúng ta sẽ sử dụng mã từ Data.Map
, hãy lấy danh sách các mục được lọc và sắp xếp đó và biến chúng thành bản đồ:
import Data.List hiding (find)
import System.Directory (listDirectory)
import qualified Data.Map
find :: String -> IO (Data.Map.Map Int String)
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry
let foundMap = Data.Map.fromList $ zip ([1 ..] :: [Int]) found -- List to Map
return foundMap
find "Creating"
fromList [(1,"08-Creating-non-parameterized-types.ipynb"),(2,"09-Creating-parameterized-and-recursive-types.ipynb"),(3,"10-Creating-Type-Classes.ipynb")]
Chúng ta chỉ thêm một dòng mã. Như chúng ta đã nói trước đây, bản đồ lưu trữ các liên kết giữa các khóa và giá trị duy nhất. Chúng ta có các giá trị nhưng không có khóa!
Chúng ta sẽ sử dụng hàm zip
để gán một khóa duy nhất cho mỗi giá trị. Như chúng ta đã thấy trong bài tập về nhà của bài học đệ quy, hàm zip
nhận hai danh sách và trả về một danh sách các bộ với các cặp tương ứng.
Chúng ta đang nén một danh sách vô hạn các số được sắp xếp bắt đầu từ một và danh sách các mục được lọc và sắp xếp. Vì vậy, chúng ta sẽ nhận được một danh sách các cặp với một số là phần tử đầu tiên và một mục import là phần tử thứ hai.
Khá thuận tiện là mô-đun Data.Map
cung cấp một hàm gọi là fromList
nhận danh sách các cặp và trả về giá trị kiểu Map
. Trong trường hợp này, giá trị nó trả về thuộc loại Map Int String
vì các khóa là Int
và các giá trị là String
.
Với tính năng cuối cùng này, chúng ta đã giành được quyền kiểm soát hoàn toàn các môi trường của mình. Mặc dù, việc viết Data.Map
ở mọi nơi trở nên lỗi thời khá nhanh. Và nếu chúng ta đủ điều kiện import khẩu với tên dài hơn hoặc đủ điều kiện cho một số mô-đun, mã của chúng ta bắt đầu trở nên lộn xộn và khó đọc hơn, như câu này.
Haskell cho phép chúng ta đổi tên namespace thành một tên thuận tiện hơn. Ví dụ:
import Data.List hiding (find)
import System.Directory (listDirectory)
import qualified Data.Map as Map -- Renamed namespace
find :: String -> IO (Map.Map Int String)
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry
let foundMap = Map.fromList $ zip ([1 ..] :: [Int]) found -- List to Map
return foundMap
find "Creating"
fromList [(1,"08-Creating-non-parameterized-types.ipynb"),(2,"09-Creating-parameterized-and-recursive-types.ipynb"),(3,"10-Creating-Type-Classes.ipynb")]
Lưu ý rằng tên mô-đun được viết hoa, nếu bạn đổi tên chúng, tên mới này cũng phải được viết hoa!
Và mẹo cuối cùng, chúng ta có thể kết hợp tất cả các kỹ thuật khác nhau này. Ví dụ: nếu hai mô-đun làm cùng một việc và không có xung đột về tên, thì chúng ta có thể đặt tên giống nhau cho cả hai không gian tên và xử lý chúng như thể chúng đến từ một mô-đun duy nhất.
Điều này không ứng dụng ngay bây giờ, nhưng có một kết hợp import sẽ ứng dụng. Chức năng find
của chúng ta trông khá tốt. Nhưng có một điều làm tôi khó chịu là Map.Map
. Tôi không quan tâm đến Map.fromList
. Trên thực tế, tôi thích nó hơn! Điều đó cho tôi biết rằng fromList
đến từ mô-đun Data.Map
. Nhưng Map.Map
hơi dư thừa. Tất nhiên, constructor kiểu Map
đến từ mô-đun Data.Map
!
Hãy kết hợp một vài lần import để tránh sự dư thừa này!:
import Data.List hiding (find)
import System.Directory (listDirectory)
import qualified Data.Map as Map hiding (Map) -- import qualified + Rename namespace + hide Map
import Data.Map (Map) -- Import only Map
find :: String -> IO (Map Int String)
find str = do
entry <- listDirectory "."
let found = sort $ filter (str `isInfixOf`) entry
let foundMap = Map.fromList $ zip ([1 ..] :: [Int]) found
return foundMap
find "Creating"
fromList [(1,"08-Creating-non-parameterized-types.ipynb"),(2,"09-Creating-parameterized-and-recursive-types.ipynb"),(3,"10-Creating-Type- Classes.ipynb")]
Bằng cách ẩn constructor kiểu Map
khỏi quá trình import đủ điều kiện và import nó một cách riêng biệt, về cơ bản, chúng ta đã loại bỏ constructor kiểu Map
khỏi không gian tên Map
và thêm nó vào không gian tên chính của chúng ta.
Mọi thứ khác đều giống nhau, nhưng bây giờ, chữ ký của find
dễ đọc hơn.
Đó là gần như mọi thứ về import mô-đun và quản lý môi trường của bạn. Nhưng hãy nhớ rằng, chúng ta đã nói rằng các mô-đun cũng cho phép chúng ta cấu trúc, tái sử dụng và bảo trì mã của mình tốt hơn? Chà, để xem thế nào!
Tạo mô-đun của riêng bạn
Vì các mô-đun chỉ là các tệp Haskell đơn giản có thể được import vào các tệp Haskell khác, nên bạn có thể dễ dàng tự tạo một mô-đun.
Giả sử chúng ta muốn một phiên bản khác của hàm sum
trả về lỗi nếu chúng ta ứng dụng nó cho danh sách trống thay vì giá trị 0 mà sum
trả về.
Để tạo một mô-đun hiển thị một mô-đun như vậy, trước tiên chúng ta tạo một tệp Haskell mà chúng ta gọi là SumNonEmpty.hs
. Ở đầu tệp này, chúng ta viết một câu lệnh mô-đun như
module SumNonEmpty where
Với câu lệnh này, chúng ta đã xác định tên của mô-đun là SumNonEmpty
, tên này lại bắt đầu bằng một chữ cái viết hoa.
Bạn nên đặt tên của mô-đun làm tên của tệp, mặc dù điều này không bắt buộc.
Và bây giờ, chúng ta có thể viết mã mà mô-đun của chúng ta cung cấp:
module SumNonEmpty where
data MyData a b = Error a | Result b deriving (Show)
sumNonEmpty :: Num a => [a] -> MyData String a
sumNonEmpty [] = Error "List is empty"
sumNonEmpty xs = Result (sum xs)
Và đó là khá nhiều nó! Chúng ta đã tạo mô-đun của riêng mình.
Bây giờ chúng ta có thể import nó vào một tệp khác (trong cùng thư mục) giống như bất kỳ mô-đun nào khác:
import SumNonEmpty
sum [] -- 0
sum [1..3] -- 6
sumNonEmpty [] -- Error "List is empty"
sumNonEmpty [1..3] -- Result 6
0 6 Error "List is empty" Result 6
Trong ví dụ trước, mô-đun đã xuất nằm trong cùng thư mục với tệp import mô-đun đó. Nhưng họ có thể ở những nơi khác nhau. Trong trường hợp đó, bản thân quá trình import thể hiện vị trí của mã.
Ví dụ, trong trường hợp này:
import Data.Time.Calendar
import Data.Time.Clock.System
Chúng ta có thể suy ra rằng các lần import dịch sang cấu trúc tệp tiếp theo:
Data
|
|--- Time
|
|--- Calendar.hs
|--- Clock
|
|--- System.hs
Trong đó mô-đun Calendar
nằm trong tệp Data/Time/Calendar.hs
và mô-đun System
nằm trong tệp Data/Time/Clock/System.hs
.
Ngược lại với các hạn chế import khẩu, như chúng ta đã làm cho đến bây giờ, Haskell cũng cung cấp cho bạn quyền kiểm soát xuất khẩu. Đó là, những gì mô-đun tiếp xúc với thế giới bên ngoài.
Trong ví dụ trên, mô-đun của chúng ta xuất tất cả những gì được khai báo trong tệp của nó. Nhưng có rất nhiều trường hợp bạn không muốn điều đó. Ví dụ: khi bạn tạo một hàm trợ giúp chỉ được sử dụng bên trong mô-đun, như sau:
module SumNonEmpty1 where
errorMessage1 = "List is empty"
data MyData1 a b = Error1 a | Result1 b deriving (Show)
sumNonEmpty1 :: Num a => [a] -> MyData1 String a
sumNonEmpty1 [] = Error1 errorMessage1
sumNonEmpty1 xs = Result1 (sum xs)
Trong trường hợp này, bất kỳ ai import SumNonEmpty1
đều có quyền truy cập vào errorMessage
. Nhưng sẽ không hợp lý nếu sử dụng thông báo lỗi này bên ngoài định nghĩa sumNonEmpty
. Vì vậy, không có lý do gì để người tiêu dùng mô-đun có thể truy cập nó!
Giải pháp rất đơn giản: Khởi tạo rõ ràng những gì mô-đun xuất. Như thế này:
module SumNonEmpty2 (sumNonEmpty2, MyData2) where
errorMessage2 = "List is empty"
data MyData2 a b = Error2 a | Result2 b deriving (Show)
sumNonEmpty2 :: Num a => [a] -> MyData2 String a
sumNonEmpty2 [] = Error2 errorMessage2
sumNonEmpty2 xs = Result2 (sum xs)
Lưu ý: Thực tế phổ biến là đặt một bản xuất trên mỗi dòng nếu chúng không vừa trong một dòng.
Bằng cách tuyên bố rõ ràng rằng mô-đun xuất sumNonEmpty, MyData
, mọi thứ khác bên trong mô-đun sẽ không thể truy cập được đối với người dùng cuối (người import mô-đun). Như bạn có thể thấy ở đây:
import SumNonEmpty2
sumNonEmpty2 [3.5, 7]
errorMessage2
Kết quả2 10,5
<interactive>:1:1: error:
• Variable not in scope: errorMessage2
• Perhaps you meant ‘errorMessage1’ (imported from SumNonEmpty1)
Nhưng chúng ta cũng tạo ra một vấn đề bây giờ. Chúng ta đã xuất loại MyData2
nhưng không xuất loại constructor của nó. Điều đó có nghĩa là chúng ta không có cách nào trích xuất kết quả bằng cách khớp mẫu:
import SumNonEmpty2
resOfSum = sumNonEmpty2 [100.150]
resOfSum
hai lầnAsMuch Error2 _ = 0
hai lầnAsMuch Kết quả x = x * 2
Kết quả2 6375
<interactive>:1:13: error:
Not in scope: data constructor ‘Error2’
Perhaps you meant one of these: ‘Error’ (imported from SumNonEmpty), ‘Error1’ (imported from SumNonEmpty1), variable ‘error’ (imported from Prelude)
Điều này thường được giải quyết theo hai cách khác nhau. Bằng cách xuất các constructor để chúng ta có thể sử dụng chúng khi import mô-đun:
-- Alternative one:
module SumNonEmpty2 (sumNonEmpty2, MyData2 (Error2, Result2)) where
-- Alternative two:
module SumNonEmpty2 (sumNonEmpty2, MyData2 (..)) where -- (..) means "all constructors".
Hoặc bằng cách giữ cho các constructor không thể truy cập được nhưng xuất một hàm có thể trích xuất chúng:
module SumNonEmpty3 (sumNonEmpty3, MyData3, getResult) where
errorMessage3 = "List is empty"
data MyData3 a b = Error3 a | Result3 b deriving (Show)
sumNonEmpty3 :: Num a => [a] -> MyData3 String a
sumNonEmpty3 [] = Error3 errorMessage3
sumNonEmpty3 xs = Result3 (sum xs)
getResult :: (Num a) => a -> MyData3 String a -> a
getResult def (Result3 x) = x
getResult def _ = def
import SumNonEmpty3
resOfSum = sumNonEmpty3 [100.150]
resOfSum
getResult 99 resOfSum
getResult 99 (sumNonEmpty3 [])
Result3 6375
6375
99
Bây giờ chúng ta đã biết cách làm việc với các mô-đun, hãy tìm hiểu về mô-đun nổi tiếng nhất.
Mô-đun Mở đầu
Khi làm việc trong GHCi hoặc viết mã Haskell của riêng bạn, một số hàm có sẵn theo mặc định, chẳng hạn như head
, sum
và length
.
Điều này là do các hàm đó là một phần của mô-đun có tên Prelude được import theo mặc định.
Từ khúc dạo đầu có nghĩa là phần giới thiệu về một điều gì đó quan trọng hơn. Đó là mã và mô-đun bạn sẽ viết.
Vì vậy, mô-đun Prelude
cung cấp các chức năng cơ bản, kiểu dữ liệu và lớp kiểu mà nhà phát triển bình thường có thể cần cho mã của mình.
Bạn có thể tìm thấy danh sách tất cả các chức năng, loại và lớp loại do Prelude cung cấp tại đây.
Ngoài Prelude, còn có các thư viện tiêu chuẩn khác không được import theo mặc định nhưng có sẵn để import mà không cần bạn tải xuống riêng.
Thư viện tiêu chuẩn
Nếu bạn truy cập trang web này, bạn có thể thấy tất cả các thư viện được đưa vào Haskell.
Thật khó khăn, tôi biết. Nhưng bạn không cần phải ghi nhớ tất cả những điều này. Có những công cụ như Hoogle cho phép bạn tìm kiếm trong các thư viện bất cứ khi nào bạn cần. Chúng ta sẽ khám phá cách sử dụng công cụ này và các công cụ tiện lợi khác trong bài học sau. Bây giờ, đó là nó cho ngày hôm nay!
Điều này là dành cho hôm nay!
Bài tập về nhà của bài học này sẽ là đọc qua mọi thứ được cung cấp bởi Prelude.
Nhưng! Trong đó, cũng có nhiều nội dung chúng ta chưa đề cập đến và điều đó phụ thuộc vào các khái niệm mà chúng ta sẽ tìm hiểu sâu hơn trong khóa học này. Vì vậy, nó không bắt buộc phải hiểu tất cả mọi thứ. Mục tiêu là để có ý tưởng về những gì mô-đun Prelude cung cấp, sử dụng nó như một phần bổ sung của tất cả các chức năng, kiểu và lớp kiểu mà chúng ta đã sử dụng cho đến nay và giúp bạn quen với việc đọc tài liệu chính thức.
Nguồn bài viết tại đây