lomeo: (лямбда)
Dmitry Antonyuk ([personal profile] lomeo) wrote2009-02-05 12:15 pm

downcasting

Downcasting - операция, как известо, опасная. К сожалению, совсем избавиться от неё нельзя1. Например, в случае, если мы работаем с колбэками, принимающими параметры, типы которых надо понизить в самих колбэках. Или вспомним обычный метод 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) в данном случае лучше, наверное, говорить - "уточнить"

[identity profile] lomeo.livejournal.com 2009-02-05 03:18 pm (UTC)(link)
> Если у класса несколько методов, и мы хотим таскать туда-сюда списки объектов, не говоря заранее, какой метод мы собираемся применять

Вот именно, поэтому первый метод не проходит - у нас ещё нет событий, но есть обработчики для разных типов событий. Мы не можем написать [listen (Letter x), listen LeftBtn], потому что событий Letter/LeftBtn ещё нет.

Что касается второго метода, то опять же - получается, что у нас всего один listen на один тип a. Но ведь для разных условий мы можем по разному обрабатывать одно и то же событие? Классы типов - очень жёсткое решение в этом случае. Чтобы это обойти придётся заводить классы под разные задачи, а это уровень кода, значит рантайм формировать списки обработчиков мы тоже не сможем.

[identity profile] anonymous8216.livejournal.com 2009-02-05 03:52 pm (UTC)(link)
1. Когда появится событие Letter x, мы захотим добавить его в класс Event, и тут-то нам придется написать для него реализацию listen.

2. В графических программах классы под разные задачи уже есть, они называются — окно, кнопка, текстовое поле и т.д. Есть иерархия типов, начинающаяся с Widget, всяческие окна и кнопки — его подтипы, и у каждого есть методы onKeyPress, onMouseMove и так далее. Обработчик событий от мыши тогда будет всегда одинаковый — найти виджет, внутри которого (геометрически) произошло событие, и вызвать его метод onMouseMove. Что происходит, когда появляется новое событие, кажем, ScreenTouch, не предусмотренное виджетами? Во-первых, можно добавить дефолтный пустой onScreenTouch в корневой тип, и все его унаследуют, а в нужных случаях (когда действительно требуется реакция) мы его переопределим. Во-вторых, можно завести отдельную иерархию виджетов, умеющих отвечать на ScreenTouch, и отдельный список таких объектов. Если нам в каком-нибудь текстовом поле нужна нестандартная обработка какого-нибудь mouse move, мы просто делаем новый виджет "нестандартное текстовое поле".

2а. В других случаях мы вводим для класса несколько фундаментальных методов (с разными реализациями для разных инстансов, разумеется), надеясь, что из них можно скомбинировать любую желаемую операцию. Например, в классическом примере графического редактора имеется класс Shape, у которого есть методы draw, translate, rotate, scale и так далее.

[identity profile] lomeo.livejournal.com 2009-02-05 03:56 pm (UTC)(link)
Ну не может у двух кнопок быть один обработчик событий! Они же разные вещи делают - эти кнопки. Одна окно закрывает, а другая, скажем, ракеты запускает. А класс у кнопок один. и onClick один, просто к этому onClick разные обработчики (listeners) подвязаны.

Или я чего то не понимаю.

[identity profile] anonymous8216.livejournal.com 2009-02-05 04:27 pm (UTC)(link)
Во-первых, две разные кнопки совершенно запросто могут быть двух разных классов, примеры таких библиотек немногочисленны, но имеются. Еще во-первых, и обработчики могут быть одинаковые, например, взять хранящуюся в кнопке команду и поставить в очередь на выполнение (а у команд уже обработчики будут разные). Во-вторых, положим, у кнопок обработчики разные, а у текстовых полей, например, одинаковые. Разве это важно? Важно, что те и другие выставляют наружу одинаковый интерфейс.

[identity profile] lomeo.livejournal.com 2009-02-05 04:34 pm (UTC)(link)
Так или я чего то не понимаю, или одно из двух :-)

Шут с ними, с кнопками, я расскажу, как получился этот пост. VladD2 с RSDN высказался в том смысле, что Haskell не обладает компонентностью. В качестве примера он привёл WPF дизайнер. Смысл в том, цитирую "Когда можно в работающем приложении подгружать разные компоненты и получать разное поведение". В работающем! Т.е. классы кнопок дописать мы уже не сможем - это не лисп или питон какой нибудь. Одинаковые обработчики тоже отпадают, потому что речь сейчас не о них. И вот есть у нас две кнопки, к событиям (сигналам) которых мы хотим привязаться. Как это сделать? В Яве это будут две имплементации Listener. Каждая из них учитывает своё событие и может кастить параметры. А потом мы у одной из этих кнопок заменяем обработчик (listener) на другой. С классами типов всё это становится уж очень сложным.

[identity profile] anonymous8216.livejournal.com 2009-02-05 05:18 pm (UTC)(link)
Как вообще в Хаскеле нынче реализована динамическая загрузка? Если скажете, я, наверное, смогу продемонстрировать работающий пример. Или не смогу ;)

Может быть, как-нибудь сяду и напишу на Template Haskell систему объектов, со всеми нужными прибамбасами.

[identity profile] lomeo.livejournal.com 2009-02-05 05:27 pm (UTC)(link)
Динамическая загрузка типа hs-plugins (http://www.cse.unsw.edu.au/~dons/hs-plugins/index.html)?

Если да, то это к вопросу никаким боком не относится. Мне так кажется.

Я хочу получить библиотечную возможность подключать к потоку событий (который может включать пользовательские события - т.е. типы данных пользователя) самые разные обработчики. Обработчики сами должны определять их ли событие пришло и обрабатываеть его соответствующим образом. Хороший пример - исключения. Есть код, я хочу иметь универсальный catch, которому передаю этот код и свои обработчики, а он сам всё за меня должен порешать. Клиентский код должен быть простым - библиотечный не обязательно.

Т.е. одно из решений - я имею простые функции ConcreteEvent -> Result. Это, по-моему, удобно.

Как может выглядеть такая библиотечка? Я предложил способ и мне было интересно узнать альтернативы. Вы говорите, что они есть, но я пока не пойму, что собственно предлагается.

Сейчас я должен уйти, но ночью (или завтра) обязательно отвечу :-)

[identity profile] palm-mute.livejournal.com 2009-02-05 05:41 pm (UTC)(link)
>Если да, то это к вопросу никаким боком не относится. Мне так кажется.
По-моему, Влад под компонентностью понимает именно это.

[identity profile] lomeo.livejournal.com 2009-02-05 11:00 pm (UTC)(link)
Ну, тогда вообще проблем не вижу:
Dynamic Applications From the Ground Up (http://www.cse.unsw.edu.au/~dons/papers/SC05.html) приводилось на RSDN много раз.

[identity profile] anonymous8216.livejournal.com 2009-02-06 01:44 pm (UTC)(link)
понятно. с исключениями, похоже, Typeable самый лучший вариант.

[identity profile] justbulat.livejournal.com 2009-02-07 01:19 am (UTC)(link)
проблема в том, что existential хранит только тот интерфейс, который ты явно задал. представь, что у тебя есть список компонент на форме, и ты хочешь очистить все EditBox. а не получится - твой список existential хранит только интерфейс класса GUIObject, в котором нет методов, специфичных для едитбоксов