Hạn chế đơn hình là gì?
Các hạn chế monomorphism như đã nêu bởi Haskell wiki là:
một quy tắc phản trực giác trong suy luận kiểu Haskell. Nếu bạn quên cung cấp chữ ký kiểu, đôi khi quy tắc này sẽ điền vào các biến kiểu tự do với các kiểu cụ thể bằng cách sử dụng quy tắc "kiểu mặc định".
Điều này có nghĩa là, trong một số trường hợp , nếu kiểu của bạn không rõ ràng (tức là đa hình), trình biên dịch sẽ chọn để khởi tạo kiểu đó thành một thứ không mơ hồ.
Làm thế nào để tôi sửa chữa nó?
Trước hết, bạn luôn có thể cung cấp một cách rõ ràng chữ ký loại và điều này sẽ tránh việc kích hoạt hạn chế:
plus :: Num a => a -> a -> a
plus = (+)
Prelude> plus 1.0 1
2.0
Ngoài ra, nếu bạn đang xác định một hàm, bạn có thể tránh kiểu không có dấu chấm và ví dụ: viết:
plus x y = x + y
Tắt nó đi
Có thể chỉ cần tắt hạn chế để bạn không phải làm gì với mã của mình để sửa nó. Hành vi được kiểm soát bởi hai phần mở rộng:
MonomorphismRestriction
sẽ kích hoạt nó (là mặc định) trong khi
NoMonomorphismRestriction
sẽ tắt nó.
Bạn có thể đặt dòng sau ở đầu tệp của mình:
{-# LANGUAGE NoMonomorphismRestriction #-}
Nếu bạn đang sử dụng GHCi, bạn có thể bật tiện ích mở rộng này bằng :set
lệnh:
Prelude> :set -XNoMonomorphismRestriction
Bạn cũng có thể yêu ghc
cầu bật tiện ích mở rộng từ dòng lệnh:
ghc ... -XNoMonomorphismRestriction
Lưu ý: Bạn thực sự nên thích tùy chọn đầu tiên hơn là chọn tiện ích mở rộng thông qua các tùy chọn dòng lệnh.
Tham khảo trang của GHC để biết giải thích về điều này và các phần mở rộng khác.
Một lời giải thích đầy đủ
Tôi sẽ cố gắng tóm tắt bên dưới mọi thứ bạn cần biết để hiểu giới hạn đơn hình là gì, tại sao nó được giới thiệu và cách nó hoạt động.
Một ví dụ
Hãy định nghĩa tầm thường sau đây:
plus = (+)
bạn nghĩ có thể thay thế mọi lần xuất hiện +
bằng plus
. Đặc biệt vì (+) :: Num a => a -> a -> a
bạn cũng muốn có plus :: Num a => a -> a -> a
.
Không may, không phải trường hợp này. Ví dụ trong chúng tôi thử như sau trong GHCi:
Prelude> let plus = (+)
Prelude> plus 1.0 1
Chúng tôi nhận được kết quả sau:
<interactive>:4:6:
No instance for (Fractional Integer) arising from the literal ‘1.0’
In the first argument of ‘plus’, namely ‘1.0’
In the expression: plus 1.0 1
In an equation for ‘it’: it = plus 1.0 1
Bạn có thể cần đến :set -XMonomorphismRestriction
các phiên bản GHCi mới hơn.
Và trên thực tế, chúng ta có thể thấy rằng kiểu plus
không như chúng ta mong đợi:
Prelude> :t plus
plus :: Integer -> Integer -> Integer
Điều đã xảy ra là trình biên dịch đã thấy rằng plus
có kiểu Num a => a -> a -> a
, một kiểu đa hình. Hơn nữa, nó xảy ra rằng định nghĩa trên nằm dưới các quy tắc mà tôi sẽ giải thích sau và vì vậy anh ấy quyết định tạo kiểu đơn hình bằng cách đặt mặc định cho biến kiểu a
. Mặc định là Integer
như chúng ta có thể thấy.
Lưu ý rằng nếu bạn cố gắng biên dịch mã trên bằng cách sử dụng, ghc
bạn sẽ không gặp bất kỳ lỗi nào. Điều này là do cách ghci
xử lý (và phải xử lý) các định nghĩa tương tác. Về cơ bản, mọi câu lệnh được nhập vào ghci
phải được đánh dấu hoàn toàn trước khi xem xét phần sau; nói cách khác, nó như thể mọi câu lệnh nằm trong một mô-đun riêng biệt
. Sau này tôi sẽ giải thích lý do tại sao vấn đề này.
Một số ví dụ khác
Hãy xem xét các định nghĩa sau:
f1 x = show x
f2 = \x -> show x
f3 :: (Show a) => a -> String
f3 = \x -> show x
f4 = show
f5 :: (Show a) => a -> String
f5 = show
Chúng tôi mong muốn tất cả các chức năng này để hành xử theo cách tương tự và có cùng loại, tức là các loại show
: Show a => a -> String
.
Tuy nhiên, khi biên dịch các định nghĩa trên, chúng tôi gặp các lỗi sau:
test.hs:3:12:
No instance for (Show a1) arising from a use of ‘show’
The type variable ‘a1’ is ambiguous
Relevant bindings include
x :: a1 (bound at blah.hs:3:7)
f2 :: a1 -> String (bound at blah.hs:3:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show x
In the expression: \ x -> show x
In an equation for ‘f2’: f2 = \ x -> show x
test.hs:8:6:
No instance for (Show a0) arising from a use of ‘show’
The type variable ‘a0’ is ambiguous
Relevant bindings include f4 :: a0 -> String (bound at blah.hs:8:1)
Note: there are several potential instances:
instance Show Double -- Defined in ‘GHC.Float’
instance Show Float -- Defined in ‘GHC.Float’
instance (Integral a, Show a) => Show (GHC.Real.Ratio a)
-- Defined in ‘GHC.Real’
...plus 24 others
In the expression: show
In an equation for ‘f4’: f4 = show
Vì vậy f2
và f4
không biên dịch. Hơn nữa, khi cố gắng xác định các hàm này trong GHCi, chúng tôi không gặp lỗi , nhưng loại cho f2
và f4
là () -> String
!
Monomorphism hạn chế là những gì làm f2
và f4
đòi hỏi một loại monomorphic, và bewteen hành vi khác nhau ghc
và ghci
là do khác nhau
quy tắc mặc định .
Khi nào nó xảy ra?
Trong Haskell, như được định nghĩa bởi báo cáo , có hai loại ràng buộc riêng biệt . Ràng buộc hàm và ràng buộc mẫu. Một liên kết hàm không là gì khác hơn là một định nghĩa của một hàm:
f x = x + 1
Lưu ý rằng cú pháp của chúng là:
<identifier> arg1 arg2 ... argn = expr
Modulo bảo vệ và where
tuyên bố. Nhưng chúng không thực sự quan trọng.
trong đó phải có ít nhất một đối số .
Một liên kết mẫu là một khai báo của biểu mẫu:
<pattern> = expr
Một lần nữa, những người bảo vệ modulo.
Lưu ý rằng các biến là các mẫu , vì vậy ràng buộc:
plus = (+)
là một ràng buộc mẫu . Nó liên kết mẫu plus
(một biến) với biểu thức (+)
.
Khi ràng buộc mẫu chỉ bao gồm một tên biến, nó được gọi là
ràng buộc mẫu đơn giản .
Hạn chế đơn hình áp dụng cho các liên kết mẫu đơn giản!
Vâng, chính thức chúng ta nên nói rằng:
Nhóm khai báo là một tập hợp tối thiểu các ràng buộc phụ thuộc lẫn nhau.
Mục 4.5.1 của báo cáo .
Và sau đó (Phần 4.5.5 của báo cáo ):
một nhóm khai báo nhất định không bị hạn chế nếu và chỉ khi:
mọi biến trong nhóm được ràng buộc bởi một ràng buộc hàm (ví dụ f x = x
) hoặc một ràng buộc mẫu đơn giản (ví dụ: plus = (+)
Phần 4.4.3.2), và
một chữ ký kiểu rõ ràng được đưa ra cho mọi biến trong nhóm được ràng buộc bởi ràng buộc mẫu đơn giản. (vd plus :: Num a => a -> a -> a; plus = (+)
).
Ví dụ do tôi thêm vào.
Vì vậy, nhóm khai báo hạn chế là một nhóm có
các ràng buộc mẫu không đơn giản (ví dụ (x:xs) = f something
hoặc (f, g) = ((+), (-))
) hoặc có một số ràng buộc mẫu đơn giản không có chữ ký kiểu (như trong plus = (+)
).
Hạn chế đơn hình ảnh hưởng đến các nhóm khai báo bị hạn chế .
Hầu hết thời gian bạn không xác định các hàm đệ quy lẫn nhau và do đó một nhóm khai báo chỉ trở thành một ràng buộc.
Nó làm gì?
Hạn chế về tính đơn hình được mô tả bởi hai quy tắc trong Phần 4.5.5 của báo cáo .
Quy tắc đầu tiên
Hạn chế thông thường của Hindley-Milner đối với tính đa hình là chỉ những biến kiểu không xảy ra tự do trong môi trường mới có thể được tổng quát hóa. Ngoài ra, các biến kiểu hạn chế của một nhóm khai báo hạn chế có thể không được tổng quát hóa trong bước tổng quát hóa cho nhóm đó.
(Nhớ lại rằng biến kiểu bị ràng buộc nếu nó phải thuộc một lớp kiểu nào đó; xem Phần 4.5.2.)
Phần được đánh dấu là những gì giới hạn tính đơn hình giới thiệu. Nó nói rằng nếu kiểu là đa hình (tức là nó chứa một số biến kiểu)
và biến kiểu đó bị ràng buộc (nghĩa là nó có ràng buộc về lớp: ví dụ kiểu Num a => a -> a -> a
là đa hình vì nó chứa a
và cũng tương phản vì a
có ràng buộc Num
đối với nó .)
thì không thể khái quát được.
Nói một cách đơn giản, không khái quát hóa có nghĩa là việc sử dụng hàm plus
có thể thay đổi kiểu của nó.
Nếu bạn có các định nghĩa:
plus = (+)
x :: Integer
x = plus 1 2
y :: Double
y = plus 1.0 2
thì bạn sẽ gặp lỗi loại. Bởi vì khi trình biên dịch thấy điều đó plus
được gọi qua một Integer
trong khai báo của x
nó sẽ thống nhất biến kiểu a
với Integer
và do đó kiểu plus
trở thành:
Integer -> Integer -> Integer
nhưng sau đó, khi nó gõ, hãy kiểm tra định nghĩa của y
, nó sẽ thấy định nghĩa đó plus
được áp dụng cho một Double
đối số và các kiểu không khớp.
Lưu ý rằng bạn vẫn có thể sử dụng plus
mà không gặp lỗi:
plus = (+)
x = plus 1.0 2
Trong trường hợp này, kiểu plus
đầu tiên được suy ra là Num a => a -> a -> a
nhưng sau đó việc sử dụng nó trong định nghĩa x
, nơi 1.0
yêu cầu một Fractional
ràng buộc, sẽ thay đổi nó thành Fractional a => a -> a -> a
.
Cơ sở lý luận
Báo cáo cho biết:
Quy tắc 1 được yêu cầu vì hai lý do, cả hai đều khá tinh tế.
Quy tắc 1 ngăn các phép tính lặp lại bất ngờ. Ví dụ: genericLength
là một hàm chuẩn (trong thư viện Data.List
) có kiểu được cung cấp bởi
genericLength :: Num a => [b] -> a
Bây giờ hãy xem xét biểu thức sau:
let len = genericLength xs
in (len, len)
Có vẻ như len
chỉ nên được tính một lần, nhưng nếu không có Quy tắc 1, nó có thể được tính hai lần, một lần ở mỗi trong số hai lần nạp chồng khác nhau.
Nếu lập trình viên thực sự muốn tính toán được lặp lại, một chữ ký kiểu rõ ràng có thể được thêm vào:
let len :: Num a => a
len = genericLength xs
in (len, len)
Về điểm này, ví dụ từ wiki , tôi tin là rõ ràng hơn. Xem xét chức năng:
f xs = (len, len)
where
len = genericLength xs
Nếu len
là đa hình, kiểu của f
sẽ là:
f :: Num a, Num b => [c] -> (a, b)
Vì vậy, hai phần tử của tuple (len, len)
thực sự có thể là
các giá trị khác nhau ! Nhưng điều này có nghĩa là tính toán được thực hiện bởi genericLength
phải được lặp lại để thu được hai giá trị khác nhau.
Cơ sở lý luận ở đây là: mã chứa một lệnh gọi hàm, nhưng không giới thiệu quy tắc này có thể tạo ra hai lệnh gọi hàm ẩn, điều này phản trực quan.
Với hạn chế đơn hình, kiểu f
trở thành:
f :: Num a => [b] -> (a, a)
Theo cách này, không cần phải thực hiện tính toán nhiều lần.
Quy tắc 1 ngăn chặn sự mơ hồ. Ví dụ, hãy xem xét nhóm khai báo
[(n, s)] = đọc t
Nhớ lại rằng đó reads
là một hàm tiêu chuẩn có kiểu được cung cấp bởi chữ ký
lần đọc :: (Đọc a) => Chuỗi -> [(a, Chuỗi)]
Nếu không có Quy tắc 1, n
sẽ được chỉ định kiểu ∀ a. Read a ⇒ a
và s
kiểu ∀ a. Read a ⇒ String
. Loại thứ hai là một loại không hợp lệ, bởi vì nó vốn không rõ ràng. Không thể xác định sử dụng quá tải ở mức nào s
, cũng như không thể giải quyết điều này bằng cách thêm chữ ký kiểu cho s
. Do đó, khi các ràng buộc mẫu không đơn giản được sử dụng (Phần 4.4.3.2), các kiểu được suy ra luôn là đơn hình trong các biến kiểu ràng buộc của chúng, bất kể có cung cấp chữ ký kiểu hay không. Trong trường hợp này, cả hai n
và s
đều là đơn hình trong a
.
Vâng, tôi tin rằng ví dụ này là tự giải thích. Có những tình huống khi không áp dụng quy tắc dẫn đến sự mơ hồ về kiểu.
Nếu bạn tắt tiện ích mở rộng như đề xuất ở trên, bạn sẽ gặp lỗi loại khi cố gắng biên dịch khai báo ở trên. Tuy nhiên, đây thực sự không phải là vấn đề: bạn đã biết rằng khi sử dụng read
bạn phải bằng cách nào đó cho trình biên dịch biết loại mà nó sẽ cố gắng phân tích cú pháp ...
Quy tắc thứ hai
- Bất kỳ biến kiểu đơn hình nào vẫn còn khi suy luận kiểu cho toàn bộ mô-đun hoàn tất, được coi là không rõ ràng và được giải quyết thành các kiểu cụ thể bằng cách sử dụng các quy tắc mặc định (Phần 4.3.4).
Điều này có nghĩa rằng. Nếu bạn có định nghĩa thông thường của mình:
plus = (+)
Điều này sẽ có một loại Num a => a -> a -> a
mà a
là một
monomorphic kiểu biến do để cai trị 1 mô tả ở trên. Khi toàn bộ mô-đun được suy ra, trình biên dịch sẽ chỉ cần chọn một kiểu thay thế kiểu đó a
theo các quy tắc mặc định.
Kết quả cuối cùng là: plus :: Integer -> Integer -> Integer
.
Lưu ý rằng điều này được thực hiện sau khi toàn bộ mô-đun được suy ra.
Điều này có nghĩa là nếu bạn có các khai báo sau:
plus = (+)
x = plus 1.0 2.0
bên trong một mô-đun, trước khi nhập, mặc định loại plus
sẽ là:
Fractional a => a -> a -> a
(xem quy tắc 1 để biết lý do tại sao điều này xảy ra). Tại thời điểm này, tuân theo các quy tắc mặc định, a
sẽ được thay thế bằng Double
và vì vậy chúng ta sẽ có plus :: Double -> Double -> Double
và x :: Double
.
Mặc định
Như đã nêu trước khi tồn tại một số quy tắc mặc định, được mô tả trong Phần 4.3.4 của Báo cáo , mà người giả mạo có thể áp dụng và điều đó sẽ thay thế một kiểu đa hình bằng một kiểu đơn hình. Điều này xảy ra bất cứ khi nào một kiểu không rõ ràng .
Ví dụ trong biểu thức:
let x = read "<something>" in show x
ở đây biểu thức không rõ ràng vì các loại cho show
và read
là:
show :: Show a => a -> String
read :: Read a => String -> a
Vì vậy, x
loại có Read a => a
. Nhưng hạn chế này được thỏa mãn bởi rất nhiều loại:
Int
, Double
hoặc ()
ví dụ. Chọn cái nào? Không có gì có thể cho chúng tôi biết.
Trong trường hợp này, chúng ta có thể giải quyết sự không rõ ràng bằng cách cho trình biên dịch biết kiểu nào chúng ta muốn, thêm một chữ ký kiểu:
let x = read "<something>" :: Int in show x
Bây giờ vấn đề là: vì Haskell sử dụng Num
lớp kiểu để xử lý số, nên có rất nhiều trường hợp biểu thức số chứa sự mơ hồ.
Xem xét:
show 1
Kết quả phải là gì?
Như trước đây 1
có loại Num a => a
và có nhiều loại số có thể được sử dụng. Chọn cái nào?
Việc gặp lỗi trình biên dịch hầu như mỗi khi chúng ta sử dụng một số không phải là điều tốt, và do đó các quy tắc mặc định đã được đưa ra. Các quy tắc có thể được kiểm soát bằng cách sử dụng một default
khai báo. Bằng cách chỉ định, default (T1, T2, T3)
chúng ta có thể thay đổi cách kẻ giả mạo mặc định các loại khác nhau.
Một biến kiểu không rõ ràng v
có thể mặc định được nếu:
v
chỉ xuất hiện trong contraints của các loại C v
đã C
là một lớp (tức là nếu nó xuất hiện như trong: Monad (m v)
sau đó nó là không defaultable).
- ít nhất một trong những lớp này là
Num
hoặc một lớp con của Num
.
- tất cả các lớp này được định nghĩa trong Prelude hoặc một thư viện chuẩn.
Một biến kiểu có thể mặc định được thay thế bằng kiểu đầu tiên trong default
danh sách là một thể hiện của tất cả các lớp của biến không rõ ràng.
default
Khai báo mặc định là default (Integer, Double)
.
Ví dụ:
plus = (+)
minus = (-)
x = plus 1.0 1
y = minus 2 1
Các loại được suy ra sẽ là:
plus :: Fractional a => a -> a -> a
minus :: Num a => a -> a -> a
mà theo các quy tắc mặc định, trở thành:
plus :: Double -> Double -> Double
minus :: Integer -> Integer -> Integer
Lưu ý rằng điều này giải thích tại sao trong ví dụ trong câu hỏi chỉ có sort
định nghĩa nêu ra lỗi. Loại Ord a => [a] -> [a]
không thể được đặt mặc định vì Ord
không phải là một lớp số.
Mặc định mở rộng
Lưu ý rằng GHCi đi kèm với các quy tắc mặc định mở rộng (hoặc ở đây là GHC8 ), có thể được bật trong tệp cũng như sử dụng các ExtendedDefaultRules
phần mở rộng.
Các biến kiểu defaultable không cần phải chỉ xuất hiện trong contraints nơi mà tất cả các lớp học là tiêu chuẩn và phải có ít nhất một lớp đó là một trong những
Eq
, Ord
, Show
hay Num
và lớp con của nó.
Hơn nữa default
khai báo mặc định là default ((), Integer, Double)
.
Điều này có thể tạo ra kết quả kỳ lạ. Lấy ví dụ từ câu hỏi:
Prelude> :set -XMonomorphismRestriction
Prelude> import Data.List(sortBy)
Prelude Data.List> let sort = sortBy compare
Prelude Data.List> :t sort
sort :: [()] -> [()]
trong ghci, chúng tôi không gặp lỗi kiểu nhưng các Ord a
ràng buộc dẫn đến một lỗi mặc định ()
là vô dụng.
Liên kết hữu ích
Có rất nhiều tài nguyên và thảo luận về hạn chế đơn hình.
Dưới đây là một số liên kết mà tôi thấy hữu ích và có thể giúp bạn hiểu hoặc đi sâu hơn vào chủ đề: