Mat*_*hid 31 polymorphism haskell
构建GUI小部件类的层次结构几乎是面向对象编程的标准练习.你有一些抽象Widget类,有一个可以包含其他小部件的小部件的抽象子类,然后你有大量的支持文本显示的小部件的抽象类,支持作为输入焦点的小部件,具有布尔值的小部件状态,直到实际的具体类,如按钮,滑块,滚动条,复选框等.
我的问题是:在Haskell中执行此操作的最佳方法是什么?
有许多因素使构建Haskell GUI变得困难,但不是我的问题的一部分.在Haskell中进行交互式I/O有点棘手.实现GUI几乎总是意味着将包装器写入极低级别的C或C++库.编写这样的包装器的人倾向于逐字复制现有的API(可能是任何知道包装库的人都会有宾至如归的感觉).这些问题目前我不感兴趣.我真的很感兴趣的是如何最好地模拟Haskell中的子类型多态.
我们假设的GUI库需要什么样的属性?好吧,我们希望可以随时添加新的窗口小部件类型.(换句话说,一组封闭的可能小部件并不好.)我们希望最大限度地减少代码重复.(有很多小部件类型!)理想情况下,我们希望能够在必要时规定一个特定的小部件类型,但也能够在需要时处理任何小部件类型的集合.
在任何自尊的OO语言中,上述所有内容当然都是微不足道的.但是在Haskell中执行此操作的最佳方法是什么?我可以想到几种方法,但我不确定哪种方法会"最好".
dfl*_*str 30
拥有实际的小部件对象是非常面向对象的.功能性世界中常用的技术是使用功能反应编程(FRP).我将简要概述使用FRP时纯Haskell中的小部件库的样子.
tl/dr:您不处理"Widget对象",而是处理"事件流"的集合,而不关心哪些小部件或这些流来自何处.
在FRP中,有一个基本概念Event a,可以看作是无限列表[(Time, a)].因此,如果您想要计算一个计数的计数器,您可以将其写为[(00:01, 1), (00:02, 4), (00.03, 7), ...],将特定计数器值与给定时间相关联.如果要对正在按下的按钮进行建模,则会产生一个[(00:01, ButtonPressed), (00:02, ButtonReleased), ...]
除了建模值是连续的之外,通常还有一种称为a的东西Signal a,就像a 一样Event a.在特定时间,您没有一组离散的值,但是您可以在其中询问Signal它的值,00:02:231并且它将为您提供值4.754或其他值.可以将信号看作模拟信号,就像医院心脏电荷计(心电图设备/动态心电图监护仪)上的模拟信号一样:它是一条连续的线路,可以上下跳跃但从不会产生"间隙".例如,一个窗口总是有一个标题(但也许它是空字符串),因此您可以随时询问它的值.
在GUI库,在较低的水平,有好多是mouseMovement :: Event (Int, Int)和mouseAction :: Event (MouseButton, MouseAction)什么的.这mouseMovement是实际的USB/PS2鼠标输出,因此您只能将位置差异视为事件(例如,当用户将鼠标向上移动时,您将获得该事件(12:35:235, (0, -5)).然后您可以"集成"或更确切地说"累积"获得mousePosition :: Signal (Int, Int)绝对鼠标坐标的移动事件mousePosition也可以考虑绝对指向设备,如触摸屏,或重新定位鼠标光标的OS事件等.
类似地,对于键盘,有一个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 -},
...)
Run Code Online (Sandbox Code Playgroud)
该函数将是神奇的,因为它必须调用特定于操作系统的函数并创建一个窗口(除非操作系统本身是FRP,但我对此表示怀疑).这也是为什么它在FRP单子,因为它会调用createWindow并setTitle与registerKeyCallback在等IO幕后单子.
当然,人们可以将所有这些值分组到数据结构中,以便:
window :: WindowProperties -> ReactiveWidget
-> FRP (ReactiveWindow, ReactiveWidget)
Run Code Online (Sandbox Code Playgroud)
的WindowProperties是信号和确定窗口的外观和行为的事件(例如,如果应该有关闭按钮,标题应该是什么,等等).
该ReactiveWidget代表S&居是键盘和鼠标事件,如果你想从你的应用程序中模拟鼠标点击,以及Event DrawCommand代表您要绘制窗口上的东西流.此数据结构对所有小部件都是通用的.
的ReactiveWindow代表像窗口事件被最小化等,并且输出ReactiveWidget表示鼠标和键盘事件从外部/用户的到来.
然后一个人会创建一个实际的小部件,让我们说一个按钮.它会有签名:
button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget)
Run Code Online (Sandbox Code Playgroud)
该ButtonProperties会确定颜色/文本/等按钮,而ReactiveButton将包含例如一个Event ButtonAction与Signal ButtonState阅读按钮的状态.
请注意,该button函数是纯函数,因为它仅依赖于纯FRP值,如事件和信号.
如果想要对小部件进行分组(例如,将它们水平堆叠),则必须创建例如:
horizontalLayout :: HLayoutProperties -> ReactiveWidget
-> (ReactiveLayout, ReactiveWidget)
Run Code Online (Sandbox Code Playgroud)
该HLayoutProperties会包含有关边界大小和信息ReactiveWidgetS为含有小部件.在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 ()
Run Code Online (Sandbox Code Playgroud)
(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.
Run Code Online (Sandbox Code Playgroud)
请注意,任何地方都不必有任何"窗口小部件对象".布局只是一个根据分区系统转换输入和输出事件的函数,因此您可以使用您可以访问小部件的事件流,或者您可以让另一个子系统完全生成流.按钮和标签也是如此:它们只是将点击事件转换为绘图命令或类似事物的函数.它是完全解耦的代表,其性质非常灵活.
该wxHaskell GUI库使优秀的使用幻影类型的部件等级模型.
这个想法如下:所有小部件共享相同的实现,即它们是指向C++对象的外部指针.但是,这并不意味着所有小部件都需要具有相同的类型.相反,我们可以构建这样的层次结构:
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)
Run Code Online (Sandbox Code Playgroud)
这样,类型的值Control A也与类型匹配Window b,因此您可以将控件用作窗口,但不能反过来.如您所见,子类型是通过嵌套类型参数实现的.
有关此技术的更多信息,请参阅Dan Leijen关于wxHaskell的论文中的第5节.
注意,该技术似乎局限于小部件的实际表示是均匀的,即总是相同的情况.但是,我相信,通过一些思考,它可以扩展到小部件具有不同表示的情况.
特别是,观察结果是可以通过在数据类型中包含方法来建模面向对象,就像这样
data CWindow a = CWindow
{ close :: IO ()
, ...
}
data CButton a = CButton
{ onClick :: (Mouse -> IO ()) -> IO ()
, ...
}
Run Code Online (Sandbox Code Playgroud)
子类型可以在这里保存一些样板,但不是必需的.
要了解可以在Haskell中完成哪些OOP(例如子类型多态),您可以查看OOHaskell.这再现了各种强大的OOP类型系统的语义,保留了大多数类型推断.实际数据编码没有被优化,但我怀疑类型系列可能允许更好的演示.
可以使用类型类来建模界面层次结构(例如Widget).可以添加新实例,因此打开了一组具体小部件.如果您想要一个可能的小部件的特定列表,那么GADT可以是一个简洁的解决方案.
子类的特殊操作是向上转换和向下转换.
首先需要有一个Widgets集合,通常的结果是使用存在类型.如果您阅读HList库的所有位,还有其他有趣的解决方案.上传非常简单,编译器可以确定所有强制转换在编译时都有效.向下转换本质上是动态的,需要一些运行时类型信息支持,通常是Data.Typeable.给定类似于Typeable的东西,向下转换只是另一个类型类,结果包含在Maybe中以指示失败.
有大部分与此相关的样板,但QusiQuoting和Templating可以减少这一点.类型推断仍然可以在很大程度上起作用.
我没有探索新的约束种类和类型,但它们增强了对向上转换和向下转换的存在性解决方案.