Cú pháp “Just” trong Haskell có nghĩa là gì?


118

Tôi đã lùng sục trên internet để tìm lời giải thích thực tế về chức năng của từ khóa này. Mọi hướng dẫn Haskell mà tôi đã xem chỉ bắt đầu sử dụng nó một cách ngẫu nhiên và không bao giờ giải thích nó làm gì (và tôi đã xem nhiều).

Đây là một đoạn mã cơ bản từ Real World Haskell sử dụng Just. Tôi hiểu mã làm gì, nhưng tôi không hiểu mục đích hoặc chức năng của Justnó là gì.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Từ những gì tôi đã quan sát, nó liên quan đến Maybeviệc đánh máy, nhưng đó là tất cả những gì tôi đã học được.

Một lời giải thích tốt về những gì Justphương tiện sẽ được đánh giá rất cao.

Câu trả lời:


211

Nó thực sự chỉ là một hàm tạo dữ liệu bình thường được định nghĩa trong Prelude , là thư viện chuẩn được nhập tự động vào mọi mô-đun.

Có thể là gì, về mặt cấu trúc

Định nghĩa trông giống như sau:

data Maybe a = Just a
             | Nothing

Khai báo đó xác định một kiểu, Maybe ađược tham số hóa bởi một biến kiểu a, điều này có nghĩa là bạn có thể sử dụng nó với bất kỳ kiểu nào thay cho a.

Xây dựng và Phá hủy

Kiểu có hai hàm tạo, Just aNothing. Khi một kiểu có nhiều hàm tạo, điều đó có nghĩa là một giá trị của kiểu phải được xây dựng chỉ với một trong các hàm tạo có thể. Đối với loại này, một giá trị được xây dựng thông qua Justhoặc Nothing, không có khả năng nào khác (không lỗi).

Nothingkhông có kiểu tham số, khi nó được sử dụng như một phương thức khởi tạo, nó đặt tên cho một giá trị hằng số là thành viên của kiểu Maybe acho tất cả các kiểu a. Nhưng hàm Justtạo có tham số kiểu, có nghĩa là khi được sử dụng làm hàm tạo, nó hoạt động giống như một hàm từ kiểu asang Maybe a, tức là nó có kiểua -> Maybe a

Vì vậy, các hàm tạo của một kiểu xây dựng một giá trị của kiểu đó; mặt khác của vấn đề là khi nào bạn muốn sử dụng giá trị đó và đó là lúc kết hợp mẫu có tác dụng. Không giống như các hàm, các hàm tạo có thể được sử dụng trong các biểu thức ràng buộc mẫu và đây là cách mà bạn có thể thực hiện phân tích trường hợp các giá trị thuộc về các kiểu có nhiều hơn một hàm tạo.

Để sử dụng một Maybe agiá trị trong một đối sánh mẫu, bạn cần cung cấp một mẫu cho mỗi hàm tạo, như sau:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

Trong biểu thức trường hợp đó, mẫu đầu tiên sẽ khớp nếu giá trị là Nothingvà mẫu thứ hai sẽ khớp nếu giá trị được tạo với Just. Nếu cái thứ hai khớp, nó cũng liên kết tên valvới tham số đã được chuyển cho hàm Justtạo khi giá trị mà bạn đang khớp với được tạo.

Có thể có nghĩa là gì

Có thể bạn đã quen với cách thức hoạt động của nó; thực sự không có bất kỳ phép thuật nào đối với Maybecác giá trị, nó chỉ là một Kiểu dữ liệu đại số Haskell (ADT) bình thường. Nhưng nó được sử dụng khá nhiều vì nó có hiệu quả "nâng" hoặc mở rộng một kiểu, chẳng hạn như Integertừ ví dụ của bạn, sang một ngữ cảnh mới trong đó nó có giá trị phụ ( Nothing) thể hiện sự thiếu giá trị! Các hệ thống kiểu sau đó yêu cầu bạn kiểm tra cho rằng giá trị tăng thêm trước khi nó sẽ cho phép bạn nhận được ở Integerđó có thể có mặt ở đó. Điều này ngăn chặn một số lỗi đáng chú ý.

Nhiều ngôn ngữ ngày nay xử lý loại giá trị "không có giá trị" này thông qua tham chiếu NULL. Tony Hoare, một nhà khoa học máy tính lỗi lạc (ông đã phát minh ra Quicksort và là người đoạt giải Turing), sở hữu điều này là "sai lầm tỷ đô la" của mình . Loại Có thể không phải là cách duy nhất để khắc phục điều này, nhưng nó đã được chứng minh là một cách hiệu quả để làm điều đó.

Có thể là một Functor

Ý tưởng chuyển đổi kiểu này sang kiểu khác sao cho các phép toán trên kiểu cũ cũng có thể được chuyển đổi để hoạt động trên kiểu mới là khái niệm đằng sau lớp kiểu Haskell được gọi Functor, Maybe acó một ví dụ hữu ích.

Functorcung cấp một phương thức được gọi là fmapánh xạ các hàm có phạm vi trên các giá trị từ kiểu cơ sở (chẳng hạn như Integer) đến các hàm có phạm vi trên các giá trị từ kiểu nâng lên (chẳng hạn như Maybe Integer). Một hàm được chuyển đổi với fmapđể hoạt động trên một Maybegiá trị hoạt động như sau:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Vì vậy, nếu bạn có một Maybe Integergiá trị m_xvà một Int -> Inthàm f, bạn có thể fmap f m_xáp dụng hàm ftrực tiếp cho hàm Maybe Integermà không cần lo lắng nếu nó thực sự có giá trị hay không. Trên thực tế, bạn có thể áp dụng toàn bộ chuỗi Integer -> Integerhàm nâng lên cho Maybe Integercác giá trị và chỉ phải lo lắng về việc kiểm tra rõ ràng Nothingmột lần khi bạn hoàn thành.

Có thể là một Đơn nguyên

Tôi không chắc bạn đã quen với khái niệm này như thế Monadnào, nhưng ít nhất bạn đã từng sử dụng IO atrước đây, và kiểu chữ ký IO atrông khá giống với Maybe a. Mặc dù IOđặc biệt ở chỗ nó không để lộ các hàm tạo của nó cho bạn và do đó chỉ có thể được "chạy" bởi hệ thống thời gian chạy Haskell, nó cũng vẫn là một yếu tố Functorbổ sung Monad. Trên thực tế, có một ý nghĩa quan trọng trong đó a Monadchỉ là một loại đặc biệt Functorvới một số tính năng bổ sung, nhưng đây không phải là nơi để đạt được điều đó.

Dù sao, Monads thích IOcác kiểu ánh xạ thành các kiểu mới đại diện cho "các phép tính dẫn đến giá trị" và bạn có thể nâng các hàm thành Monadcác kiểu thông qua một hàm rất fmapgiống được gọi là liftMbiến một hàm thông thường thành một "tính toán dẫn đến giá trị thu được bằng cách đánh giá chức năng."

Bạn có thể đã đoán ra (nếu bạn đã đọc đến đây) Maybecũng là một Monad. Nó đại diện cho "các phép tính không thể trả về giá trị". Cũng giống như với fmapví dụ, điều này cho phép bạn thực hiện toàn bộ các phép tính mà không cần phải kiểm tra lỗi rõ ràng sau mỗi bước. Và trên thực tế, cách Monadcá thể được xây dựng, việc tính toán trên Maybecác giá trị sẽ dừng ngay khi Nothinggặp phải, vì vậy nó giống như một sự hủy bỏ ngay lập tức hoặc một sự trở lại vô giá trị ở giữa một quá trình tính toán.

Bạn có thể đã viết có thể

Như tôi đã nói trước đây, không có gì cố hữu đối với Maybekiểu được đưa vào cú pháp ngôn ngữ hoặc hệ thống thời gian chạy. Nếu Haskell không cung cấp nó theo mặc định, bạn có thể tự cung cấp tất cả các chức năng của nó! Trên thực tế, bạn có thể tự mình viết lại nó, với các tên khác nhau và có cùng chức năng.

Hy vọng rằng bạn đã hiểu về Maybekiểu và các hàm tạo của nó, nhưng nếu vẫn còn điều gì chưa rõ, hãy cho tôi biết!


19
Thật là một câu trả lời xuất sắc! Cần phải đề cập rằng Haskell thường sử dụng Maybenhững ngôn ngữ khác sẽ sử dụng nullhoặc nil(với những thứ khó chịu NullPointerExceptionẩn nấp trong mọi ngóc ngách). Bây giờ các ngôn ngữ khác cũng bắt đầu sử dụng cấu trúc này: Scala as Option, và ngay cả Java 8 cũng sẽ có Optionalkiểu này.
Landei

3
Đây là một lời giải thích tuyệt vời. Nhiều giải thích mà tôi đã đọc ám chỉ ý tưởng rằng Just là một hàm tạo cho kiểu Có thể, nhưng không ai thực sự làm cho nó rõ ràng.
reem

@Landei, cảm ơn vì đề xuất. Tôi đã thực hiện một chỉnh sửa để đề cập đến sự nguy hiểm của các tham chiếu rỗng.
Levi Pearson

@Landei Loại tùy chọn đã có từ ML vào những năm 70, nhiều khả năng Scala đã chọn nó từ đó vì Scala sử dụng quy ước đặt tên của ML, Tùy chọn với một số hàm tạo và không.
stonemetal

@Landei Swift của Apple cũng sử dụng các tùy chọn khá nhiều
Jamin

37

Hầu hết các câu trả lời hiện tại là những giải thích mang tính kỹ thuật cao về cách thức Justvà bạn bè làm việc; Tôi nghĩ tôi có thể thử giải thích nó dùng để làm gì.

Rất nhiều ngôn ngữ có một giá trị như nullvậy có thể được sử dụng thay vì một giá trị thực, ít nhất là đối với một số loại. Điều này đã khiến nhiều người rất phẫn nộ và bị nhiều người coi là một hành động xấu. Tuy nhiên, đôi khi sẽ hữu ích khi có một giá trị như nullbiểu thị sự vắng mặt của một thứ.

Haskell giải quyết vấn đề này bằng cách yêu cầu bạn đánh dấu rõ ràng những nơi bạn có thể có Nothing(phiên bản của a null). Về cơ bản, nếu hàm của bạn thường trả về kiểu Foo, thay vào đó, nó sẽ trả về kiểu Maybe Foo. Nếu bạn muốn chỉ ra rằng không có giá trị, hãy trả lại Nothing. Nếu bạn muốn trả về một giá trị bar, thay vào đó bạn nên trả về Just bar.

Vì vậy, về cơ bản, nếu bạn không thể có Nothing, bạn không cần Just. Nếu bạn có thể có Nothing, bạn cần Just.

Không có gì kỳ diệu về Maybe; nó được xây dựng trên hệ thống kiểu Haskell. Điều đó có nghĩa là bạn có thể sử dụng tất cả các thủ thuật đối sánh mẫu Haskell thông thường với nó.


1
Câu trả lời rất thoải mái bên cạnh những câu trả lời khác, nhưng tôi nghĩ rằng nó vẫn sẽ được hưởng lợi từ một ví dụ mã :)
PascalVKooten

13

Cho một kiểu t, giá trị của Just tlà một giá trị hiện có của kiểu t, trong đó Nothingthể hiện sự không đạt được giá trị hoặc trường hợp có giá trị sẽ là vô nghĩa.

Trong ví dụ của bạn, có số dư âm không có ý nghĩa, và vì vậy nếu điều này xảy ra, nó được thay thế bằng Nothing.

Ví dụ khác, điều này có thể được sử dụng trong phép chia, xác định một hàm phân chia nhận abvà trả về Just a/bnếu bkhác không, và Nothingnếu không. Nó thường được sử dụng như thế này, như một sự thay thế thuận tiện cho các trường hợp ngoại lệ hoặc giống như ví dụ trước đó của bạn, để thay thế các giá trị không có ý nghĩa.


1
Vì vậy, giả sử trong đoạn mã trên tôi đã loại bỏ Just, tại sao điều đó không hoạt động? Bạn phải có Chỉ bất cứ lúc nào bạn muốn có Không có gì? Điều gì xảy ra với một biểu thức trong đó một hàm bên trong biểu thức đó trả về Không có gì?
reem

7
Nếu bạn loại bỏ Just , mã của bạn sẽ không đánh máy được. Lý do Justlà để duy trì các loại thích hợp. Có một kiểu (Thực ra là một đơn nguyên, nhưng dễ nghĩ hơn là chỉ là một kiểu) Maybe t, bao gồm các phần tử của biểu mẫu Just tNothing. Kể từ khi Nothingcó kiểu Maybe t, một biểu thức có thể đánh giá một Nothinghoặc một số giá trị của kiểu tkhông được nhập đúng. Nếu một hàm trả về Nothingtrong một số trường hợp, thì bất kỳ biểu thức nào sử dụng hàm đó phải có một số cách để kiểm tra điều đó ( isJusthoặc một câu lệnh trường hợp), để xử lý tất cả các trường hợp có thể xảy ra.
qaphla

2
Vì vậy, chỉ đơn giản là ở đó để nhất quán trong loại Có thể vì một t thông thường không có trong loại Có thể. Mọi thứ bây giờ rõ ràng hơn nhiều. Cảm ơn bạn!
reem

3
@qaphla: Nhận xét của bạn về việc nó là "một đơn nguyên, thực sự, [...]" là gây hiểu lầm. Maybe t chỉ một loại. Thực tế là có một Monadví dụ cho Maybekhông biến nó thành một cái gì đó không phải là một loại.
Sarah,

2

Một hàm tổng a-> b có thể tìm một giá trị kiểu b với mọi giá trị có thể có của kiểu a.

Trong Haskell không phải tất cả các chức năng đều là tổng thể. Trong trường hợp cụ thể này, hàm lendkhông phải là tổng - nó không được xác định cho trường hợp số dư nhỏ hơn dự trữ (mặc dù, theo ý tôi, sẽ có ý nghĩa hơn nếu không cho phép Số dư mới nhỏ hơn dự trữ - như vậy, bạn có thể vay 101 từ số dư 100).

Các thiết kế khác giải quyết các chức năng không tổng:

  • ném các ngoại lệ khi kiểm tra giá trị đầu vào không phù hợp với phạm vi
  • trả về giá trị đặc biệt (kiểu nguyên thủy): lựa chọn ưa thích là giá trị âm cho các hàm số nguyên có nghĩa là trả về Số tự nhiên (ví dụ: String.indexOf - khi không tìm thấy chuỗi con, chỉ mục trả về thường được thiết kế là số âm)
  • trả về một giá trị đặc biệt (con trỏ): NULL hoặc một số giá trị tương tự
  • âm thầm trả lại mà không làm gì cả: ví dụ: lendcó thể được viết để trả lại số dư cũ, nếu điều kiện cho vay không được đáp ứng
  • trả về một giá trị đặc biệt: Không có gì (hoặc Gói bên trái một số đối tượng mô tả lỗi)

Đây là những giới hạn thiết kế cần thiết trong các ngôn ngữ không thể thực thi toàn bộ các chức năng (ví dụ, Agda có thể, nhưng điều đó dẫn đến các phức tạp khác, như trở nên không hoàn chỉnh).

Vấn đề với việc trả về một giá trị đặc biệt hoặc ném các ngoại lệ là người gọi có thể dễ dàng bỏ qua việc xử lý một khả năng như vậy do nhầm lẫn.

Vấn đề với việc âm thầm loại bỏ lỗi cũng rất rõ ràng - bạn đang hạn chế những gì người gọi có thể làm với chức năng này. Ví dụ, nếu lendsố dư cũ được trả lại, người gọi không có cách nào biết được số dư đã thay đổi hay chưa. Nó có thể là một vấn đề hoặc không, tùy thuộc vào mục đích dự định.

Giải pháp của Haskell buộc trình gọi của một hàm phải xử lý kiểu như Maybe a , hoặc Either error avì kiểu trả về của hàm.

Theo cách lendđịnh nghĩa này, là một hàm không phải lúc nào cũng tính toán số dư mới - đối với một số trường hợp, số dư mới không được xác định. Chúng tôi báo hiệu trường hợp này cho người gọi bằng cách trả về giá trị đặc biệt Không có gì hoặc bằng cách gói số dư mới trong Chỉ. Người gọi bây giờ có quyền tự do lựa chọn: xử lý lỗi không cho vay theo cách đặc biệt, hoặc bỏ qua và sử dụng số dư cũ - chẳng hạn maybe oldBalance id $ lend amount oldBalance.


-1

Hàm if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)phải có cùng kiểu ifTrueifFalse .

Vì vậy, khi chúng ta viết then Nothing, chúng ta phải sử dụng Maybe agõ vàoelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type

1
Tôi chắc rằng bạn muốn nói điều gì đó thực sự sâu sắc ở đây
xem

1
Haskell có kiểu suy luận. Không cần phải nêu rõ loại NothingJust newBalancerõ ràng.
sang phải

Đối với lời giải thích này cho những người chưa biết, các loại rõ ràng làm rõ ý nghĩa của việc sử dụng Just.
philip
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.