Có thể có chức năng nhận cuộc gọi hàm nước ngoài trong đó một số đối số của hàm nước ngoài là CString và trả về một hàm chấp nhận String thay thế không?
Bạn có thể yêu cầu không?
<lambdabot> The answer is: Yes! Haskell can do that.
Ok. Điều tốt chúng tôi đã làm rõ.
Khởi động với một vài thủ tục tẻ nhạt:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
Ah, nó không phải là quá tệ mặc dù. Nhìn kìa, ma, không trùng lặp!
Sự cố có vẻ phù hợp với chức năng IO, vì mọi thứ chuyển đổi thành CStrings chẳng hạn như newCString hoặc withCString là IO.
Phải. Điều cần quan sát ở đây là có hai vấn đề liên quan đến nhau liên quan đến chính chúng ta: Sự tương ứng giữa hai loại, cho phép chuyển đổi; và bất kỳ ngữ cảnh bổ sung nào được giới thiệu bằng cách thực hiện chuyển đổi. Để đối phó với điều này hoàn toàn, chúng tôi sẽ làm cho cả hai phần rõ ràng và trộn chúng xung quanh một cách thích hợp. Chúng tôi cũng cần chú ý đến phương sai; nâng toàn bộ chức năng yêu cầu làm việc với các loại ở cả vị trí biến đổi và đối xứng, vì vậy chúng tôi sẽ cần chuyển đổi theo cả hai hướng.
Bây giờ, cho một hàm chúng ta muốn dịch, kế hoạch đi một cái gì đó như thế này:
- Chuyển đổi tranh luận của chức năng, nhận được một loại mới và một số ngữ cảnh.
- Trì hoãn ngữ cảnh vào kết quả của hàm, để lấy đối số theo cách chúng tôi muốn.
- Collapse bối cảnh dư thừa nếu có thể
- Đệ quy dịch kết quả của chức năng, để đối phó với các chức năng đa luận
Vâng, đó là âm thanh không quá khó khăn. Thứ nhất, bối cảnh rõ ràng:
class (Functor f, Cxt t ~ f) => Context (f :: * -> *) t where
type Collapse t :: *
type Cxt t :: * -> *
collapse :: t -> Collapse t
này nói rằng chúng ta có một bối cảnh f
, và một số loại t
với bối cảnh đó. Hàm loại Cxt
trích xuất bối cảnh thuần túy từ t
và Collapse
cố gắng kết hợp ngữ cảnh nếu có thể. Hàm collapse
cho phép chúng ta sử dụng kết quả của hàm kiểu.
Còn bây giờ, chúng tôi có những bối cảnh tinh khiết, và IO
:
newtype PureCxt a = PureCxt { unwrapPure :: a }
instance Context IO (IO (PureCxt a)) where
type Collapse (IO (PureCxt a)) = IO a
type Cxt (IO (PureCxt a)) = IO
collapse = fmap unwrapPure
{- more instances here... -}
đủ đơn giản. Xử lý các kết hợp khác nhau của ngữ cảnh hơi tẻ nhạt, nhưng các trường hợp rõ ràng và dễ viết.
Chúng tôi cũng sẽ cần một cách để xác định bối cảnh cho loại chuyển đổi. Hiện tại, bối cảnh cũng giống nhau theo một trong hai hướng, nhưng chắc chắn nó có thể hiểu được nếu không, vì vậy tôi đã xử lý chúng một cách riêng biệt. Do đó, chúng tôi có hai gia đình loại, cung cấp bối cảnh ngoài cùng mới cho một chuyển đổi nhập/xuất khẩu:
type family ExpCxt int :: * -> *
type family ImpCxt ext :: * -> *
Một số trường hợp ví dụ:
type instance ExpCxt() = PureCxt
type instance ImpCxt() = PureCxt
type instance ExpCxt String = IO
type instance ImpCxt CString = IO
Tiếp theo, chuyển đổi loại hình cá nhân. Chúng tôi sẽ lo lắng về đệ quy sau này.Thời gian cho một lớp học loại:
class (Foreign int ~ ext, Native ext ~ int) => Convert ext int where
type Foreign int :: *
type Native ext :: *
toForeign :: int -> ExpCxt int ext
toNative :: ext -> ImpCxt ext int
này nói rằng hai loại ext
và int
là duy nhất chuyển đổi cho nhau. Tôi nhận ra rằng có thể không phải lúc nào cũng mong muốn chỉ có một bản đồ cho từng loại, nhưng tôi không cảm thấy muốn làm phức tạp thêm nữa (ít nhất, không phải bây giờ).
Như đã lưu ý, tôi cũng đã xử lý các chuyển đổi đệ quy tại đây; có lẽ chúng có thể được kết hợp, nhưng tôi cảm thấy nó sẽ rõ ràng hơn theo cách này. Chuyển đổi không đệ quy có ánh xạ đơn giản, được xác định rõ ràng giới thiệu ngữ cảnh tương ứng, trong khi chuyển đổi đệ quy cần phải truyền và hợp nhất ngữ cảnh và xử lý phân biệt các bước đệ quy từ trường hợp cơ sở.
Ồ, và bạn có thể đã nhận thấy rằng doanh nghiệp buồn cười vui nhộn đang diễn ra ở đó trong bối cảnh lớp học. Điều đó cho thấy một ràng buộc rằng hai loại phải bằng nhau; trong trường hợp này, nó liên kết từng loại hàm với tham số kiểu đối diện, cung cấp tính chất hai chiều được đề cập ở trên. Er, bạn có thể muốn có một GHC khá gần đây, mặc dù. Trên các GHC cũ hơn, điều này sẽ cần các phụ thuộc chức năng thay thế, và sẽ được viết như một cái gì đó như class Convert ext int | ext -> int, int -> ext
.
Chức năng chuyển đổi cấp cụm từ khá đơn giản - lưu ý ứng dụng chức năng loại trong kết quả của chúng; ứng dụng là kết hợp trái như thường lệ, do đó, đó chỉ là áp dụng ngữ cảnh từ các họ kiểu cũ hơn. Ngoài ra, hãy lưu ý tên chéo trong tên, trong đó xuất hiện ngữ cảnh từ tra cứu bằng cách sử dụng loại gốc tự nhiên.
Vì vậy, chúng ta có thể chuyển đổi các loại mà không cần IO
:
instance Convert CDouble Double where
type Foreign Double = CDouble
type Native CDouble = Double
toForeign = pure . realToFrac
toNative = pure . realToFrac
... cũng như các loại mà làm:
instance Convert CString String where
type Foreign String = CString
type Native CString = String
toForeign = newCString
toNative = peekCString
Bây giờ để tấn công ở trung tâm của vấn đề và dịch toàn bộ các hàm một cách đệ quy. Sẽ không có gì ngạc nhiên khi tôi đã giới thiệu nhưng lớp học khác là. Trên thực tế, hai, khi tôi đã tách các chuyển đổi xuất/nhập lần này.
class FFImport ext where
type Import ext :: *
ffImport :: ext -> Import ext
class FFExport int where
type Export int :: *
ffExport :: int -> Export int
Không có gì thú vị ở đây. Bạn có thể nhận thấy một mô hình phổ biến bây giờ - chúng tôi đang thực hiện số lượng bằng nhau về tính toán ở cả cấp độ và loại, và chúng tôi đang thực hiện chúng song song, ngay cả đến điểm bắt chước tên và cấu trúc biểu thức. Điều này là khá phổ biến nếu bạn đang thực hiện tính toán cấp độ loại cho những thứ liên quan đến giá trị thực, vì GHC bị kén chọn nếu nó không hiểu bạn đang làm gì. Lót những thứ như thế này làm giảm đáng kể đau đầu.
Dù sao, đối với mỗi lớp này, chúng ta cần một cá thể cho mỗi trường hợp cơ bản có thể và một cho trường hợp đệ quy. Than ôi, chúng ta không thể dễ dàng có một trường hợp cơ bản chung, do sự vô nghĩa khó chịu thông thường với chồng chéo. Nó có thể được thực hiện bằng cách sử dụng fundeps và loại điều kiện bình đẳng, nhưng ... ugh. Có lẽ sau này. Một tùy chọn khác sẽ là tham số hóa hàm chuyển đổi theo một loại cấp cho độ sâu chuyển đổi mong muốn, có nhược điểm là ít tự động hơn, nhưng cũng có được lợi ích rõ ràng, chẳng hạn như ít có khả năng vấp ngã đa hình hoặc các loại mơ hồ.
Hiện tại, tôi sẽ giả định rằng mọi chức năng kết thúc bằng một cái gì đó trong IO
, vì IO a
có thể phân biệt với a -> b
mà không trùng lặp.
Thứ nhất, trường hợp cơ sở:
instance (Context IO (IO (ImpCxt a (Native a)))
, Convert a (Native a)
) => FFImport (IO a) where
type Import (IO a) = Collapse (IO (ImpCxt a (Native a)))
ffImport x = collapse $ toNative <$> x
Các trở ngại ở đây khẳng định một bối cảnh cụ thể sử dụng một trường hợp được biết đến, và rằng chúng ta có một số loại hình cơ bản với một chuyển đổi. Một lần nữa, lưu ý cấu trúc song song được chia sẻ theo chức năng loại Import
và hàm số ffImport
. Ý tưởng thực tế ở đây phải khá rõ ràng - chúng tôi ánh xạ hàm chuyển đổi trên IO
, tạo ngữ cảnh lồng nhau của một số loại, sau đó sử dụng Collapse
/collapse
để làm sạch sau đó.
Trường hợp đệ quy là tương tự, nhưng phức tạp hơn:
instance (FFImport b, Convert a (Native a)
, Context (ExpCxt (Native a)) (ExpCxt (Native a) (Import b))
) => FFImport (a -> b) where
type Import (a -> b) = Native a -> Collapse (ExpCxt (Native a) (Import b))
ffImport f x = collapse $ ffImport . f <$> toForeign x
Chúng tôi đã thêm một ràng buộc FFImport
cho cuộc gọi đệ quy, và tranh cãi bối cảnh đã trở nên lúng túng hơn vì chúng ta không biết chính xác những gì nó là, chỉ xác định đủ để đảm bảo chúng ta có thể đối phó với nó. Cũng lưu ý rằng sự tương phản ở đây, trong đó chúng tôi đang chuyển đổi chức năng thành kiểu gốc, nhưng chuyển đổi đối số thành loại nước ngoài. Ngoài ra, nó vẫn còn khá đơn giản.
Bây giờ, tôi đã để lại một số trường hợp tại thời điểm này, nhưng mọi thứ khác theo cùng các mẫu như trên, vì vậy, hãy bỏ qua đến cuối và phạm vi hàng hóa. Một số chức năng nước ngoài tưởng tượng:
foreign_1 :: (CDouble -> CString -> CString -> IO())
foreign_1 = undefined
foreign_2 :: (CDouble -> SizedArray a -> IO CString)
foreign_2 = undefined
Và chuyển đổi:
imported1 = ffImport foreign_1
imported2 = ffImport foreign_2
gì, không có loại chữ ký? Nó có hoạt động không?
> :t imported1
imported1 :: Double -> String -> [Char] -> IO()
> :t imported2
imported2 :: Foreign.Storable.Storable a => Double -> AsArray a -> IO [Char]
Đúng, đó là loại phỏng đoán. Ah, đó là những gì tôi muốn thấy.
Chỉnh sửa: Đối với bất kỳ ai muốn thử điều này, tôi đã lấy mã đầy đủ để trình diễn ở đây, làm sạch nó một chút và uploaded it to github.
Bạn có thể cho chúng ta thấy những gì bạn đã viết? –
Đây là một công việc khá lộn xộn :-) Tôi tưởng tượng câu trả lời nếu nó tồn tại quá đau đớn để sử dụng thực sự. Bạn đã xem 'hsc2hs' chưa? Nó khá mạnh mẽ và có thể tạo ra các loại chữ ký mà bạn muốn làm bước tiền xử lý. – sclv
Một giải pháp mà tôi đã xem xét là tạo ra một thứ như hàm convertNth, nó sẽ lấy một số và một hàm, và thực hiện chuyển đổi sang vị trí đó. Tôi nghĩ rằng tôi sắp xếp như thế nào mà sẽ làm việc, mặc dù tôi đã không thử nó được nêu ra, vì vậy có lẽ nó sẽ trình bày một số khó khăn tôi đã không nghĩ đến. Bên cộng sẽ là tôi vẫn có thể sử dụng chức năng hiện tại của tôi cho không dây và chỉ phải gọi một cách rõ ràng các chuỗi. Lý tưởng nhất, tất nhiên, tôi hoặc ai đó sẽ chỉ ra cách tự động xử lý các chuỗi. – ricree