12

Gibt es eine Möglichkeit, Funktionen wie withCString zu verketten? Damit meine ich jede Funktion, die etwa wie f :: Foo -> (CFoo -> IO a) -> IO a aussieht.Gibt es eine Möglichkeit, Funktionen wie mitCString zu verketten?

Zum Beispiel können, sagen, dass es eine Funktion cFunc :: CString -> CFoo -> CBar -> IO()

Usualy ist, würde ich so etwas wie:

haskellFunc string foo bar = 
    withCString string $ \ cString -> 
    withCFoo foo $ \ cFoo -> 
     withCBar bar $ \ cBar -> 
     cFunc cString cFoo cBar 

Aber ich möchte, wie etwas zu tun ist:

haskellFunc = (withCString |.| withCFoo |.| withCBar) cFunc 

mit ein geeigneter Kompositionsoperator |.|.

Ich schreibe Bibliothek mit vielen C-Bindungen, und diese Textplatte kommt oft . Mache ich etwas falsch?

Antwort

17

Sie die Continuation applicative zum Komponieren diese a -> (b -> IO c) -> IO c Funktionen verwenden können:

import Control.Monad.Cont 

haskellFunc :: String -> Foo -> Bar -> IO() 
haskellFunc string foo bar = flip runCont id $ 
    cFunc <$> 
     cont (withCString string) <*> 
     cont (withCFoo foo) <*> 
     cont (withCBar bar) 

Oder mit ein wenig Extra-Syntax:

haskellFunc' :: String -> Foo -> Bar -> IO() 
haskellFunc' string foo bar = flip runCont id $ 
    cFunc <<$>> withCString string <<*>> withCFoo foo <<*>> withCBar bar 
    where 
    f <<$>> x = f <$> cont x 
    f <<*>> x = f <*> cont x 
+1

Oder wenn wir jemals idiomatische Klammern bekommen (oder SHE verwenden wollen), '(| cFunc (cont (withCString string)) (cont (mitCFoo foo)) (cont (withCBar bar)) |)' – copumpkin

+2

Ein weiterer Vorteil von zu erkennen, dass dies nur "Cont" in Verkleidung ist, dass Sie andere Leckereien kostenlos bekommen. Sagen wir zum Beispiel, dass Sie eine beliebige Sammlung dieser CPS-artigen Allokatoren benötigen: Sie könnten einfach 'sequence',' traverse' oder ähnliches verwenden, um eine Liste oder eine andere Sammlung von Werten auf einmal zu erhalten. – copumpkin

+1

Noch einmal, Haskell enttäuscht nicht. So elegant und schön :) – ivokosir

1

Leider können Sie keine Funktion schreiben, die etwas so Allgemeines tut, wie Sie möchten. Das Problem ist mit Haskells Typsystem. In Ihrem Beispiel benötigt drei Argumente. Wenn Sie also Ihre Convenience-Funktion schreiben, erwartet Sie eine C-Funktion, die drei Argumente benötigt. Es gäbe keine Möglichkeit, eine Funktion zu schreiben, die eine Anzahl von Argumenten akzeptieren könnte; Haskells Typsystem ist zu streng. In diesem Sinne können Sie jedoch mehrere verschiedene Funktionen für jeweils eine mit einer anderen Anzahl von Argumenten schreiben. Ob sich dies lohnt oder nicht, hängt davon ab, wie oft Sie diese Art von Kesselplatte verwenden müssen.

cApply2 :: (a' -> b' -> c) 
     -> (a -> (a' -> c)) 
     -> (b -> (b' -> c)) 
     -> a -> b -> c 
cApply2 cFunc withArg1 withArg2 arg1 arg2 = 
    withArg1 arg1 $ \cArg1 -> 
    withArg2 arg2 $ \cArg2 -> 
     cFunc cArg1 cArg2 

cApply3 :: (a' -> b' -> c' -> d) 
     -> (a' -> (a -> d)) 
     -> (b' -> (b -> d)) 
     -> (c' -> (c -> d)) 
     -> a -> b -> c -> d 
cApply3 cFunc withArg1 withArg2 withArg3 arg1 arg2 arg3 = 
    withArg1 arg1 $ \cArg1 -> 
    withArg2 arg2 $ \cArg2 -> 
     withArg3 arg3 $ \cArg3 -> 
     cFunc cArg1 cArg2 cArg3 

Jetzt können Sie die C-Funktionen wie folgt verwenden.

haskellFunc :: String -> Foo -> Bar -> IO() 
haskellFunc = cApply3 cFunc withCString withCFoo withCBar 
4

Ich nahm einen Stich bei diesem. Das Ergebnis ist nicht schöne, aber es funktioniert. Der TL; DR ist, dass bis zum Ende, wir Ihre Funktion wie diese schreiben können, vorausgesetzt, ich keine lähmenden Fehler:

haskellFunc string foo bar = cFunc <^ string <^> foo ^> bar 

Wir brauchen ein paar GHC Erweiterungen für diese zu arbeiten, aber sie sind ziemlich zahm:

{-# LANGUAGE MultiParamTypeClasses #-} 
-- So that we can declare an instance for String, 
-- aka [Char]. Without this extension, we'd only 
-- be able to declare an instance for [a], which 
-- is not what we want. 
{-# LANGUAGE FlexibleInstances #-} 

Zuerst definiere ich eine typeclass die gemeinsame Natur der CString darzustellen, CFoo und CBar, unter Verwendung von withCType als einheitliche Namen für withC___:

-- I use c as the type variable to indicate that 
-- it represents the "C" version of our type. 
class CType a c where 
    withCType :: a -> (c -> IO b) -> IO b 

Dann werden einige Dummy-Typen und Instanzen, so dass ich konnte dies in Isolation typecheck:

-- I'm using some dummy types I made up so I could 
-- typecheck this answer standalone. 
newtype CString = CString String 
newtype CInt = CInt Int 
newtype CChar = CChar Char 

instance (CType String CString) where 
    -- In reality, withCType = withCString 
    withCType str f = f (CString str) 

instance (CType Int CInt) where 
    withCType str f = f (CInt str) 

instance (CType Char CChar) where 
    withCType str f = f (CChar str) 

Mein erster Gedanke war, dass wir so etwas wie dieses haben würde, dass wir unsere Funktionen auf dem zugrunde liegenden C verwenden würde aufrufen Typen ...

liftC :: CType a c => (c -> IO b) -> (a -> IO b) 
liftC cFunc x = withCType x cFunc 

Aber das lässt uns nur Funktionen eines Arguments heben. Wir möchten Funktionen von mehreren Argumenten heben ...

liftC2 :: (CType a c, CType a' c') => (c -> c' -> IO b) -> (a -> a' -> IO b) 
liftC2 cFunc x y = withCType x (\cx -> withCType y (cFunc cx)) 

Das funktioniert gut, aber es wäre toll, wenn wir nicht für jede arity einer von denen zu definieren, brauchten wir suchen. Wir wissen bereits, dass Sie alle die liftM2, liftM3 usw. Funktionen mit Ketten von <$> und <*> ersetzen können, und es wäre schön, das gleiche zu tun.

So war mein erster Gedanke, zu versuchen liftC in einen Bediener zu drehen, und es zwischen jedem Argumente durchsetzen. So würde es in etwa so aussehen:

Nun ... wir können das nicht ganz tun. Weil die Typen nicht funktionieren. Bedenken Sie:

(<^>) :: CType a c => (c -> IO b) -> (a -> IO b) 
cFunc <^> x = withCType x cFunc 

Der IO Teil withCType dies erschwert. Um dies gut zu verketten, müssten wir eine andere Funktion des Formulars (c -> IO b) zurückbekommen, aber stattdessen bekommen wir das IO Rezept zurück, um das zu produzieren. Das Ergebnis des Aufrufs des obigen <^> für eine "binäre" Funktion ist beispielsweise IO (c -> IO b). Das ist beunruhigend.

Wir können dies umgehen, indem wir drei verschiedene Operatoren ... von denen einige in IO arbeiten und von denen einige nicht arbeiten, und sie an der richtigen Position in einer Aufrufkette verwenden. Das ist nicht sehr gepflegt oder nett. Aber es funktioniert. Es muss ein sauberer Weg sein, diese gleiche Sache zu tun ...

-- Start of the chain: pure function to a pure 
-- value. The "pure value" in our case will be 
-- the "function expecting more arguments" after 
-- we apply its first argument. 
(<^) :: CType a c => (c -> b) -> (a -> IO b) 
cFunc <^ x = withCType x (\cx -> return (cFunc cx)) 

-- Middle of the chain: we have an IO function now, 
-- but it produces a pure value -- "gimme more arguments." 
(<^>) :: CType a c => IO (c -> b) -> a -> IO b 
iocFunc <^> x = iocFunc >>= (<^ x) 

-- End of the chain: we have an IO function that produces 
-- an IO value -- no more arguments need to be provided; 
-- here's the final value. 
(^>) :: CType a c => IO (c -> IO b) -> a -> IO b 
iocFunc ^> x = withCType x =<< iocFunc 

Wir haben dieses seltsame franken wie diese verwenden können (das Hinzufügen von mehr <^> s für höhere arity Funktionen):

main = do 
    x <- cFunc <^ "hello" <^> (10 :: Int) ^> 'a' 
    print x 

cFunc :: CString -> CInt -> CChar -> IO() 
cFunc _ _ _ = pure() 

Dies ist etwas unelegant. Ich würde gerne einen saubereren Weg sehen, um das zu erreichen. Und ich liebe nicht die Symbole I für diejenigen Betreiber wählte ...