Подтвердить что ты не робот

Политизм подтипа в Haskell

Построение иерархии классов виджета графического интерфейса является в значительной степени стандартным упражнением в объектно-ориентированном программировании. У вас есть своего рода абстрактный класс Widget с абстрактным подклассом для виджетов, который может содержать другие виджеты, а затем у вас есть избыток дополнительных абстрактных классов для виджетов, поддерживающих текстовый дисплей, виджеты, которые поддерживают фокус ввода, виджеты, которые имеют логическое состояние, вплоть до конкретных конкретных классов, таких как кнопки, ползунки, полосы прокрутки, флажки и т.д.

Мой вопрос: каков наилучший способ сделать это в Haskell?

Есть ряд вещей, которые затрудняют создание GUI Haskell, но не являются частью моего вопроса. Выполнение интерактивного ввода-вывода слегка затруднительно в Haskell. Реализация GUI почти всегда означает запись обертки в крайне низкоуровневую библиотеку C или С++. И люди, пишущие такие обертки, имеют тенденцию копировать существующий API дословно (предположительно, так, чтобы кто-нибудь, кто знает обернутую библиотеку, будет чувствовать себя как дома). На данный момент эти проблемы меня не интересуют. Меня интересует только то, как лучше всего моделировать полиморфизм подтипа в Haskell.

Какие свойства мы хотели бы получить от нашей гипотетической библиотеки GUI? Ну, мы хотим, чтобы в любое время можно было добавлять новые типы виджетов. (Другими словами, закрытый набор возможных виджетов не подходит). Мы хотим минимизировать дублирование кода. (Существует много типов виджетов!) В идеале мы хотим, чтобы при необходимости могли оговаривать один конкретный тип виджетов, а также иметь возможность обрабатывать коллекции любого типа виджетов, если это необходимо.

Все вышесказанное, конечно, тривиально в любом уважающем себя языке OO. Но каков наилучший способ сделать это в Haskell? Я могу придумать несколько подходов, но я не уверен, какой из них был бы "лучшим".

4b9b3361

Ответ 1

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

Моделирование иерархии интерфейсов (например, Widget) может выполняться с помощью классов типов. Добавление новых экземпляров возможно, поэтому набор конкретных виджетов открыт. Если вам нужен конкретный список возможных виджетов, то GADT могут быть кратким решением.

Специальная операция с подклассами - это повышение и понижение.

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

Ответ 2

Наличие реальных объектов виджетов - это нечто очень объектно-ориентированное. Обычно используемым методом в функциональном мире является использование функционального реактивного программирования (FRP). Я кратко расскажу о том, как будет выглядеть библиотека виджета в чистом Haskell при использовании FRP.


tl/dr: Вы не обрабатываете "объекты виджета", вместо этого вы обрабатываете коллекции "потоков событий" и не заботитесь о том, какие виджеты или откуда эти потоки.


В FRP существует основное понятие Event a, которое можно рассматривать как бесконечный список [(Time, a)]. Итак, если вы хотите смоделировать счетчик, который подсчитывает, вы должны записать его как [(00:01, 1), (00:02, 4), (00.03, 7), ...], который связывает определенное значение счетчика с заданным временем. Если вы хотите смоделировать кнопку, которая нажата, вы создаете [(00:01, ButtonPressed), (00:02, ButtonReleased), ...]

Там также обычно называется Signal a, который похож на Event a, за исключением того, что смоделированное значение является непрерывным. У вас нет дискретного набора значений в определенное время, но вы можете задать Signal для своего значения, скажем, 00:02:231, и оно даст вам значение 4.754 или что-то еще. Подумайте о сигнале как аналоговом сигнале, подобном сигналу на измерителе сердечного заряда (электрокардиографическое устройство/монитор Холтера) в больнице: это непрерывная линия, которая прыгает вверх и вниз, но никогда не делает "пробела". Окно всегда имеет заголовок, например (но, возможно, это пустая строка), поэтому вы всегда можете задать его значение.


В графической библиотеке на низком уровне будет mouseMovement :: Event (Int, Int) и mouseAction :: Event (MouseButton, MouseAction) или что-то еще. mouseMovement является фактическим выводом мыши USB/PS2, поэтому вы можете получать только разницу позиций как события (например, когда пользователь перемещает мышь вверх, вы получите событие (12:35:235, (0, -5)). Тогда вы сможете "интегрировать" "или, скорее," накапливать "события движения, чтобы получить mousePosition :: Signal (Int, Int), который дал вам абсолютные координаты мыши. mousePosition также может принимать во внимание устройства с абсолютным указателем, такие как сенсорные экраны или события ОС, которые перемещают курсор мыши и т.д.

Аналогично для клавиатуры будет keyboardAction :: Event (Key, Action), а также можно "интегрировать" этот поток событий в keyboardState :: Signal (Key -> KeyState), который позволяет читать состояние ключа в любой момент времени.


Все усложняется, когда вы хотите нарисовать материал на экране и взаимодействовать с виджетами.

Чтобы создать только одно окно, можно было бы вызвать "волшебную функцию":

window :: Event DrawCommand -> Signal WindowIcon -> Signal WindowTitle -> ...
       -> FRP (Event (Int, Int) {- mouse events -},
               Event (Key, Action) {- key events -},
               ...)

Функция была бы волшебной, потому что ей пришлось бы вызывать функции, специфичные для ОС, и создавать окно (если только сама ОС не является FRP, но я сомневаюсь в этом). Вот почему она находится в монаде FRP, потому что она вызывала бы createWindow и setTitle и registerKeyCallback и т.д. В монаде IO за кулисами.

Можно было бы, конечно, сгруппировать все эти значения в структуры данных, чтобы было:

window :: WindowProperties -> ReactiveWidget
       -> FRP (ReactiveWindow, ReactiveWidget)

WindowProperties - это сигналы и события, которые определяют внешний вид и поведение окна (например, если должны быть закрытые кнопки, что должно быть заголовком и т.д.).

ReactiveWidget представляет S & Es, которые являются событиями клавиатуры и мыши, в случае, если вы хотите эмулировать щелчки мыши из своего приложения и Event DrawCommand, который представляет поток вещей, которые вы хотите нарисовать в окне. Эта структура данных является общей для всех виджетов.

ReactiveWindow представляет события, такие как минимизируемое окно и т.д., а вывод ReactiveWidget представляет события мыши и клавиатуры, исходящие извне/пользователя.

Тогда можно было бы создать фактический виджет, скажем, кнопку. У него будет подпись:

button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)

ButtonProperties будет определять цвет/текст/etc кнопки, а ReactiveButton будет содержать, например. a Event ButtonAction и Signal ButtonState, чтобы прочитать состояние кнопки.

Обратите внимание, что функция button является чистой функцией, поскольку она зависит только от чистых значений FRP, таких как события и сигналы.

Если вы хотите группировать виджеты (например, стекировать их по горизонтали), нужно было бы создать, например. а:

horizontalLayout :: HLayoutProperties -> ReactiveWidget
                 -> (ReactiveLayout, ReactiveWidget)

HLayoutProperties будет содержать информацию о размерах границ и ReactiveWidget для содержащихся виджетов. Затем ReactiveLayout содержит [ReactiveWidget] с одним элементом для каждого дочернего виджета.

Каким будет макет, он будет иметь внутренний Signal [Int], определяющий высоту каждого виджета в макете. Затем он получит все события из ввода ReactiveWidget, затем на основе макета раздела выберите выходной ReactiveWidget, чтобы отправить событие, в то же время также преобразуя начало координат, например. события мыши по смещению раздела.


Чтобы продемонстрировать, как этот API будет работать, рассмотрите эту программу:

main = runFRP $ do rec -- Recursive do, lets us use winInp lazily before it is defined

  -- Create window:
  (win, winOut) <- window winProps winInp

      -- Create some arbitrary layout with our 2 widgets:
  let (lay, layOut) = layout (def { widgets = [butOut, labOut] }) layInp
      -- Create a button:
      (but, butOut) = button butProps butInp
      -- Create a label:
      (lab, labOut) = label labProps labInp
      -- Connect the layout input to the window output
      layInp = winOut
      -- Connect the layout output to the window input
      winInp = layOut
      -- Get the spliced input from the layout
      [butInp, layInp] = layoutWidgets lay
      -- "pure" is of course from Applicative Functors and indicates a constant Signal
      winProps = def { title = pure "Hello, World!", size = pure (800, 600) }
      butProps = def { title = pure "Click me!" }
      labProps = def { text = reactiveIf
                              (buttonPressed but)
                              (pure "Button pressed") (pure "Button not pressed") }
  return ()

(def от Data.Default в data-default)

Это создает граф событий так:

     Input events ->            Input events ->
win ---------------------- lay ---------------------- but \
     <- Draw commands etc.  \   <- Draw commands etc.      | | Button press ev.
                             \  Input events ->            | V
                              \---------------------- lab /
                                <- Draw commands etc.

Обратите внимание, что в любом месте не должно быть никаких "объектов виджета". Макет - это просто функция, которая преобразует входные и выходные события в соответствии с системой секционирования, поэтому вы можете использовать потоки событий, к которым вы получаете доступ к виджетам, или вы можете позволить другой подсистеме полностью генерировать потоки. То же самое касается кнопок и меток: это просто функции, которые конвертируют события click в команды рисования или подобные вещи. Это представление полной развязки и очень гибкое по своей природе.

Ответ 3

wxHaskell Библиотека GUI отлично использует типы phantom для моделирования иерархии виджетов.

Идея такова: все виджеты используют одну и ту же реализацию, а именно - внешние указатели на объекты С++. Однако это не означает, что все виджеты должны иметь один и тот же тип. Вместо этого мы можем построить такую ​​иерархию:

type Object a = ForeignPtr a

data CWindow a
data CControl a
data CButton a

type Window  a = Object  (CWindow a)
type Control a = Window  (CControl a)
type Button  a = Control (CButton a)

Таким образом, значение типа Control A также соответствует типу Window b, поэтому вы можете использовать элементы управления как окна, но не наоборот. Как вы можете видеть, подтипирование реализуется через параметр вложенного типа.

Подробнее об этом методе см. в разделе 5 в статье Дан Лейен о wxHaskell.


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

В частности, наблюдение заключается в том, что объектная ориентация может быть смоделирована путем включения методов в тип данных, например, это

data CWindow a = CWindow
    { close   :: IO ()
    , ...
    }
data CButton a = CButton
    { onClick :: (Mouse -> IO ()) -> IO ()
    , ...
    }

Подтипирование может сохранить некоторые шаблоны здесь, но это не требуется.