Священный грааль динамической диспетчеризации

от автора

Большой бедой Узким местом статической типизации являются гетерогенные коллекции и вариадические функции. Поэтому в RPC-библиотеках часто встречается подход, когда входящие данные так и лежат одним ADT-куском, а для методов один такой же плоский тип "[Foo] -> IO Foo", реализации которого копипастят десериализацию/сериализацию, что неудобно и плодит ошибки, в т.ч. рантаймовые.

Решение этой задачи меня беспокоило практически с самого начала практического применения мной хаскеля и, наконец, вчера ночью на меня снизошло вдохновение аж в 6.5 миллиолега и после сеанса гадания на ошибках и беседы с ghci у меня всё получилось.

Допустим мы хотим сделать список/словарь Текст -> Метод. И сразу же облом:

methods :: [(String, ????)] methods = [ ("add", \x y -> return (x + y))           , ("reverse", \s -> return (reverse s))           ] 

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

data BaconBox = forall a. a  methods :: [(String, BaconBox)] 

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

В этом примере будет использоваться «протокол» Read / Show. Не самый лучший, но для остальных всё тоже самое. Типом данных, соответственно будет String.

class Tossable t where     toss :: [String] -> IO String  data BaconBox = forall a. (Tossable a) => BaconBox a 

С таким определением уже понятно даже компилятору, что внутрь коробки можно положить любой тип, для которого определена функция toss, которая… Бах! Тут мы плавно переходим ко второй проблеме. Ведь хочется, чтобы вызываемые методы могли иметь любые аргументы и в любом количестве. Т.е. чтобы в коробку можно было просто взять и положить любой обработчик RPC, для которого определены процедуры маршалинга.

У Олега и на хаскельной вики есть пример кое-чего похожего — printf с любым количеством аргументов любого типа: www.haskell.org/haskellwiki/Varargs. Но это не совсем то. Но и не совсем не то! awesome.gif

Трюк основывается на двух инстансах — базовая форма и свёртывание аргументов.

Базовая форма определяет что же всё-таки делать, когда данные собраны. В ней же опредляется класс выходного типа метода.

instance (Show a) => Tossable (IO a) where     toss [] f = fmap show f     toss _ _ = fail "Избыточное количество аргументов" 

Если все аргументы использованы и у нас на руках есть готовое значние (в данном случае это действие, которое надо выполнить), т.е. к функции применены все аргументы — мы его выполняем и сразу же сериализуем результат. Оставшиеся аргументы можно выбросить, а можно и ругнуться — я предпочитаю кинуть ошибку, чем гадать о корректности.

Для свёртывания аргументов используется весьма интересный инстанс, демонстрирующий во всей красе мощщъ функционального подхода в целом и правильной системы типов в частности. Тут фиксируется класс входных аргументов, гарантирующий [s]успешность[/s] наличие десерилизатора для типа, используемого в вызове RPC метода.

instance (Read a, Tossable t) => Tossable (a -> t) where     toss [] _ = fail "Недостаточно аргументов"     toss (a:as) f = toss as (f (read a)) 

Не смотря на магичность определения, происходящее довольно тривиально. Если наш тип это функция (a -> t), а у нас есть ещё для неё аргументы, то мы десериализуем следующий аргумент в соответствии с типом, который ожидает функция и применяем его к ней.

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

doAnd :: Bool -> Bool -> IO Bool doAnd a b = return (a && b)  doSum3 :: Double -> Double -> Double -> IO Bool doSum3 x y z = return (x + y + z)  main = do     toss [] (doAnd True True) >>= print     toss ["True"] (doAnd True) >>= print     toss ["True", "True"] doAnd >>= print      toss ["42", "2.71828", "3.14159"] doSum3 >>= print  

Et voila! Функция спокойно распаковывает и скармливает аргументы и упаковывает результат.

Остался последний штрих — динамическая диспетчеризация методов из коробок. Для этого добавим в нашу коробку немного метаданных и функцию поиска по ним:

data BaconBox = forall a. (Tossable a) => BaconBox String a  tossBacon :: (Show a) => [BaconBox] -> String -> [String] -> IO a tossBacon [] _ _ = fail "Метод не найден" tossBacon (BaconBox bn bf : bs) name args     | bn == name = toss args bf     | otherwise = tossBacon bs name args 

Специальная функция нужна для того, чтобы у компилятора не взорвался мозг(sic!) при раскрытии экзистенциального контейнера любым способом кроме паттерн-матчинга. FP-world problems…

-- класс, инстансы, функции как раньше  bacon :: [BaconBox] bacon = [ BaconBox "bool.and" ["x", "y"] doAnd         , BaconBox "num.sum3" ["x", "y", "z"] doSum3         ]  main :: IO () main = do     (method:args) <- getArgs     tossBacon method args bacon >>= print 

Итак, у нас получился контейнер, позволяющий совать внутрь функции с произвольным (но строго ограниченым по вводу и выводу) типом и вызывать их по имени, автоматически выполняя рутинную работу по маршалингу. Меньше кода — меньше багов. Ура!

ссылка на оригинал статьи http://habrahabr.ru/post/182576/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *