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 12:25 pm (UTC)2. В твоих примерах неясно, зачем нужен апкаст/даункаст. Почему нельзя перенумеровать все возможные варианты и сунуть в один тип.
3. Как раз позавчера читал про Typeable, и стало очень интересно: Как оно устроено "внутре"?
no subject
Date: 2009-02-05 12:34 pm (UTC)2. Не масштабируемо. Тогда нельзя будет создавать свои ивенты.
3. Очень просто :-) Важен только параметр класса типов - из него достаётся информация о типе. С год или два назад я читал где то статью про deriving Typeable, там был алгоритм даже расписан по конструированию инстансов. Что касается cast, то его можно нарисовать прямо в Haskell без FFI.
no subject
Date: 2009-02-05 12:40 pm (UTC)3. Не понял. У нас есть значение типа a, с ним в нужные функции передаётся instance Typeable a, как из этого инстанса достать информацию, шоп скастовать это значение к типу b?
no subject
Date: 2009-02-05 12:44 pm (UTC)3. Это у меня каша в голове. Даже не понимаю, почему я так считал!
no subject
Date: 2009-02-05 12:52 pm (UTC)Ошибсо малость. :)
no subject
Date: 2009-02-05 12:55 pm (UTC)no subject
Date: 2009-02-05 12:59 pm (UTC)Примерно так:
no subject
Date: 2009-02-05 01:01 pm (UTC)no subject
Date: 2009-02-05 01:13 pm (UTC)no subject
Date: 2009-02-14 10:08 pm (UTC)no subject
Date: 2009-02-07 01:14 am (UTC)3. тип запоминают, а ссылку из универсального Dynamic в правильный делают с помощью unsafeCoerce (грязного кастинга типа откуда угодно куда угодно)
no subject
Date: 2009-02-05 02:08 pm (UTC)no subject
Date: 2009-02-05 02:34 pm (UTC)Вот
Нам нужен именно
KeyboardEvent
, а приходит нам просто (Typeable e => e).И настоящиее объекты как соорудить?
no subject
Date: 2009-02-05 03:12 pm (UTC)Теперь есть два пути. Первый — без построения объектов. Вместо того, чтобы класть в список события, положим туда события вместе с обработчиками:
[listen (Letter x), listen LeftBtn]
Ничего другого ведь мы с событиями делать не собираемся, правильно? Здесь нам помогает ленивость, а в энергичном языке мы бы положили отложенный вызов (\() -> listen (Letter x)).
Если у класса несколько методов, и мы хотим таскать туда-сюда списки объектов, не говоря заранее, какой метод мы собираемся применять, можно воспользоваться экзистенциальными типами. Но здесь я покажу опять на примере события, с единственным методом. Пусть опять определен класс Event и его инстансы. Теперь так:
Теперь можно класть в список события
x = [EventObj LeftBtn, EventObj IdleEvent]
и вызывать для каждого из них listen.
Компилятор Хаскеля невидимо для программиста строит из EventObj натуральный объект, очень похожий на объект из Java или C++: данные (типа "некий a класса Event") плюс словарь методов (все методы класса Event для данного a) — то, что на ОО-жаргоне называется vtable.
no subject
Date: 2009-02-05 03:18 pm (UTC)Вот именно, поэтому первый метод не проходит - у нас ещё нет событий, но есть обработчики для разных типов событий. Мы не можем написать [listen (Letter x), listen LeftBtn], потому что событий Letter/LeftBtn ещё нет.
Что касается второго метода, то опять же - получается, что у нас всего один listen на один тип a. Но ведь для разных условий мы можем по разному обрабатывать одно и то же событие? Классы типов - очень жёсткое решение в этом случае. Чтобы это обойти придётся заводить классы под разные задачи, а это уровень кода, значит рантайм формировать списки обработчиков мы тоже не сможем.
no subject
Date: 2009-02-05 03:52 pm (UTC)2. В графических программах классы под разные задачи уже есть, они называются — окно, кнопка, текстовое поле и т.д. Есть иерархия типов, начинающаяся с Widget, всяческие окна и кнопки — его подтипы, и у каждого есть методы onKeyPress, onMouseMove и так далее. Обработчик событий от мыши тогда будет всегда одинаковый — найти виджет, внутри которого (геометрически) произошло событие, и вызвать его метод onMouseMove. Что происходит, когда появляется новое событие, кажем, ScreenTouch, не предусмотренное виджетами? Во-первых, можно добавить дефолтный пустой onScreenTouch в корневой тип, и все его унаследуют, а в нужных случаях (когда действительно требуется реакция) мы его переопределим. Во-вторых, можно завести отдельную иерархию виджетов, умеющих отвечать на ScreenTouch, и отдельный список таких объектов. Если нам в каком-нибудь текстовом поле нужна нестандартная обработка какого-нибудь mouse move, мы просто делаем новый виджет "нестандартное текстовое поле".
2а. В других случаях мы вводим для класса несколько фундаментальных методов (с разными реализациями для разных инстансов, разумеется), надеясь, что из них можно скомбинировать любую желаемую операцию. Например, в классическом примере графического редактора имеется класс Shape, у которого есть методы draw, translate, rotate, scale и так далее.
no subject
Date: 2009-02-05 03:56 pm (UTC)Или я чего то не понимаю.
no subject
Date: 2009-02-05 04:27 pm (UTC)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)no subject
Date: 2009-02-07 01:19 am (UTC)