外部関数の引数の一部が CString である外部関数呼び出しを受け取り、代わりに String を受け入れる関数を返す関数を持つことは可能ですか?
可能ですか?
<lambdabot> The answer is: Yes! Haskell can do that.
Ok。解決できて良かったです。
いくつかの面倒な手順でウォームアップします。
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
あ、悪くないですけどね。ほら、お母さん、重ならないで!
newCString や withCString などの CString に変換されるものはすべて IO であるため、この問題は IO 関数に当てはまるようです。
右。ここで注意すべきことは、私たち自身が関心を持っている 2 つのやや相互に関連した問題があるということです。および変換を実行することによって導入された余分なコンテキスト。これに完全に対処するために、両方の部分を明示的にして、適切にシャッフルします。分散にも注意する必要があります。関数全体を持ち上げるには、共変と反変の両方の位置で型を操作する必要があるため、両方向の変換が必要になります。
ここで、翻訳したい関数が与えられると、計画は次のようになります。
- 関数の引数を変換し、新しい型とコンテキストを受け取ります。
- コンテキストを関数の結果に任せて、必要な方法で引数を取得します。
- 可能であれば冗長なコンテキストを折りたたむ
- 関数の結果を再帰的に変換して、複数引数の関数を処理します
まあ、それはそれほど難しくないように聞こえます。まず、明示的なコンテキスト:
class (Functor f, Cxt t ~ f) => Context (f :: * -> *) t where
type Collapse t :: *
type Cxt t :: * -> *
collapse :: t -> Collapse t
これは、 context と、その 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... -}
十分に単純です。コンテキストのさまざまな組み合わせを処理するのは少し面倒ですが、インスタンスは明白で簡単に記述できます。
また、変換する型を指定してコンテキストを決定する方法も必要です。現在、どちらの方向にもコンテキストは同じですが、そうでない場合も確かに考えられるので、それらを別々に扱いました。したがって、インポート/エクスポート変換の新しい最も外側のコンテキストを提供する 2 つの型ファミリがあります。
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
これは、2 つの型ext
とint
が互いに一意に変換可能であることを示しています。タイプごとに常に 1 つのマッピングしか持たないことが望ましいとは限らないことは理解していますが、これ以上複雑にする気はありませんでした (少なくとも、現時点ではそうではありません)。
前述のとおり、ここでは再帰変換の処理も延期しました。おそらくそれらは組み合わせることができますが、この方法でより明確になると感じました。非再帰的な変換には、対応するコンテキストを導入する単純で明確に定義されたマッピングがありますが、再帰的な変換では、コンテキストを伝播およびマージし、再帰的なステップを基本ケースと区別して処理する必要があります。
ああ、もうお気づきかもしれませんが、クラスのコンテキストで面白い波状のチルダ ビジネスが行われています。これは、2 つの型が等しくなければならないという制約を示しています。この場合、各型関数を反対の型パラメーターに結び付け、上記の双方向性を提供します。ええと、あなたはおそらくかなり最近の 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
ここで問題の核心に迫り、関数全体を再帰的に変換します。さらに別の型クラスを導入したことは驚くべきことではありません。今回はインポート/エクスポートの変換を分離したので、実際には 2 つです。
class FFImport ext where
type Import ext :: *
ffImport :: ext -> Import ext
class FFExport int where
type Export int :: *
ffExport :: int -> Export int
ここには何も興味深いものはありません。ここまでで、共通のパターンに気付いているかもしれません。用語と型の両方のレベルでほぼ同じ量の計算を行っており、名前と式の構造を模倣するところまで、それらを並行して行っています。実数値を含むものに対して型レベルの計算を行っている場合、これはかなり一般的です。なぜなら、GHC は何をしているのか理解できないと面倒になるからです。このように並べると、頭痛が大幅に軽減されます。
とにかく、これらのクラスごとに、考えられる基本ケースごとに 1 つのインスタンスと、再帰ケース用に 1 つのインスタンスが必要です。残念ながら、オーバーラップに関する通常の厄介なナンセンスのために、一般的な基本ケースを簡単に作成することはできません。これは、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
ここでも、 type functionと 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 にアップロードしました。