Поливариадная функция Хаскеля с IO - PullRequest
9 голосов
/ 11 августа 2011

Возможно ли иметь функцию, которая принимает вызов сторонней функции, где некоторые аргументы сторонней функции являются CString, и возвращает функцию, которая принимает вместо нее строку?

Вот пример того, что я ищу:

 foreign_func_1 :: (CDouble -> CString -> IO())
 foreign_func_2 :: (CDouble -> CDouble -> CString -> IO ())

 externalFunc1 :: (Double -> String -> IO())
 externalFunc1 = myFunc foreign_func_1

 externalFunc2 :: (Double -> Double -> String -> IO())
 externalFunc2 = myFunc foreign_func_2

Я понял, как это сделать с числовыми типами Си. Однако я не могу найти способ сделать это, который может разрешить преобразование строк.

Кажется, проблема в том, чтобы соответствовать функциям ввода-вывода, поскольку все, что преобразуется в строки CString, такие как newCString или withCString, является IO.

Вот как выглядит код для простого преобразования парных чисел.

class CConvertable interiorArgs exteriorArgs where
   convertArgs :: (Ptr OtherIrrelevantType -> interiorArgs) -> exteriorArgs

instance CConvertable (IO ()) (Ptr OtherIrrelevantType -> IO ()) where
   convertArgs = doSomeOtherThingsThatArentCausingProblems
instance (Real b, Fractional a, CConvertable intArgs extArgs) => CConvertable (a->intArgs) (b->extArgs) where
    convertArgs op x= convertArgs (\ctx -> op ctx (realToFrac x))

Ответы [ 4 ]

16 голосов
/ 12 августа 2011

Возможно ли иметь функцию, которая принимает вызов сторонней функции, где некоторые аргументы сторонней функции являются CString, и возвращать функцию, которая принимает вместо нее строку?

Возможно ли это,Вы спрашиваете?

<lambdabot> The answer is: Yes! Haskell can do that.

Хорошо.Хорошо, что мы все выяснили.

Разминка с несколькими утомительными формальностями:

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

Ах, все не так плохо.Послушай, ма, никаких наложений!

Кажется, проблема в том, что все подходит для функций ввода-вывода, поскольку все, что преобразуется в строки CString, такие как newCString или withCString, является IO.

Right,Здесь следует обратить внимание на то, что есть два взаимосвязанных вопроса, которыми мы должны заниматься: соответствие между двумя типами, допускающее преобразования;и любой дополнительный контекст, введенный путем выполнения преобразования.Чтобы полностью разобраться с этим, мы сделаем обе части явными и перетасуем их соответствующим образом.Нам также необходимо учитывать дисперсию ;поднятие целой функции требует работы с типами как в ковариантном, так и в контравариантном положении, поэтому нам понадобятся преобразования, идущие в обоих направлениях.

Теперь, учитывая функцию, которую мы хотим перевести, план выглядит примерно так:

  • Преобразование аргумента функции, получение нового типа и некоторого контекста.
  • Отложите контекст на результат функции, чтобы получить аргумент так, как мы этого хотим.
  • Сверните избыточные контексты, где это возможно
  • Рекурсивный перевод результата функции для обработки функций с несколькими аргументами

Что ж, это не кажется слишком сложным.Во-первых, явные контексты:

class (Functor f, Cxt t ~ f) => Context (f :: * -> *) t where
    type Collapse t :: *
    type Cxt t :: * -> *
    collapse :: t -> Collapse t

Это говорит о том, что у нас есть контекст f, и некоторый тип t с этим контекстом.Функция типа 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... -}

Достаточно просто.Обработка различных комбинаций контекстов немного утомительна, но примеры очевидны и их легко написать.

Нам также понадобится способ определения контекста для данного типа для преобразования.В настоящее время контекст одинаков в обоих направлениях, но вполне возможно, что он будет иным, поэтому я рассмотрел их отдельно.Таким образом, у нас есть два семейства типов, предоставляющих новый внешний контекст для преобразования импорта / экспорта:

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

Это говорит о том, что два типа ext и int являются уникально конвертируемыми друг в друга.Я понимаю, что не всегда желательно иметь только одно сопоставление для каждого типа, но мне не хотелось усложнять ситуацию (по крайней мере, не сейчас).

Как уже отмечалось, я такжеотложить обработку рекурсивных преобразований здесь;возможно, они могли бы быть объединены, но я чувствовал, что так будет яснее.У нерекурсивных преобразований есть простые, четко определенные отображения, которые вводят соответствующий контекст, в то время как рекурсивные преобразования должны распространять и объединять контексты и иметь дело с выделением рекурсивных шагов из базового случая.к настоящему моменту в классе разворачивается забавная шаткая тильда.Это указывает на ограничение, что два типа должны быть равны;в этом случае он связывает каждую функцию типа с параметром противоположного типа, что придает двунаправленный характер, упомянутый выше.Э-э, вы, вероятно, хотите иметь сравнительно недавно GHC.На старых GHC вместо этого потребовались бы функциональные зависимости, и они были бы записаны как что-то вроде class Convert ext int | ext -> int, int -> ext.

Функции преобразования уровня термина довольно просты - обратите внимание на применение функции типа в их результате; Приложение, как всегда, является левоассоциативным, так что это просто применение контекста из более ранних семейств типов. Также обратите внимание на пересечение имен в том, что контекст export происходит из поиска с использованием типа native .

Итак, мы можем конвертировать типы, которые не нужны 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

Теперь поразить суть дела и рекурсивно перевести целые функции. Неудивительно, что я ввел еще один класс . На самом деле, два, так как на этот раз я разделил конверсии импорта / экспорта.

class FFImport ext where
    type Import ext :: *
    ffImport :: ext -> Import ext

class FFExport int where
    type Export int :: *
    ffExport :: int -> Export int

Ничего интересного здесь. Возможно, вы уже заметили общую закономерность - мы выполняем примерно одинаковое количество вычислений как на уровне терминов, так и на уровне типов, и мы делаем их в тандеме, даже до того, чтобы имитировать имена и структуру выражений. Это довольно часто, если вы выполняете вычисления на уровне типов для вещей, связанных с реальными значениями, поскольку GHC становится суетливым, если не понимает, что вы делаете. Выстраивание подобных вещей значительно уменьшает головные боли.

В любом случае, для каждого из этих классов нам нужен один экземпляр для каждого возможного базового случая и один для рекурсивного случая. Увы, мы не можем легко иметь общий базовый случай из-за обычной надоедливой чепухи с перекрытием. Это можно сделать, используя 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

Ограничения здесь утверждают определенный контекст, используя известный экземпляр, и что у нас есть некоторый базовый тип с преобразованием. Опять же, обратите внимание на параллельную структуру, совместно используемую функцией type Import и функцией term 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 .

7 голосов
/ 12 августа 2011

Это можно сделать с помощью шаблона haskell. Во многом это проще, чем альтернативы, включающие классы, так как это легче сопоставить с образцом Language.Haskell.TH.Type, чем то же самое с экземплярами.

{-# LANGUAGE TemplateHaskell #-}
--  test.hs
import FFiImport
import Foreign.C

foreign_1 :: CDouble -> CString -> CString -> IO CString
foreign_2 :: CDouble -> CString -> CString -> IO (Int,CString)
foreign_3 :: CString -> IO ()

foreign_1 = undefined; foreign_2 = undefined; foreign_3 = undefined

fmap concat (mapM ffimport ['foreign_1, 'foreign_2, 'foreign_3])

Предполагаемые типы сгенерированных функций:

imported_foreign_1 :: Double -> String -> String -> IO String
imported_foreign_2 :: Double -> String -> String -> IO (Int, String)
imported_foreign_3 :: String -> IO ()

Проверка сгенерированного кода путем загрузки test.hs с -ddump-splices (обратите внимание, что GHC по-прежнему не хватает некоторых скобок в красивой печати) показывает, что foreign_2 пишет определение, которое после некоторого переворота выглядит так:

imported_foreign_2 w x y
  = (\ (a, b) -> ((return (,) `ap` return a) `ap` peekCString b) =<<
     join
       (((return foreign_2 `ap`
          (return . (realToFrac :: Double -> CDouble)) w) `ap`
         newCString x) `ap`
        newCString y))

или переведено, чтобы сделать запись:

imported_foreign_2 w x y = do
       w2 <- return . (realToFrac :: Double -> CDouble) w
       x2 <- newCString x
       y2 <- newCString y
       (a,b) <- foreign_2 w2 x2 y2
       a2 <- return a
       b2 <- peekCString b
       return (a2,b2) 

Генерация кода первым способом проще в том, что меньше переменных для трек. Хотя foldl ($) f [x, y, z] не проверяет тип, когда это будет означать ((f $ x) $ y $ z) = f x y z это приемлемо в шаблоне haskell, который включает в себя только несколько разных типы.

Теперь для фактической реализации этих идей:

{-# LANGUAGE TemplateHaskell #-}
-- FFiImport.hs
module FFiImport(ffimport) where
import Language.Haskell.TH; import Foreign.C; import Control.Monad

-- a couple utility definitions

-- args (a -> b -> c -> d) = [a,b,c]
args (AppT (AppT ArrowT x) y) = x : args y
args _ = []

-- result (a -> b -> c -> d) = d
result (AppT (AppT ArrowT _) y) = result y
result y = y

-- con (IO a) = IO
-- con (a,b,c,d) = TupleT 4
con (AppT x _) = con x
con x = x

-- conArgs (a,b,c,d) = [a,b,c,d]
-- conArgs (Either a b) = [a,b]
conArgs ty = go ty [] where
    go (AppT x y) acc = go x (y:acc)
    go _ acc = acc

Соединение $ (ffimport 'foreign_2) ищет тип foreign_2 с reify для решить, какие функции применять к аргументам или результату.

-- Possibly useful to parameterize based on conv'
ffimport :: Name -> Q [Dec]
ffimport n = do
    VarI _ ntype _ _ <- reify n

    let ty :: [Type]
        ty = args ntype

    let -- these define conversions
        --   (ffiType, (hsType -> IO ffiType, ffiType -> IO hsType))
        conv' :: [(TypeQ, (ExpQ, ExpQ))]
        conv' = [
            ([t| CString |], ([| newCString |],
                              [| peekCString |])),
            ([t| CDouble |], ([| return . (realToFrac :: Double -> CDouble) |],
                              [| return . (realToFrac :: CDouble -> Double) |]))
            ]

        sequenceFst :: Monad m => [(m a, b)] -> m [(a,b)]
        sequenceFst x = liftM (`zip` map snd x) (mapM fst x)

    conv' <- sequenceFst conv'
    -- now    conv' :: [(Type, (ExpQ, ExpQ))]

Учитывая вышеизложенное, применять эти функции довольно просто, когда типы совпадают. Задний случай будет короче, если преобразовать компоненты возвращенные кортежи не были важны.

    let conv :: Type -- ^ type of v
             -> Name -- ^ variable to be converted
             -> ExpQ
        conv t v
            | Just (to,from) <- lookup t conv' =
                [| $to $(varE v) |]
            | otherwise = [| return $(varE v) |]

        -- | function to convert result types back, either
        --  occuring as IO a, IO (a,b,c)   (for any tuple size)
        back :: ExpQ
        back
            |   AppT _ rty <- result ntype,
                TupleT n <- con rty,
                n > 0, -- for whatever reason   $(conE (tupleDataName 0))
                       -- doesn't work when it could just be  $(conE '())
                convTup <- map (maybe [| return |] snd .
                                    flip lookup conv')
                                    (conArgs rty)
                                 = do
                    rs <- replicateM n (newName "r")
                    lamE [tupP (map varP rs)]
                        [| $(foldl (\f x -> [| $f `ap` $x |])
                              [| return $(conE (tupleDataName n)) |]
                              (zipWith (\c r -> [| $c $(varE r)|]) convTup rs))
                        |]
            |   AppT _ nty <- result ntype,
                Just (_,from) <- nty `lookup` conv' = from
            | otherwise = [| return |]

Наконец, соберите обе части вместе в определении функции:

    vs <- replicateM (length ty) (newName "v")

    liftM (:[]) $
        funD (mkName $ "imported_"++nameBase n)
         [clause
            (map varP vs)
            (normalB [| $back =<< join
                        $(foldl (\x y -> [| $x `ap` $y |])
                                [| return $(varE n) |]
                                (zipWith conv ty vs))
                |])
            []]
4 голосов
/ 11 августа 2011

Вот ужасное решение двух типов.Первая часть (названная бесполезно foo) будет принимать такие типы, как Double -> Double -> CString -> IO (), и превращать их в такие, как IO (Double -> IO (Double -> IO (String -> IO ()))).Таким образом, каждое преобразование вынуждается в IO только для того, чтобы все было полностью однородно.

Вторая часть (названная cio для "collapse io") возьмет эти вещи и вытолкнет все IO бит до конца.

class Foo a b | a -> b where
    foo :: a -> b
instance Foo (IO a) (IO a) where
    foo = id
instance Foo a (IO b) => Foo (CString -> a) (IO (String -> IO b)) where
    foo f = return $ \s -> withCString s $ \cs -> foo (f cs)
instance Foo a (IO b) => Foo (Double -> a) (IO (Double -> IO b)) where
    foo f = return $ \s -> foo (f s)

class CIO a b | a -> b where
    cio :: a -> b
instance CIO (IO ()) (IO ()) where
    cio = id
instance CIO (IO b) c => CIO (IO (a -> IO b)) (a -> c) where
    cio f = \a -> cio $ f >>= ($ a)

{-
*Main> let x = foo (undefined :: Double -> Double -> CString -> IO ())
*Main> :t x
x :: IO (Double -> IO (Double -> IO (String -> IO ())))
*Main> :t cio x
cio x :: Double -> Double -> String -> IO ()
-}

Помимо того, что это вообще ужасная вещь, есть два специфических ограничения: во-первых, не может быть написан универсальный экземпляр Foo. Так что для каждого типа, который вы хотите преобразовать, даже если преобразование просто id, вам нужен экземпляр Foo. Второе ограничение заключается в том, что базовый вариант перехвата CIO не может быть записан из-за оболочек IO вокруг всего.работает только для вещей, которые возвращают IO (). Если вы хотите, чтобы это работало для чего-то, возвращающего IO Int, вам нужно также добавить этот экземпляр.

Я подозреваю, что при достаточной работе и некоторых хитростях typeCast эти ограничения могут бытьпреодолеть. Но код достаточно ужасен, как есть, поэтому я бы не рекомендовал его.

0 голосов
/ 11 августа 2011

Это определенно возможно.Обычный подход состоит в том, чтобы создать лямбды для перехода на withCString.Используя ваш пример:

myMarshaller :: (CDouble -> CString -> IO ()) -> CDouble -> String -> IO ()
myMarshaller func cdouble string = ...

withCString :: String -> (CString -> IO a) -> IO a

Внутренняя функция имеет тип CString -> IO a, который в точности соответствует типу после применения CDouble к функции C func.У вас тоже есть CDouble в поле зрения, так что это все, что вам нужно.

myMarshaller func cdouble string =
  withCString string (\cstring -> func cdouble cstring)
...