是否有可能有一个函数接受外部函数调用,其中一些外部函数的参数是 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
这表示我们有一个 context f
,以及具有该上下文的某种类型t
。type 函数从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
.
术语级转换函数非常简单——注意其结果中的类型函数应用;application 和往常一样是左关联的,所以这只是应用早期类型系列的上下文。还要注意名称中的交叉,因为导出上下文来自使用本机类型的查找。
所以,我们可以转换不需要的类型IO
:
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 不了解您在做什么,它就会变得很挑剔。像这样排列起来可以显着减少头痛。
无论如何,对于这些类中的每一个,我们都需要为每个可能的基本情况提供一个实例,并为递归情况提供一个实例。唉,我们不能轻易地拥有一个通用的基本情况,因为重叠通常是令人讨厌的废话。它可以使用fundeps和类型相等条件来完成,但是......呃。也许以后。另一种选择是通过类型级数对转换函数进行参数化,给出所需的转换深度,其缺点是自动化程度较低,但也可以从显式中获得一些好处,例如不太可能偶然发现多态或模棱两可的类型。
现在,我将假设每个函数都以 in 结尾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
这里的约束使用已知实例断言特定上下文,并且我们有一些带有转换的基本类型。再次注意 type functionImport
和 term function共享的并行结构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]
是的,这就是推断的类型。啊,这就是我喜欢看的。
编辑:对于任何想尝试这个的人,我在这里获取了演示的完整代码,稍微清理了一下,然后上传到了 github。