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