Подтвердить что ты не робот

Есть ли способ связать функции, такие как withCString?

Есть ли способ связать функции, такие как withCString? Под этим я подразумеваю любые функция, которая выглядит примерно как f :: Foo -> (CFoo -> IO a) -> IO a.

Например, скажем, существует функция cFunc :: CString -> CFoo -> CBar -> IO ()

Обычно, я бы сделал что-то вроде:

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

Но я хотел бы сделать что-то вроде:

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

с некоторым подходящим оператором композиции |.|.

Я пишу библиотеку с большим количеством связок C, и этот шаблон довольно часто. Я что-то делаю неправильно?

4b9b3361

Ответ 1

Вы можете использовать Cont приложение для отклонения для составления этих 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)

Или с дополнительным синтаксисом:

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

Ответ 2

Я принял удар. Результат не красив, но он работает. TL, DR - это то, что к концу мы можем написать вашу функцию следующим образом, предполагая, что я не совершил ошибок:

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

Нам нужны некоторые расширения GHC для этого, но они довольно ручные:

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

Сначала я определяю typeclass для представления общей природы CString, CFoo и CBar, используя withCType как единственное имя для 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

Затем некоторые типы и экземпляры фиктивного типа, чтобы я мог проверить это отдельно:

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

Моя первоначальная мысль заключалась в том, что у нас было бы что-то подобное, что мы будем использовать для вызова наших функций на базовых типах C...

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

Но это только позволяет нам поднимать функции одного аргумента. Мы хотели бы поднять функции нескольких аргументов...

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

Это работает очень хорошо, но было бы здорово, если бы нам не нужно было определять одну из них для каждой из нас. Мы уже знаем, что вы можете заменить все функции liftM2, liftM3 и т.д. Цепочками <$> и <*>, и было бы неплохо сделать то же самое здесь.

Итак, моя первая мысль заключалась в том, чтобы попытаться превратить liftC в оператор и проинтегрировать его между каждым аргументом. Поэтому он выглядит примерно так:

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

Ну... мы не можем этого сделать. Потому что типы не работают. Рассмотрим это:

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

Часть IO withCType делает это трудным. Для того, чтобы это хорошо сочеталось, нам нужно было вернуть другую функцию формы (c -> IO b), но вместо этого мы вернемся к рецепту IO, чтобы создать это. Результатом вызова вышеуказанного <^> на "двоичную" функцию, например, является IO (c -> IO b). Это беспокоит.

Мы можем взломать это, предоставляя три разных оператора... некоторые из которых работают в IO, а некоторые из них не работают, и используют их в правильной позиции в цепочке вызовов. Это не очень аккуратно или приятно. Но это действительно работает. Должен быть более чистый способ сделать то же самое...

-- 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 the final value.
(^>) :: CType a c => IO (c -> IO b) -> a -> IO b
iocFunc ^> x = withCType x =<< iocFunc

Мы можем использовать этот странный frankenstein как это (добавив больше <^> для функций более высокой степени):

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

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

Это несколько неэлегантно. Мне бы хотелось увидеть более чистый способ понять это. И я не люблю символы, которые я выбрал для этих операторов...

Ответ 3

К сожалению, вы не можете написать функцию, которая делает что-то общее, как вы хотели бы сделать. Проблема заключается в системе типа Haskell. В вашем примере cFunc принимает три аргумента, поэтому, когда вы написали свою удобную функцию, ожидалось бы, что функция C приняла три аргумента. Не было бы возможности написать функцию, которая могла бы принять cFunc любого количества аргументов; Система типа Haskell слишком строгая. Имея это в виду, однако, вы можете написать несколько разных функций, каждый для cFunc с различным количеством аргументов. Независимо от того, стоит ли это усилий, зависит от того, как часто вам понадобится использовать такую ​​плиту котла.

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

Теперь вы можете использовать такие C-функции.

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