downcasting
Feb. 5th, 2009 12:15 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Downcasting - операция, как известо, опасная. К сожалению, совсем избавиться от неё нельзя1. Например, в случае, если мы работаем с колбэками, принимающими параметры, типы которых надо понизить в самих колбэках. Или вспомним обычный метод
Но, раз нельзя избавиться, то мы можем по крайней мере постараться снизить его опасность. Для этого необходимо иметь гарантии, что объект имеет нужный нам тип:
или перехватывать исключение
В результате, мы получаем гибкий и безопасный код, проверка у которого вынесена из клиентского кода в сигнатуру. Например, реализуем контроллер, принимающий список обработчиков самых разных типов событий, и вызывающий нужный. Нам потребуется пара расширений.
и один импорт, необходимый для downcasting-а.
Сам обработчик будет принимать любое событие и возвращать некий результат. Обработчик параметризуется типом этого результата:
Сам контроллер должен найти нужный нам обработчик и снизить3 для него тип. Если же обработчик не найден, то позвать обработчик по умолчанию. Искать он будет в списке, поэтому достаточно свернуть этот список по функции, которая попытается откастить событие и в случае неудачи продолжить цепочку.
Несложно и функционально, правда? Мы можем описать конкретные контроллеры. Например, облегченную IO версию с типом IO ().
А вот и пример. У нас есть несколько событий.
И пара обработчиков - для первой пары событий.
Тогда контроллер будет выглядеть так:
Ещё один момент. В данной реализации контроллер может принимать обработчики разных типов. А что, если мы захотим иметь отдельные обработчики для левой и правой кнопки? Или для двойного щелчка и одинарного? В этом случае нам необходимо сообщать в обработчике, обработал ли он событие. Эту функциональность просто добавить, используя
(1) "нельзя" - значит, что у меня код не избавлен совсем от этого запаха.
(2) есть, конечно
(3) в данном случае лучше, наверное, говорить - "уточнить"
equals
в Java.Но, раз нельзя избавиться, то мы можем по крайней мере постараться снизить его опасность. Для этого необходимо иметь гарантии, что объект имеет нужный нам тип:
if (source instanceof Button) { Button btn = (Button) src; ... }
или перехватывать исключение
ClassCastException
. В Haskell, в одном из самых безопасных языков в мире, эти два действия - проверка и downcasting объединены, что делает операцию в целом безопасной2.cast :: (Typeable a, Typeable b) => a -> Maybe b
В результате, мы получаем гибкий и безопасный код, проверка у которого вынесена из клиентского кода в сигнатуру. Например, реализуем контроллер, принимающий список обработчиков самых разных типов событий, и вызывающий нужный. Нам потребуется пара расширений.
> {-# LANGUAGE ExistentialQuantification, DeriveDataTypeable #-}
и один импорт, необходимый для downcasting-а.
> import Data.Typeable (Typeable, cast)
Сам обработчик будет принимать любое событие и возвращать некий результат. Обработчик параметризуется типом этого результата:
> data Listener a = forall e. Typeable e => Listener (e -> a)
Сам контроллер должен найти нужный нам обработчик и снизить3 для него тип. Если же обработчик не найден, то позвать обработчик по умолчанию. Искать он будет в списке, поэтому достаточно свернуть этот список по функции, которая попытается откастить событие и в случае неудачи продолжить цепочку.
> listen atlast listeners event = foldr go (atlast event) listeners > where > go (Listener l) rest = maybe rest l (cast event)
Несложно и функционально, правда? Мы можем описать конкретные контроллеры. Например, облегченную IO версию с типом IO ().
> type ListenerIO = Listener (IO ()) > listenIO :: Typeable e => [ListenerIO] -> e -> IO () > listenIO = listen $ \e -> return ()
А вот и пример. У нас есть несколько событий.
> data KeyboardEvent = Letter Char | Enter > deriving (Typeable) > data MouseEvent = LeftBtn | RightBtn > deriving (Typeable) > data IdleEvent = IdleEvent > deriving (Typeable)
И пара обработчиков - для первой пары событий.
> -- просто, чтобы меньше писать > p = putStrLn > listenKeyboard (Letter x) = p [x] > listenKeyboard Enter = p "Enter" > listenMouse LeftBtn = p "Left" > listenMouse RightBtn = p "Right"
Тогда контроллер будет выглядеть так:
> foo :: Typeable e => e -> IO () > foo = listen (const $ p "Unknown") [Listener listenKeyboard, Listener listenMouse] *Main> foo LeftBtn Left *Main> foo (Letter 'x') x *Main> foo IdleEvent Unknown
Ещё один момент. В данной реализации контроллер может принимать обработчики разных типов. А что, если мы захотим иметь отдельные обработчики для левой и правой кнопки? Или для двойного щелчка и одинарного? В этом случае нам необходимо сообщать в обработчике, обработал ли он событие. Эту функциональность просто добавить, используя
Maybe
. И всё таки жалко, что строки из case
(паттерн -> выражение) не являются first-class в Haskell.(1) "нельзя" - значит, что у меня код не избавлен совсем от этого запаха.
(2) есть, конечно
unsafeCoerse
, но мы не будем его пользовать.(3) в данном случае лучше, наверное, говорить - "уточнить"
no subject
Date: 2009-02-05 04:34 pm (UTC)Шут с ними, с кнопками, я расскажу, как получился этот пост. VladD2 с RSDN высказался в том смысле, что Haskell не обладает компонентностью. В качестве примера он привёл WPF дизайнер. Смысл в том, цитирую "Когда можно в работающем приложении подгружать разные компоненты и получать разное поведение". В работающем! Т.е. классы кнопок дописать мы уже не сможем - это не лисп или питон какой нибудь. Одинаковые обработчики тоже отпадают, потому что речь сейчас не о них. И вот есть у нас две кнопки, к событиям (сигналам) которых мы хотим привязаться. Как это сделать? В Яве это будут две имплементации Listener. Каждая из них учитывает своё событие и может кастить параметры. А потом мы у одной из этих кнопок заменяем обработчик (listener) на другой. С классами типов всё это становится уж очень сложным.
no subject
Date: 2009-02-05 05:18 pm (UTC)Может быть, как-нибудь сяду и напишу на Template Haskell систему объектов, со всеми нужными прибамбасами.
no subject
Date: 2009-02-05 05:27 pm (UTC)Если да, то это к вопросу никаким боком не относится. Мне так кажется.
Я хочу получить библиотечную возможность подключать к потоку событий (который может включать пользовательские события - т.е. типы данных пользователя) самые разные обработчики. Обработчики сами должны определять их ли событие пришло и обрабатываеть его соответствующим образом. Хороший пример - исключения. Есть код, я хочу иметь универсальный catch, которому передаю этот код и свои обработчики, а он сам всё за меня должен порешать. Клиентский код должен быть простым - библиотечный не обязательно.
Т.е. одно из решений - я имею простые функции ConcreteEvent -> Result. Это, по-моему, удобно.
Как может выглядеть такая библиотечка? Я предложил способ и мне было интересно узнать альтернативы. Вы говорите, что они есть, но я пока не пойму, что собственно предлагается.
Сейчас я должен уйти, но ночью (или завтра) обязательно отвечу :-)
no subject
Date: 2009-02-05 05:41 pm (UTC)По-моему, Влад под компонентностью понимает именно это.
no subject
Date: 2009-02-05 11:00 pm (UTC)Dynamic Applications From the Ground Up (http://www.cse.unsw.edu.au/~dons/papers/SC05.html) приводилось на RSDN много раз.
no subject
Date: 2009-02-06 01:44 pm (UTC)