downcasting

Feb. 5th, 2009 12:15 pm
lomeo: (лямбда)
[personal profile] lomeo
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) в данном случае лучше, наверное, говорить - "уточнить"

Date: 2009-02-05 12:25 pm (UTC)
From: [identity profile] nealar.livejournal.com
1. Кажется, такой обработчик есть в Data.Typeable.
2. В твоих примерах неясно, зачем нужен апкаст/даункаст. Почему нельзя перенумеровать все возможные варианты и сунуть в один тип.
3. Как раз позавчера читал про Typeable, и стало очень интересно: Как оно устроено "внутре"?

Date: 2009-02-05 12:34 pm (UTC)
From: [identity profile] lomeo.livejournal.com
1. Будем посмотреть

2. Не масштабируемо. Тогда нельзя будет создавать свои ивенты.

3. Очень просто :-) Важен только параметр класса типов - из него достаётся информация о типе. С год или два назад я читал где то статью про deriving Typeable, там был алгоритм даже расписан по конструированию инстансов. Что касается cast, то его можно нарисовать прямо в Haskell без FFI.

Date: 2009-02-05 12:40 pm (UTC)
From: [identity profile] nealar.livejournal.com
1. Под "такой обработчик" подразумевалось контроллер, принимающий список обработчиков самых разных типов событий, и вызывающий нужный.
3. Не понял. У нас есть значение типа a, с ним в нужные функции передаётся instance Typeable a, как из этого инстанса достать информацию, шоп скастовать это значение к типу b?

Date: 2009-02-05 12:44 pm (UTC)
From: [identity profile] lomeo.livejournal.com
1. Не нашёл - покажи пожалуйста.

3. Это у меня каша в голове. Даже не понимаю, почему я так считал!

Date: 2009-02-05 12:52 pm (UTC)
From: [identity profile] nealar.livejournal.com
http://www.haskell.org/ghc/dist/current/docs/libraries/base/Control-Exception.html#v%3Acatches
Ошибсо малость. :)

Date: 2009-02-05 12:55 pm (UTC)
From: [identity profile] lomeo.livejournal.com
Угу, точно, значит нужен ещё catchesMaybe ;-)

Date: 2009-02-05 12:59 pm (UTC)
From: [identity profile] palm-mute.livejournal.com
>У нас есть значение типа a, с ним в нужные функции передаётся instance Typeable a, как из этого инстанса достать информацию, шоп скастовать это значение к типу b?

Примерно так:
{-# LANGUAGE ScopedTypeVariables #-}
import Data.Typeable
import Unsafe.Coerce

mycast :: forall a b . (Typeable a, Typeable b) => a -> Maybe b
mycast x
    | typeOf x == typeOf (undefined :: b) = Just (unsafeCoerce x)
    | otherwise = Nothing

Date: 2009-02-05 01:01 pm (UTC)
From: [identity profile] lomeo.livejournal.com
Ну, имелось в виду средствами языка, без FFI. unsafeCoerse - нечестно.

Date: 2009-02-05 01:13 pm (UTC)
From: [identity profile] palm-mute.livejournal.com
Посмотрел в исходники (http://darcs.haskell.org/packages/base/Data/Typeable.hs), именно так и делают. Конечно, без unsafeCoerce - никак.

Date: 2009-02-14 10:08 pm (UTC)
From: (Anonymous)
2. data Event ext = KeyboardEvent KeyboardEvent | MouseEvent MouseEvent | Extension ext

Date: 2009-02-07 01:14 am (UTC)
From: [identity profile] justbulat.livejournal.com
2. обработка исключений. в 6.10 оно так и сделано, можешь почитать статью SM об extensible exceptions. gui-тулкиты туда же

3. тип запоминают, а ссылку из универсального Dynamic в правильный делают с помощью unsafeCoerce (грязного кастинга типа откуда угодно куда угодно)

Date: 2009-02-05 02:08 pm (UTC)
From: [identity profile] anonymous8216.livejournal.com
но можно и без даункастинга. например, в объектно-ориентированном стиле. либо просто с type classes, либо вооружиться existential types и соорудить настоящие объекты.

Date: 2009-02-05 02:34 pm (UTC)
From: [identity profile] lomeo.livejournal.com
Э, не очень понял. Как без даункастинга? Можно примеры?

Вот
listenKeyboard (Letter x) = p [x]

Нам нужен именно KeyboardEvent, а приходит нам просто (Typeable e => e).

И настоящиее объекты как соорудить?

Date: 2009-02-05 03:12 pm (UTC)
From: [identity profile] anonymous8216.livejournal.com
В этом подходе Typeable не используется. Нужно завести класс Event, обработчик listen будет его методом (в данном случае единственным).

class Event a where
  listen :: a -> IO ()
instance Event KeyboardEvent where ...
instance Event MouseEvent where ...


Теперь есть два пути. Первый — без построения объектов. Вместо того, чтобы класть в список события, положим туда события вместе с обработчиками:

[listen (Letter x), listen LeftBtn]

Ничего другого ведь мы с событиями делать не собираемся, правильно? Здесь нам помогает ленивость, а в энергичном языке мы бы положили отложенный вызов (\() -> listen (Letter x)).

Если у класса несколько методов, и мы хотим таскать туда-сюда списки объектов, не говоря заранее, какой метод мы собираемся применять, можно воспользоваться экзистенциальными типами. Но здесь я покажу опять на примере события, с единственным методом. Пусть опять определен класс Event и его инстансы. Теперь так:

{-# LANGUAGE ExistentialQuantification #-}


class Event a where
  listen :: a -> IO ()

data EventObj = forall a . (Event a) => EventObj a

instance Event EventObj where
   listen (EventObj a) = listen a


Теперь можно класть в список события

x = [EventObj LeftBtn, EventObj IdleEvent]

и вызывать для каждого из них listen.

Компилятор Хаскеля невидимо для программиста строит из EventObj натуральный объект, очень похожий на объект из Java или C++: данные (типа "некий a класса Event") плюс словарь методов (все методы класса Event для данного a) — то, что на ОО-жаргоне называется vtable.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Profile

lomeo: (Default)
Dmitry Antonyuk

April 2024

S M T W T F S
 123456
7891011 1213
14151617181920
21222324252627
282930    

Style Credit

Expand Cut Tags

No cut tags
Page generated Jun. 23rd, 2025 04:11 pm
Powered by Dreamwidth Studios