12

C'è un modo per concatenare funzioni come withCString? Con ciò intendo qualsiasi funzione che assomigli a qualcosa come f :: Foo -> (CFoo -> IO a) -> IO a.C'è un modo per concatenare funzioni come conCString?

Ad esempio, consente di dire v'è una funzione cFunc :: CString -> CFoo -> CBar -> IO()

Di solito, vorrei fare qualcosa di simile:

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

ma vorrei fare qualcosa di simile:

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

con alcuni operatori di composizione appropriati |.|.

Sto scrivendo una libreria con molti binding C, e questa piastra viene spesso . Sto facendo qualcosa di sbagliato?

risposta

17

È possibile utilizzare il Continuation applicative per la composizione di queste funzioni: a -> (b -> IO c) -> IO c

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) 

o con un po 'di sintassi in più:

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

O se mai otteniamo parentesi idiom (o si desidera utilizzare SHE), '(| cFunc (cont (withCString stringa)) (cont (withCFoo foo)) (cont (withCBar bar)) |)' – copumpkin

+2

Un altro vantaggio di riconoscendo che questo è solo "Cont" travestito è che ottieni altri gadget gratis. Supponiamo ad esempio di aver bisogno di una raccolta arbitraria di questi allocatori in stile CPS: si potrebbe semplicemente usare 'sequence',' traverse' o simile per ottenere una lista o un'altra raccolta di valori tutto in una volta. – copumpkin

+1

Ancora una volta, haskell non delude. Così elegante e bellissima :) – ivokosir

1

Sfortunatamente, non è possibile scrivere una funzione che faccia qualcosa di generico come si vorrebbe. Il problema è con il sistema di tipi di Haskell. Nel tuo esempio, cFunc accetta tre argomenti, quindi quando hai scritto la tua funzione di convenienza ci si aspetterebbe una funzione C che richiedesse tre argomenti. Non ci sarebbe modo di scrivere una funzione che possa accettare un cFunc di un numero qualsiasi di argomenti; Il sistema di tipi di Haskell è troppo severo. Con questo in mente, tuttavia, è possibile scrivere diverse funzioni, ciascuna per un cFunc con un numero diverso di argomenti. Se questo vale o meno lo sforzo dipende da quanto spesso è necessario utilizzare quel tipo di piastra della caldaia.

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 

Ora è possibile utilizzare le funzioni C in questo modo.

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

Ho fatto una pugnalata a questo. Il risultato non è bello, ma funziona. Il TL; DR è che, alla fine, possiamo scrivere la funzione come questo, supponendo che ho fatto nessun errore paralizzanti:

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

Abbiamo bisogno di alcune estensioni GHC per far funzionare tutto questo, ma sono piuttosto noiosi:

{-# 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 #-} 

prima cosa definire un typeclass per rappresentare la natura comune di CString, CFoo, e CBar, utilizzando withCType come unico nome per 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 

Poi alcuni tipi fittizi e le istanze in modo che potessi TYPECHECK questo in isolamento:

-- 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) 

Il mio pensiero iniziale era che avremmo qualcosa di simile che saremmo utilizzare per richiamare le nostre funzioni sul sottostante C tipi ...

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

Ma questo ci consente di sollevare solo le funzioni di un argomento. Vorremmo sollevare funzioni di più argomenti ...

Che funziona bene, ma sarebbe bello se non avessimo bisogno di definire uno di quelli per ogni area che cerchiamo. Sappiamo già che è possibile sostituire tutte le funzioni liftM2, liftM3, ecc. Con catene di <$> e <*>, e sarebbe bello fare lo stesso qui.

Quindi il mio primo pensiero è stato quello di provare a trasformare liftC in un operatore e intersperse tra ogni argomento. Quindi sarebbe qualcosa del genere:

func <^> x <^> y <^> z 

Beh ... non possiamo proprio farlo. Perché i tipi non funzionano. Considerate questo:

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

La IO parte di withCType rende questo difficile. Per fare in modo che questa catena si concatri, dovremmo recuperare un'altra funzione del modulo (c -> IO b), ma invece torniamo alla ricetta IO per produrla. Il risultato di invocare il precedente <^> su una funzione "binaria", ad esempio, è IO (c -> IO b). È preoccupante.

Possiamo incidere su questo fornendo tre diversi operatori ... alcuni dei quali funzionano in IO e alcuni dei quali non funzionano e li utilizzano nella giusta posizione in una catena di chiamate. Questo non è molto bello o carino. Ma funziona. Ci deve essere un modo più pulito per fare la stessa cosa ...

-- 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 

Possiamo usare questo Frankenstein strano come questo (l'aggiunta di più <^> s per le funzioni più alto-arità):

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

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

Questo è alquanto inelegante. Mi piacerebbe vedere un modo più pulito per ottenere questo. E non amo i simboli che ho scelto per gli operatori ...

Problemi correlati