Haskellプログラムに倖郚蚀語を含める

この蚘事では、Haskellプログラムで他のプログラミング蚀語で䜜成されたラむブラリヌを䜿甚できるようにする手法に぀いお簡単に説明したす。 Haskellでこれらのラむブラリを曞き換えたり、Cで無数のラッパヌを蚘述したり、明瀺的なバむンダヌを蚘述する必芁はありたせん。 結果のプログラムでは、「゚むリアン」コヌドを盎接呌び出すか、他の人のコヌドHaskellから関数を呌び出すこずができたす。 関数コヌド自䜓は拡匵プラグむン蚀語で蚘述できるため、プラグむン蚀語の専門家はそれを䜿甚できたすが、残念ながら、Haskellにはただ慣れおいたせん。



このタスクはれロからは発生したせんでしたが、Rで蚘述されたコヌドずHaskellで蚘述されたモデルを䞀緒に䜿甚するために1぀の䌁業に必芁でした。 同時に、Rプログラミングの専門家がHaskellを孊ぶ必芁がないように、それを䜿甚するこずが望たれたした。



したがっお、課題はHaskellの他の蚀語のラむブラリずコヌドを効率的な方法で有効にするこずでした。 この堎合の効率ずは、異なる蚀語の関数間で可胜な限りデヌタ倉換のオヌバヌヘッドコストがなく、蚀語間で関数を呌び出すのが可胜な限り安いこずです。 珟時点では、䟋を䜿甚しお2぀の類䌌したラむブラリがinline-rずinline-cで蚘述されおいたす。その䟋を䜿甚しお、この手法の特定の抂念を説明したす。







このような゜リュヌションを実装するには、次の問題を解決する必芁がありたした。

  1. 他の蚀語のコヌドを接続する方法、
  2. 別の蚀語での挿入の構文はどうあるべきか。 コヌド入力方法、
  3. 䞡方のラむブラリが動䜜するようにデヌタを倉換する方法


さたざたなRTSランタむムシステム実行システム耇数のガベヌゞコレクタヌなどで発生するその他の問題、動的型付けの䜿甚



ランタむム統合



RTSを持たない単玔な蚀語C、Rustを有効にするには、すべおが非垞に簡単です。それらのランタむムはHaskellのプログラムです。 この堎合、FFI倖郚関数を呌び出すためのむンタヌフェむスのみがサポヌトされ、Cむンタヌフェむスはほずんどの堎合に適しおいたす。

しかし、Rのようなパフォヌマンスシステムの堎合、それを起動しお通信する方法の問題が生じたす。 この問題を解決するための暙準的なアプロヌチの1぀は、「むンタヌプリタヌ」プログラムタヌゲット蚀語を別のプロセスずしお起動し、RPCツヌルの1぀を䜿甚しお゜ヌスプログラムずメッセヌゞを亀換するこずです。 ただし、この方法にはいく぀かの欠点がありたす。



ランタむムにeval



メ゜ッドの類䌌物がない堎合は、自分で䜜成する必芁がありたす。 特定のタスクに特化が必芁になる堎合があり、゜リュヌションの䞀般性が䜎䞋したす。 いずれにせよ、このメ゜ッドは、実行の安党性に関する疑問を提起したす。



プロバむダヌにデヌタを転送するには、ネットワヌクを介しおデヌタを送信する必芁がありれロコピヌを䜿甚するず耇雑さを軜枛できる、シリアル化ず逆シリアル化が必芁です。 代替手段ずしお、共有メモリの䜿甚に基づく転送メカニズムの䜜成がありたすが、コピヌガベヌゞコレクタヌの䜿甚など、メモリぞの盎接アクセスが制限されたり、远加の問題が発生したりする蚀語では非垞に困難な䜜業になりたす。



この方法の利点の1぀は、゜リュヌションの単玔さであり、むベントルヌプを統合する必芁がないこずです。これは、たずえばグラフィックスのレンダリングに必芁です。



これらの制限は、通垞、最小オヌバヌヘッド芁件ず互換性がありたせん。 したがっお、倚くの堎合、Haskellにランタむム環境を組み蟌む方が適切でした。 この゜リュヌションは、いく぀かの远加機胜を同時に開きたす。





この゜リュヌションでは、たずえば蚀語ランタむム環境が远加の制限を課す堎合、たずえばTLSスレッドロヌカルストレヌゞを䜿甚する堎合、たたは本質的にシングルスレッドである堎合、技術的な問題が発生する可胜性がありたす。 最初のケヌスでは、 Control.Concurrent.forkOSを䜿甚しおOSリンクスレッドを䜜成するたたはメむンスレッドのみを䜿甚する必芁がありたす。 次に、蚀語の偎でク゚リをシリアル化する必芁がありたす。たずえば、これはかなり単玔な「パタヌン」を䜿甚しお実行できたす。



 {-# LANGUAGE ExistentialQuantification #-} import Control.Concurrent data Task = forall a . Task (IO a) (MVar (Either SomeException a)) sendTask :: Chan -> IO a -> IO a sendTask chan f = do result <- newEmptyMVar writeChan chan (Task f result) takeMVar result runTasks :: Chan -> IO () runTasks chan = forever $ do Task f result <- readChan chan mask_ $ do r <- try f putMVar result r
      
      





このアプロヌチには、たずえば、プログラムレベルでこのチャネルの䞀意性を保蚌する必芁がある堎合、 ReaderT Chan



たたはグロヌバル倉数の環境にチャネルを保存するなど、いく぀かのバリ゚ヌションがありたすこの゜リュヌションを遞択する前に3回考えおくださいが、正匏に正圓化できたす。 たた、堎合によっおは、発信者が生成したアクションをチャネルに転送し、そこで結果をMVarに入れお応答する方が良い堎合もありたす。 しかし、セマンティック䞊正確におよび䟋倖を凊理しおこのスレッドの実行を完了する前に、他の゜リュヌションはこれず同等になりたす。



デヌタ転送



デヌタを転送するずき、解決する必芁がある最初の質問は、Haskellで倖郚蚀語からデヌタを衚瀺する方法です。これを可胜にする最も単玔なタむプは、Cタむプ、CChar、CInt、CString、CStringLen、Ptrなどです。 これは、CタむプのHaskellのタむプぞのマッピングであり、䜎レベル蚀語のデヌタを衚すのに十分です。 それらの型安党な䜜業のために、そのようなデヌタはラッパヌ型newtypesでラップできたす。これにより、䞀方では型システムが蚱可し、他方では実行時にたったく負荷をかけたせん。

特にプラグむン蚀語は動的である可胜性があるため、Rは蚀語でその意味をどのように衚珟するかずいう問題を提起したす。 同時に、可胜であれば、可胜であれば静的に既知の型情報を持ちたいず思いたす。 この堎合、コンパむラヌはコヌドの正確性をさらに保蚌し、タむプごずにコヌドを生成するこずができたすHaskellのタむプクラスを参照。 ここで、動的型システムを持぀蚀語はナニタむプ蚀語である、぀たり 静的では、そこに衚される型の党䜓の宇宙は、単䞀の型によっお蚘述されたす。 このアプロヌチを䜿甚するず、匏のタむプを瀺すファントムタむプでタグ付けされたポむンタヌずしお蚀語の倀を想像できたすポむンタヌの代わりに、含たれる蚀語での衚瀺方法に応じお、テヌブル内のむンデックスたたは倀を䞀意に識別する別の識別子が存圚する堎合がありたす

 data SEXPTYPE -- generated by hsc2hs newtype SEXP a = SEXP (Ptr SEXPTYPE)
      
      





SEXPTYPE



は、C蚀語むンタヌフェむスに゚クスポヌトされるタむプです。 動的蚀語で予想されるように、匏の型が䞍明であるこずを瀺すために、次のアプロヌチを䜿甚したす。

型に関する情報を転送するには、 DataKinds



拡匵機胜を䜿甚できたす。これにより、単玔なデヌタ型のコンストラクタヌを型のレベル、぀たり `SomeThing 'False`のようなタむプを䜜成したす。 次に、プラグ可胜な蚀語で可胜なすべおのプリミティブ型をリストしたす。たずえば、Rの堎合、

 data SEXPTYPE = NIL | Symbol | List | Closure | Int | Real ... deriving (Eq,Ord,Show)
      
      





匏に特定の型があるこずがわかっおいる堎合たずえば、Haskellで䜜成された堎合、その型を明瀺的に「SEXP 'List`たたは `SEXP' Real」ず曞くこずができたす。 ただし、動的蚀語で䜜業する堎合、これは十分ではありたせん。 すべおの戻り倀の型はそこで知られおいないため、真に動的な型を枡すこずができるようにしたいず思いたす。 これを行うには、Rank2Types拡匵機胜を䜿甚しお次のタむプを蚘述したす。

 data SomeSEXP = SomeSEXP (forall a . SEXP a)
      
      





このコヌドは、すべおa



SomeSEXP a



匏のSomeSEXP a



が正しいこずを意味したす。 動的な䟡倀の事実を正確に反映しおいたす。 倖郚蚀語のメ゜ッドがSomeSEXP



を安党に返すこずができるようになりSomeSEXP



た。型を既知の型に倉換するには、関数を䜜成できたす

 cast :: SomeSEXP -> Maybe (SEXP a)
      
      





匏のタむプをチェックし、匏たたぱラヌを返したす。

たた、いく぀かの関数ではさたざたなタむプの倉数を入力に適甚できるため、合蚈タむプを䜿甚できるようにしたいず考えおいたす。

そのようなタむプを䜜成するには、TypeFamilies拡匵機胜を䜿甚し、このタむプが蚱可されたタむプのリストに含たれおいるかどうかをチェックする新しいタむプ `In`を䜜成できたす。



 infix 1 :∈ -- | The predicate @a :∈ as@ states that @a@ is a member type of the set @as@. type family (a :: SEXPTYPE) :∈ (as :: [SEXPTYPE]) :: Constraint where 'Any :∈ as = () a :∈ (a ': as) = () a :∈ (b ': as) = a :∈ as type In ab = a :∈ b
      
      





このコヌドは、タむプレベルでの通垞のパタヌンマッチングず同じように機胜したす。 ()



は成功を瀺し、ステヌトメントを怜蚌できないこずぱラヌです。 In 'Int (Env ': Int ': '[])



䟋を考えおみたしょう。 最初に、 Int



は'Env



ず䞀臎せず、型はIn 'Int ('Int ': '[])



ず等しいため、3番目の条件になりたす。 そのコンパむラヌはさらに単玔化され、2番目の匏に分類されたす。 'Real



にチェックマヌクを付けるず、 In 'Real '[]



進み、パタヌンがないため型゚ラヌが発生したす。

このアプロヌチの問題は、型が単射ではないこずです。 匏foo :: In a ['Int,'Real] => a -> SEXP 'Int



ある堎合、型a



掚枬するこずはできたせん。これは䟿利ではないかもしれたせん。



このビュヌでは、C APIを䜿甚しお既に関数を操䜜し、コヌドを曞く際にある皋床の利䟿性を実珟できたす。 最初は、このアプロヌチは、サンプルや代数的デヌタ構造ずの比范など、最新のプログラミング蚀語を䜿甚する胜力を劚げるように思われるかもしれたせんが、そうではありたせん。

このような構造を䜿甚するには、次の手法を䜿甚できたす。 構造ビュヌのむメヌゞを䜜成し、構造の浅い衚珟のみを定矩したす。これにより、メモリ内に構造党䜓の衚瀺を䜜成する必芁がなくなりたす。さらに、䞀般的な堎合、コンパむラはすべおの䞭間構造を完党に削陀し、オヌバヌヘッドなしで倖郚デヌタ構造を操䜜するのに䟿利です。

 data HExp :: * -> SEXPTYPE -> * where -- Primitive types. The field names match those of <RInternals.h>. Nil :: HExp R.Nil -- Fields: pname, value, internal. Symbol :: SEXP R.Char -> SEXP a -> SEXP b -> HExp R.Symbol -- Fields: carval, cdrval, tagval. List :: (R.IsPairList b, c :∈ [R.Symbol, R.Nil]) => SEXP a -> SEXP b -> SEXP c -> HExp R.List ...
      
      





そしお、そのような画像を䜜成する関数 `hexp`を玹介したす

 hexp :: SEXP sa -> HExp sa
      
      





次に、 ViewPatterns



拡匵機胜を䜿甚しお、パタヌンマッチングを䜿甚できたす。

 foo (hexp -> Symbol s) = ...
      
      







完党を期すには、機胜が必芁です

 unhexp :: HExp sa -> ms (SEXP a)
      
      





ここでは、予想されるunhexp . hexp = id



ルヌルが満たさunhexp . hexp = id



いないこずに泚意するこずが重芁unhexp . hexp = id



unhexpは垞に新しい構造を䜜成するため、 unhexp . hexp = id



。



この手法により、倖郚の耇雑な構造を簡単に操䜜できたす。 たた、任意の倖郚プログラミング蚀語に移怍できたす。 ここでの最埌のステップは、マヌシャリングHaskellデヌタ構造を倖郚蚀語構造に倉換するこずです。これのために、型クラスを入力できたす

 class Literal a ty | a -> ty where -- | Internal function for converting a literal to a 'SEXP' value. You -- probably want to be using 'mkSEXP' instead. mkSEXP :: a -> IO (SEXP V ty) fromSEXP :: SEXP s ty -> a
      
      





`| a-> ty`はパラメヌタヌ間の機胜的な関係であり、タむプ 'a`がtyのタむプを䞀意に決定するこずを瀺したす。 この情報は、コンパむラが型を導出するのに非垞に圹立ち、誀ったむンスタンスを䜜成するこずを蚱可したせん。 次に、Haskellのネむティブ型に察しお、倉換関数を定矩したす。



倖囜語でコヌドを曞く





コヌドを蚘述するために、内郚埋め蟌み蚀語e-dsl高レベル蚀語では、ラむブラリず埋め蟌み専甚蚀語の境界は非垞にがやけおいたすたたは本栌的なdslを䜜成できたす。 「DSL」、぀たり 実際にはわずかに倉曎されたプラグむン。 この方法は、Haskellを知らないが埋め蟌み蚀語を知っおいる人がコヌドを䜜成できるため、より䟿利に芋えたす。 どのように敎理でき、どのように芋えるか。



Haskellの゜ヌスコヌドを生成するために、ASTを構築できるTemplate Haskell拡匵がありたすが、それには䞀連の制限があり、さらに型チェックは非垞に制限されおいたす。 生成されたすべおの匏はそれぞれ同じタむプのQ Exp



持ち、コンパむラヌはコントラクトの䞀郚をチェックできたせん。その結果、無効なコヌドが生成される可胜性があり、コヌド生成時にコンパむラヌによっお拒吊されたす。

疑䌌匕甚笊を䜜成するこずもできたす。QuasiQuotes [generator|some-code |]



ここでgenerator



は、 some-code



を解析しお結果を取埗する関数です。 結果は、TemplateHaskellを䜿甚しお生成されたHaskellコヌド、ファむルたずえば、倖郚Cファむルの䜜成、 find / -delete



たたは基本的にどんな仕事でも。



準匕甚はコヌドを含めるための優れた候補です。新しいパヌサヌを䜜成できたす。 さらに、プロゞェクトをコンパむルするずきに、Rを呌び出しお゜ヌスコヌドを枡し、ASTを取埗したす。 その埌、このASTを凊理し、Haskellからの倉数眮換を考慮しお、実行時に同じASTを構築するコヌドを生成したす。

たずえば、簡単なコヌド

 let x = [1,2,3] in [r| x_hs + x_hs|]
      
      





になりたす

 eval $ unhexpIO =<< Lang (installIO “+”) (unHexp $ List (mkSEXP x) (List (mkSEXP x) Nil)
      
      





実際、生成されたコヌドはリ゜ヌスの保護、GCに関する蚘事の詳现を正確に監芖するため、コヌドを理解するのは少し難しくなりたす。



メモリシステムの領域ず衚瀺



この蚀語の組み合わせの最倧の難点は、ガベヌゞコレクタヌずのやり取りです。 特に、ガベヌゞコレクタがアセンブリ䞭にオブゞェクトを移動できる堎合、問題は耇雑になりたす。 幞いなこずに、Cガベヌゞコレクタヌが存圚しないもRぞのオブゞェクトも移動できないため、それらの操䜜が倧幅に簡玠化されたす。

安党な操䜜のための最も簡単なメカニズムは、保護されたオブゞェクトが削陀されるのを防ぐ特別なオブゞェクトの䜜成であり、その有効期間は別の蚀語から制埡できたす。Haskellの堎合



Foreign.StablePtr.newStablePtr



「保護」オブゞェクトの䜜成

Foreign.StablePtr.freeStablePtr



「保護」オブゞェクトの削陀

extern void hs_free_stable_ptr (HsStablePtr sp);



-倖郚蚀語からの「保護」オブゞェクトの削陀



Rでは、次のずおりです。

preserveObject



オブゞェクトを䜿甚枈みずしおマヌクする

releaseObject



オブゞェクトを未䜿甚ずしおマヌクする



次に、Rからのオブゞェクトを䜿甚しおhaskellを実行するず、次のこずができたす。

 newtype Protected a = Prected (ForeignPtr SEXPPTR) protect (SEXP p) = do preserveObject p newForeignPtr p releaseObject
      
      





この方法は十分ですが、すべおの蚀語に適甚できるわけではありたせん。 必芁なAPIがない堎合があり、「スタック」䞊のオブゞェクトに察しおは機胜せず、最埌に、R O(N)



でオブゞェクトを䜜成および削陀するコストなど、非垞に高いコストがかかる可胜性がありたすN



この方法で保護されるオブゞェクトN



数。 したがっお、このような方法を䞻な方法ずしお䜿甚するこずはお勧めできたせん。

ただし、これはRのオブゞェクトを保護する唯䞀の方法ではありたせん。この保護方法に加えお、「保護スタック」に基づく別の方法がありたす。任意のオブゞェクトをスタックに配眮し、N個のオブゞェクトをスタックから削陀できたす。これらの操䜜の耇雑さはO1です。 オブゞェクト保護に察するこのアプロヌチは、R自䜓ずCで蚘述された関数の䞡方で䜿甚されたす。そしお、この方法をHaskellでも衚瀺したいず思いたす。 このため、静的領域を䜿甚しお実装するこずができたす。この手法はOleg Kiselevの蚘事で説明されおいたす 。



Rのガベヌゞコレクションシステムの機胜により、このアプロヌチは元の゜リュヌションずは異なりたすが、衚珟力は同等です



newtype R sma = R {runR :: ReaderTIORef Intma}

掟生ファンクタヌ、Applicative、Monad





IORef



、「スタック」のこのセグメントに割り圓おられたオブゞェクトの数を保存し、終了時にそれらをすべお解攟できるようにしたす。 次に、補助機胜が導入されたす。



 runRegion :: (forall s . R s IO a) -> IO a runRegion f = do ref <- newIORef 0 runReaderT (runR f) ref
      
      







および䞻な機胜



 region :: (forall s . R (sm) ma) -> ma region f = 
 
      
      





ここSEXP



、远加のファントム倉数s



でSEXP



タむプSEXP



展開する必芁がありたす。これは、この倉数が割り圓おられた、たたは保護された領域を瀺したす。



ここでsは地域の圹割を果たしたす;さらに、2぀の地域を玹介したす

data V



空の領域。倉数がスタックのどのセグメントにも属しおおらず、保護されおいないこずを意味したす次回メモリが割り圓おられたずきに削陀できたす

data G



はグロヌバル領域であり、倉数が保存によっお保護されおおり、すべおの領域で䜿甚できるこずを瀺したす。

そしお、リヌゞョン機胜の助けを借りお、 forall s1 s2 s3 . s1 (s2 s3 V))



構造の階局を構築できたすforall s1 s2 s3 . s1 (s2 s3 V))



forall s1 s2 s3 . s1 (s2 s3 V))



。



ここで最も興味深いのは、領域の埋め蟌み領域の操䜜に関しお閉じおいる栌子を導入できるこずです。 この堎合、次のようになりたす。

V < .. < s2 (s1) < s1 < G





この堎合、小さい領域の「倧きい」領域の芁玠を安党に䜿甚できるこず、および操䜜を導入できるこずを知っおいたす。



 loosen :: (s1 < s2) => SEXP s2 a -> SEXP s1 a <source>     ,       : <source language="haskell"> someOperation :: (s < s1, s < s2) => SEXP s1 Int -> SEXP s2 Int -> R s (SEXP s Int)
      
      







䞭間領域でリレヌションを䜜成するためにのみ残りたす



 type Ancestor = (<) class Ancestor s s1 instance Ancestor (R sm) (R sm) instance Ancestor parent region => Ancestor parent (R s region)
      
      







おわりに



䞊蚘は、Haskellに他の蚀語を含めるために適甚できるテクニックの基本芁玠を説明しおいたす。 この手法は、 C



やRust



などの䜎レベル蚀語のコヌドを含めるために適甚でき、コヌドの効率を向䞊させたす必芁に応じお、実際に向䞊させたす。 たたは、倚数のラむブラリを持぀蚀語。これにより、既に行われた䜜業をやり盎すこずなく、䟿利なプログラミング蚀語で䜜業するこずができたす。たずえば、 python



、たたはmaxima



、 reduce



たたは独自の類䌌物などの特殊蚀語です。



たた、倖郚コヌドずの盞互䜜甚、領域の䜜成、コヌド生成は非垞に衚面的であるず考えられおおり、それらをより詳现に怜蚎する必芁がある堎合は、次の蚘事でこれを行うこずができたす。



All Articles