haskell polymorphism

Polimorfismo de subtipo en Haskell



polymorphism (3)

Crear una jerarquía de clases de widgets GUI es más o menos un ejercicio estándar en programación orientada a objetos. Tienes algún tipo de clase de Widget abstracto, con una subclase abstracta para widgets que pueden contener otros widgets, y luego tienes una profusión de más clases abstractas para widgets que admiten visualización de texto, widgets que admiten ser el foco de entrada, widgets que tienen un estado booleano, hasta clases concretas reales como botones, controles deslizantes, barras de desplazamiento, casillas de verificación, etc.

Mi pregunta es: ¿Cuál es la mejor manera de hacer esto en Haskell?

Hay varias cosas que dificultan la construcción de una GUI de Haskell, pero no son parte de mi pregunta. Hacer E / S interactivas es algo complicado en Haskell. La implementación de una GUI casi siempre significa escribir una envoltura en una biblioteca C o C ++ de muy bajo nivel. Y las personas que escriben tales envoltorios tienden a copiar el código API existente (presumiblemente para que cualquiera que conozca la biblioteca envuelta se sienta como en casa). Estos problemas no me interesan en este momento. Me interesa puramente la mejor manera de modelar el polimorfismo de subtipo en Haskell.

¿Qué tipo de propiedades queremos de nuestra biblioteca de GUI hipotética? Bueno, queremos que sea posible agregar nuevos tipos de widgets en cualquier momento. (En otras palabras, un conjunto cerrado de widgets posibles no es bueno.) Queremos minimizar la duplicación de código. (¡Hay muchos tipos de widgets!) Lo ideal es que podamos estipular un tipo específico de widgets cuando sea necesario, pero también que podamos manejar colecciones de cualquier tipo de widgets si es necesario.

Todo lo anterior es, por supuesto, trivial en cualquier idioma de OO que se precie. ¿Pero cuál es la mejor manera de hacer esto en Haskell? Puedo pensar en varios enfoques, pero no estoy seguro de cuál sería el "mejor".


Para entender qué OOP, como el polimorfismo de subtipo, se puede hacer en Haskell, puede observar OOHaskell . Esto reproduce la semántica de una variedad de poderosos sistemas de tipo OOP, manteniendo la mayoría de las inferencias de tipo. La codificación de datos real no se estaba optimizando, pero sospecho que las familias de tipos podrían permitir mejores presentaciones.

El modelado de la jerarquía de la interfaz (por ejemplo, Widget) se puede hacer con clases de tipo. Es posible agregar nuevas instancias, por lo que el conjunto de widgets concretos está abierto. Si desea una lista específica de posibles widgets, entonces GADT puede ser una solución sucinta.

La operación especial con subclases es upcasting y downcasting.

Esto primero se necesita para tener una colección de Widgets, y el resultado habitual es usar tipos existenciales. Hay otras soluciones interesantes si lee todos los bits de la biblioteca HList . El upcasting es bastante fácil y el compilador puede estar seguro de que todos los lanzamientos son válidos en el momento de la compilación. El downcasting es intrínsecamente dinámico y requiere algún tipo de soporte de información de tipo de tiempo de ejecución, generalmente Data.Typeable. Dado algo parecido a Typeable, el downcasting es solo otra clase de tipo, con el resultado envuelto en Maybe para indicar el fracaso.

Hay un texto estándar asociado con la mayor parte de esto, pero QusiQuoting y Templating pueden reducir esto. La inferencia tipo todavía puede funcionar en gran medida.

No he explorado los nuevos tipos y tipos de Restricción, pero aumentan la solución existencial para la difusión ascendente y descendente.


Tener objetos de widgets reales es algo que está muy orientado a objetos. Una técnica comúnmente utilizada en el mundo funcional es utilizar en su lugar la Programación Reactiva Funcional (FRP). Brevemente describiré cómo se vería una biblioteca de widgets en Haskell puro cuando use FRP.

tl / dr: no maneja "Objetos de widget", maneja colecciones de "secuencias de eventos" y no le importa de qué widgets o de dónde provienen.

En FRP, existe la noción básica de un Event a , que puede verse como una lista infinita [(Time, a)] . Por lo tanto, si desea modelar un contador que cuente hacia arriba, lo escribiría como [(00:01, 1), (00:02, 4), (00.03, 7), ...] , que asocia un [(00:01, 1), (00:02, 4), (00.03, 7), ...] valor de contador específico con un tiempo dado. Si desea modelar un botón que se está presionando, produce un [(00:01, ButtonPressed), (00:02, ButtonReleased), ...]

También suele haber algo llamado Signal a , que es como un Event a , excepto que el valor modelado es continuo. No tiene un conjunto discreto de valores en momentos específicos, pero puede pedirle a la Signal su valor en, digamos, 00:02:231 y le dará el valor 4.754 o algo así. Piense en una señal como una señal analógica, como la que se encuentra en un medidor de carga cardíaca (dispositivo electrocardiográfico / monitor Holter) en un hospital: es una línea continua que salta hacia arriba y hacia abajo, pero nunca forma un "espacio". Una ventana siempre tiene un título, por ejemplo (pero tal vez es la cadena vacía), por lo que siempre puede solicitar su valor.

En una biblioteca de GUI, en un nivel bajo, habría un mouseMovement :: Event (Int, Int) y mouseAction :: Event (MouseButton, MouseAction) o algo así. El mouseMovement es la salida real del mouse USB / PS2, por lo que solo se obtienen diferencias de posición como eventos (por ejemplo, cuando el usuario mueve el mouse, obtendría el evento (12:35:235, (0, -5)) . Luego, podría "integrar" o más bien "acumular" los eventos de movimiento para obtener un mousePosition :: Signal (Int, Int) que le proporcione coordenadas absolutas del mouse. mousePosition también podría tener en cuenta dispositivos mousePosition absolutos como pantallas táctiles , o eventos de SO que cambian la posición del cursor del mouse, etc.

Del mismo modo para un teclado, habría un keyboardAction :: Event (Key, Action) , y también podría "integrarse" ese evento en un keyboardState :: Signal (Key -> KeyState) que le permite leer el estado de una tecla en cualquier punto en el tiempo.

Las cosas se vuelven más complicadas cuando quieres dibujar cosas en la pantalla e interactuar con widgets.

Para crear solo una ventana, uno tendría una "función mágica" llamada:

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

La función sería mágica porque tendría que llamar a las funciones específicas del sistema operativo y crear una ventana (a menos que el sistema operativo en sí sea FRP, pero lo dudo). Esa también es la razón por la cual está en la mónada de FRP , porque llamaría a createWindow y setTitle y registerKeyCallback etc., en la setTitle IO detrás de las escenas.

Uno podría, por supuesto, agrupar todos esos valores en estructuras de datos para que haya:

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

Los WindowProperties son señales y eventos que determinan el aspecto y el comportamiento de la ventana (por ejemplo, si debe haber botones cercanos, cuál debe ser el título, etc.).

El ReactiveWidget representa S & E que son eventos de teclado y mouse, en caso de que desee emular clics del mouse desde su aplicación, y un Event DrawCommand que representa una secuencia de cosas que desea dibujar en la ventana. Esta estructura de datos es común a todos los widgets.

ReactiveWindow representa eventos como la ventana que se minimiza, etc., y el ReactiveWidget salida representa eventos de mouse y teclado que vienen del exterior / del usuario.

Entonces uno crearía un widget real, digamos un botón. Tendría la firma:

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

ButtonProperties determinaría el color / texto / etc. del botón, y ReactiveButton contendría, por ejemplo, un Event ButtonAction y Signal ButtonState para leer el estado del botón.

Tenga en cuenta que la función del button es una función pura, ya que solo depende de los valores puros de FRP, como los eventos y las señales.

Si uno quiere agrupar widgets (por ejemplo, apilarlos horizontalmente), uno debería crear, por ejemplo, a:

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

HLayoutProperties contendría información sobre los tamaños de los bordes y los ReactiveWidget para los widgets contenidos. El ReactiveLayout contendría un [ReactiveWidget] con un elemento para cada widget hijo.

Lo que el diseño haría es que tendría una Signal [Int] interna Signal [Int] que determina la altura de cada widget en el diseño. Luego recibiría todos los eventos de la entrada de ReactiveWidget , luego, basándose en el diseño de la partición, seleccionará un ReactiveWidget salida para enviar el evento, mientras tanto, también se transformará el origen de, por ejemplo, eventos de mouse por el desplazamiento de partición.

Para demostrar cómo funcionaría esta API, considere este programa:

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 es de Data.Default en data-default )

Esto crea un gráfico de eventos, así:

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

Tenga en cuenta que no tiene que haber ningún "objeto widget" en ninguna parte. Un diseño es simplemente una función que transforma eventos de entrada y salida de acuerdo con un sistema de particionamiento, por lo que podría usar las secuencias de eventos a las que acceda para widgets, o podría permitir que otro subsistema genere las secuencias por completo. Lo mismo ocurre con los botones y las etiquetas: son simplemente funciones que convierten eventos de clics en comandos de dibujo o cosas similares. Es una representación del desacople completo, y muy flexible en su naturaleza.


La biblioteca wxHaskell GUI hace un excelente uso de los tipos fantasma para modelar una jerarquía de widgets.

La idea es la siguiente: todos los widgets comparten la misma implementación, es decir, son punteros externos a objetos C ++. Sin embargo, esto no significa que todos los widgets deben tener el mismo tipo. En cambio, podemos construir una jerarquía como esta:

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)

De esta forma, un valor del tipo Control A también coincide con el tipo Window b , por lo que puede usar controles como ventanas, pero no al revés. Como puede ver, la subtipificación se implementa a través de un parámetro de tipo anidado.

Para más información sobre esta técnica, ver la sección 5 en el artículo de Dan Leijen sobre wxHaskell .

Tenga en cuenta que esta técnica parece estar limitada al caso en que la representación real de widgets es uniforme, es decir, siempre es la misma. Sin embargo, estoy seguro de que con un poco de pensamiento, se puede extender al caso donde los widgets tienen representaciones diferentes.

En particular, la observación es que la orientación a objetos se puede modelar incluyendo los métodos en el tipo de datos, como este

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

La subtipificación puede ahorrar algunos puntos repetitivos aquí, pero no es obligatorio.