是否有可能具有一個外部函數調用的函數,其中一些外部函數的參數是CString並返回一個接受String的函數呢?
是否有可能,你問?
<lambdabot> The answer is: Yes! Haskell can do that.
好的。我們得到的好事清理了。
熱身的幾個省繁瑣的手續:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
啊,這不是那麼糟糕,雖然。看,馬,沒有重疊!
這個問題似乎適用於IO函數,因爲所有轉換爲CString如newCString或withCString都是IO。
沒錯。這裏要注意的是,有兩個相互關聯的問題需要我們自己去關注:兩種類型之間的對應關係,允許轉換;以及通過執行轉換引入的任何額外上下文。爲了完全解決這個問題,我們將使兩個部分都清晰並適當地將它們混合。我們還需要注意方差;提升整個函數需要處理協變和逆變位置的類型,所以我們需要雙向轉換。
現在,由於我們想翻譯的功能,該計劃是這樣的:
- 轉換函數的參數,接收到新的類型和一些背景。
- 將上下文延遲到函數的結果上,以獲取我們想要的參數。
- 收起冗餘環境在可能的情況
- 遞歸轉換函數的結果,處理多參數函數
嗯,這聽起來並不太難。首先,明確的背景:
class (Functor f, Cxt t ~ f) => Context (f :: * -> *) t where
type Collapse t :: *
type Cxt t :: * -> *
collapse :: t -> Collapse t
這是說我們有一個背景f
,和某些類型的t
與該上下文。 Cxt
類型函數從t
中提取純文本上下文,Collapse
嘗試在可能的情況下合併上下文。 collapse
函數讓我們使用類型函數的結果。
現在,我們擁有純淨環境,並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... -}
夠簡單。處理上下文的各種組合有點繁瑣,但實例明顯且易於編寫。
我們還需要一種方法來確定給定要轉換的類型的上下文。目前上下文在任何一個方向上都是一樣的,但它肯定是可以想象的,所以我分別對待它們。因此,我們有兩個類型的家庭,用於導入/導出轉換提供新的背景下最外層:
type family ExpCxt int :: * -> *
type family ImpCxt ext :: * -> *
一些示例情況:
type instance ExpCxt() = PureCxt
type instance ImpCxt() = PureCxt
type instance ExpCxt String = IO
type instance ImpCxt CString = IO
接下來,將各個類型。稍後我們會擔心遞歸。時間爲另一種類型的類:
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
這是說兩種ext
和int
具有獨特的敞篷對方。我意識到可能不需要每種類型都只有一個映射,但我不想讓事情進一步複雜化(至少現在不是)。
如前所述,我也推遲處理遞歸轉換;大概他們可以合併,但我覺得這樣會更清楚。非遞歸轉換具有引入相應上下文的簡單且定義明確的映射,而遞歸轉換需要傳播和合並上下文,並處理基本情況中的區分遞歸步驟。
哦,你現在可能已經注意到,在課堂上下文中有趣的滑動波浪形業務。這表明兩種類型必須相同的約束條件;在這種情況下,它將每個類型的函數與相反的類型參數聯繫起來,這就給出了上述的雙向性質。呃,你可能想要一個相當新的GHC。在較老的GHC上,這需要功能依賴性,而且會寫成類似class Convert ext int | ext -> int, int -> ext
的東西。
術語級轉換函數非常簡單 - 在結果中記下類型函數應用程序;應用程序一直是左關聯的,所以這只是應用來自較早類型系列的上下文。還要注意名稱的交叉,因爲導出上下文來自使用本機類型的查找。
因此,我們可以將不需要0類型:
instance Convert CDouble Double where
type Foreign Double = CDouble
type Native CDouble = Double
toForeign = pure . realToFrac
toNative = pure . realToFrac
該做的......以及類型:
instance Convert CString String where
type Foreign String = CString
type Native CString = String
toForeign = newCString
toNative = peekCString
現在,在這件事的心臟罷工,並遞歸翻譯整個函數。毫不奇怪,我已經推出了又一個類型的類。實際上,這兩次,因爲我這次分開導入/導出轉換。
class FFImport ext where
type Import ext :: *
ffImport :: ext -> Import ext
class FFExport int where
type Export int :: *
ffExport :: int -> Export int
這裏沒有什麼有趣的。你現在可能已經注意到了一個共同的模式 - 我們在詞彙和類型層面都進行了大致相同的計算,並且我們一起做了這些,甚至模仿了名稱和表達結構。如果你對涉及真實值的事物進行類型級計算,這很常見,因爲如果GHC不明白你在做什麼,GHC會變得模糊。像這樣排列起來會顯着減少頭痛。
無論如何,對於這些類中的每一個,我們都需要一個實例用於每個可能的基本情況,另一個用於遞歸情況。唉,我們不容易有一個通用的基本案例,由於通常煩人的廢話重疊。它可以使用fundeps和類型相等條件來完成,但是......呃。也許以後。另一種選擇是將轉換函數參數化爲具有所需轉換深度的類型級別的數字,其缺點是自動化程度較低,但也可以從明確獲益中獲益,例如不太可能在多態或模棱兩可的類型。
現在,我將假設每個函數都以IO
中的某些內容結束,因爲IO a
與a -> b
可以區分而不會重疊。
首先,基本情況:
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
這裏的約束斷言使用公知的實例的特定上下文,並且,我們有與轉換一些基本類型。再次注意類型函數Import
和term函數ffImport
共享的並行結構。這裏的實際想法應該非常明顯 - 我們將轉換函數映射到IO
,創建某種嵌套上下文,然後使用Collapse
/collapse
進行清理。
遞歸的情況是類似的,但更復雜的:
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
我們增加了一個FFImport
約束的遞歸調用,並在上下文爭論已經變得更加尷尬,因爲我們不知道它到底是什麼是,只是足以確保我們可以處理它。還要注意這裏的逆變函數,因爲我們將函數轉換爲本地類型,但將參數轉換爲外部類型。除此之外,它仍然非常簡單。
現在,我在這一點上已經省略了一些實例,但其他一切都遵循與上述相同的模式,所以讓我們直接跳到最後並排除商品。有些虛外國功能:
foreign_1 :: (CDouble -> CString -> CString -> IO())
foreign_1 = undefined
foreign_2 :: (CDouble -> SizedArray a -> IO CString)
foreign_2 = undefined
和轉換:
imported1 = ffImport foreign_1
imported2 = ffImport foreign_2
什麼,沒有類型的簽名?它有用嗎?
> :t imported1
imported1 :: Double -> String -> [Char] -> IO()
> :t imported2
imported2 :: Foreign.Storable.Storable a => Double -> AsArray a -> IO [Char]
沒錯,這就是推斷類型。啊,這就是我想看到的。
編輯:誰想要嘗試了這一點,我已經採取了完整的代碼爲示範這裏,洗乾淨了一下,和uploaded it to github。
你能告訴我們你是寫什麼? –
這是一個相當混亂的工作:-)我想如果它的存在對於實際使用來說太痛苦了。你看過'hsc2hs'嗎?它非常強大,並且可以生成您想要作爲預處理步驟的各種簽名。 – sclv
我一直在考慮的一個解決方案是製作一個類似convertNth的函數,它需要一個數字和一個函數,然後轉換到這個位置。我想我有點想知道如何工作,儘管我還沒有嘗試過,所以也許這會帶來一些我沒有想到的困難。好的一面是,我仍然可以使用我的現有函數的非字符串,只需要顯式調出字符串。理想的情況下,我或其他人只會想出如何自動處理字符串。 – ricree