关于用户界面:功能GUI编程是否可行?

Is functional GUI programming possible?

我最近发现了fp错误(尝试学习haskell),我对到目前为止所看到的(一流的函数、懒惰的评估和所有其他的优点)印象深刻。我还不是专家,但我已经开始发现"功能性"的推理比强制性的基本算法更容易(而且我很难回到我必须去的地方)。

然而,当前的FP似乎没有进展的一个领域是GUI编程。haskell方法似乎只是包装必要的GUI工具包(如gtk+或wxwidgets),并使用"do"块来模拟必要的样式。我没有使用f,但我的理解是,它使用oop和.NET类做了类似的事情。显然,有一个很好的理由——当前的GUI编程都是关于IO和副作用的,所以在大多数当前框架中,纯函数编程是不可能的。

我的问题是,有没有可能有一个功能性的GUI编程方法?我很难想象这在实践中会是什么样子。有没有人知道有什么框架,不管是实验性的还是其他的,尝试过这种东西(甚至是任何为功能性语言从头开始设计的框架)?或者只是使用混合方法的解决方案,其中OOP用于GUI部件,FP用于逻辑?(我只是好奇地问——我很想认为fp是"未来",但GUI编程似乎是一个相当大的漏洞需要填补。)


The Haskell approach seems to be to just wrap imperative GUI toolkits (such as GTK+ or wxWidgets) and to use"do" blocks to simulate an imperative style

这并不是真正的"haskell方法"——这只是通过命令式接口最直接地绑定到命令式GUI工具包的方法。haskell恰好有相当突出的绑定。

对于gui,有几种较为成熟或更为实验性的纯函数/声明性方法,主要是在haskell中,并且主要使用函数反应式编程。

例如:

  • 反射平台,https://github.com/reflex-frp/reflex-platform
  • 葡萄柚,http://hackage.haskell.org/package/grapefruit-ui-gtk
  • 反应式,http://hackage.haskell.org/package/reactive-glut
  • wxfruit,http://hackage.haskell.org/package/wxfruit
  • 反应香蕉,http://hackage.haskell.org/package/reactive-banana

对于那些不熟悉haskell、flapjax的人来说,http://www.flapjax-lang.org/是在javascript之上实现功能性反应式编程。


My question is, is it possible to have a functional approach to GUI programming?

你要找的关键词是"功能反应式编程"(FRP)。

Conal Elliott和其他一些人为了找到正确的玻璃钢抽象概念,做了一些家庭手工业。Haskell中有几个FRP概念的实现。

您可以考虑从Conal最新的"推拉函数式反应式编程"论文开始,但还有其他一些(较旧的)实现,有些实现与haskell.org网站链接。科纳尔有一个覆盖整个领域的诀窍,他的论文可以阅读而不必参考之前的内容。

为了了解这种方法如何用于GUI开发,您可能想看看Fudget,虽然Fudget在90年代中期的设计中有点长,但它确实为GUI设计提供了一种可靠的FRP方法。


Windows演示基础是一个证明函数方法对GUI编程非常有效的证明。它有许多功能方面,"好"的WPF代码(搜索MVVM模式)强调功能方法而不是命令。我可以勇敢地宣称,WPF是最成功的实际功能GUI工具包:—)

wpf在xaml中描述了用户界面(尽管您也可以将其重写为功能性的c或f_),因此要创建一些用户界面,您需要编写:

1
2
3
4
5
<!-- Declarative user interface in WPF and XAML -->
<Canvas Background="Black">
   <Ellipse x:Name="greenEllipse" Width="75" Height="75"
      Canvas.Left="0" Canvas.Top="0" Fill="LightGreen" />
</Canvas>

此外,WPF还允许您使用另一组声明性标记声明性地描述动画和对事件的反应(同样,相同的内容可以写成C/F代码):

1
2
3
4
<DoubleAnimation
   Storyboard.TargetName="greenEllipse"
   Storyboard.TargetProperty="(Canvas.Left)"
   From="0.0" To="100.0" Duration="0:0:5" />

事实上,我认为WPF与Haskell的FRP有许多共同之处(尽管我认为WPF设计师不知道FRP,这有点不幸-WPF有时会觉得有点奇怪,不清楚您是否在使用功能观点)。


我实际上会说,函数式编程(f)是比C更好的用户界面编程工具。你只需要稍微考虑一下这个问题。

我在第16章的函数式编程书中讨论了这个主题,但是有一个免费的摘录,它显示了(imho)您可以在f_中使用的最有趣的模式。假设您想要实现矩形的绘制(用户按下按钮,移动鼠标并释放按钮)。在f中,您可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let rec drawingLoop(clr, from) = async {
   // Wait for the first MouseMove occurrence
   let! move = Async.AwaitObservable(form.MouseMove)
   if (move.Button &&& MouseButtons.Left) = MouseButtons.Left then
      // Refresh the window & continue looping
      drawRectangle(clr, from, (move.X, move.Y))
      return! drawingLoop(clr, from)
   else
      // Return the end position of rectangle
      return (move.X, move.Y) }

let waitingLoop() = async {
   while true do
      // Wait until the user starts drawing next rectangle
      let! down = Async.AwaitObservable(form.MouseDown)
      let downPos = (down.X, down.Y)
      if (down.Button &&& MouseButtons.Left) = MouseButtons.Left then
         // Wait for the end point of the rectangle
         let! upPos = drawingLoop(Color.IndianRed, downPos)
         do printfn"Drawn rectangle (%A, %A)" downPos upPos }

这是一种非常必要的方法(在通常的实用f样式中),但它避免使用可变状态来存储绘图的当前状态和存储初始位置。但是,它可以变得更加实用,我写了一个图书馆,作为我的硕士论文的一部分,它应该在接下来的几天里出现在我的博客上。

功能性反应式编程是一种功能性更强的方法,但我发现它有点难以使用,因为它依赖于相当高级的haskell特性(如箭头)。然而,它在许多情况下都非常优雅。它的局限性在于,你不能轻易地对状态机进行编码(这是一个对反应式程序有用的心理模型)。使用上述f技术非常容易。


无论您使用的是混合功能/oo语言(如f或ocaml),还是纯功能语言(如haskell),其中副作用被归为IO单体,大多数情况下,管理GUI所需的大量工作更像是"副作用",而不是纯功能算法。

也就是说,已经有一些真正可靠的研究将其应用到功能性GUI中。甚至还有一些(大部分)功能性工具包,如软糖或frantk。


您可以在f上查看DonSyme的系列,演示人员在这里创建了一个GUI。下面的链接指向系列的第三部分(您可以从此处链接到其他两部分)。

将f用于WPF开发将是一个非常有趣的GUI范例…

http://channel9.msdn.com/show/going+deep/c9-chassors-dr-don-syme-introduction-to-f-3-of-3/


功能性反应式编程背后一个开放思想的想法是拥有一个事件处理函数,它产生对事件的反应和下一个事件处理函数。因此,一个不断发展的系统被表示为一系列事件处理功能。

对我来说,学习yampa成为一个关键点,以使功能产生功能的事情。有一些关于Yampa的好论文。我推荐Yampa拱廊:

http://www.cs.nott.ac.uk/~nhn/talks/hw2003-yampaacarade.pdf(幻灯片,pdf)http://www.cs.nott.ac.uk/~nhn/publications/hw2003.pdf(全文,pdf)

在haskell.org的yampa上有一个wiki页面

http://www.haskell.org/haskellwiki/yampa

原版Yampa主页:

http://www.haskell.org/yampa(不幸的是现在已经坏了)


自从这个问题被第一次提出以来,功能反应式编程已经被ELM变得更加主流。

我建议在http://elm-lang.org上查看它,该网站还提供了一些关于如何在浏览器GUI中实现完全功能的真正优秀的交互式教程。

它允许您制作功能齐全的GUI,其中您需要自己提供的代码仅由纯函数组成。我个人发现它比各种haskell GUI框架更容易进入。


埃利奥特关于玻璃钢的谈话可以在这里找到。

此外,不是一个真正的答案,而是一句话和一些想法:"功能图形用户界面"这个术语似乎有点像矛盾修饰语(同一术语中的pureness和io)。

但我模糊的理解是,功能性GUI编程是关于声明性地定义一个依赖于时间的函数,该函数接受(实时)依赖于时间的用户输入并生成依赖于时间的GUI输出。

换句话说,这个函数的定义类似于一个微分方程的声明性定义,而不是由一个必须使用可变状态的算法来定义。

因此,在传统的FP中,使用时间无关函数,而在FRP中,使用时间相关函数作为构建块来描述程序。

让我们考虑在弹簧上模拟一个球,用户可以与之交互。球的位置是图形输出(在屏幕上),用户推球是按键(输入)。

在FRP中描述这个模拟程序(根据我的理解)是通过一个微分方程(声明性地)完成的:加速度*质量=-弹簧拉伸*弹簧常数+用户施加的力。

下面是一个关于榆树的视频,说明了这个观点。


截至2016年,Haskell还拥有多个相对成熟的玻璃钢框架,如钠和反射(也包括NetWire)。

功能性程序设计手册展示了Java版本的钠,作为工作实例,并说明了FRP GUI代码库在命令和基于行为的方法中的行为和规模。

最近还有一篇关于箭头玻璃钢的论文,以及在遵守法律的纯玻璃钢设置中合并副作用、IO和突变的前景:http://haskell.cs.yale.edu/wp-content/uploads/2015/10/dwc-yale-formatted-document.pdf。

同样值得注意的是,诸如ReactJS和Angular等JavaScript框架以及许多其他框架已经或正在使用FRP或其他功能方法来实现可伸缩和可组合的GUI组件。


为了解决这个问题,我在使用f时发表了一些我的想法,

http://fadsworld.wordpress.com/2011/04/13/f-in-the-enterprise-i/http://fadsworld.wordpress.com/2011/04/17/fin-the-enterprise-ii-2/

我还计划做一个视频教程来完成这个系列,并展示F_如何在UX编程中做出贡献。

我只在这里讨论F_的上下文。

法哈德


XUL等标记语言允许您以声明性的方式构建GUI。


所有这些答案都是建立在函数式编程的基础上的,但是要做出很多自己的设计决策。一个基本上完全由函数和简单抽象数据类型构建的库是gloss。这里是它的play函数的源代码类型

1
2
3
4
5
6
7
8
9
10
11
12
-- | Play a game in a window. Like `simulate`, but you manage your own input events.
play    :: Display              -- ^ Display mode.
        -> Color                -- ^ Background color.
        -> Int                  -- ^ Number of simulation steps to take for each second of real time.
        -> world                -- ^ The initial world.
        -> (world -> Picture)   -- ^ A function to convert the world a picture.
        -> (Event -> world -> world)    
                -- ^ A function to handle input events.
        -> (Float -> world -> world)
                -- ^ A function to step the world one iteration.
                --   It is passed the period of time (in seconds) needing to be advanced.
        -> IO ()

正如您所看到的,它完全是通过为纯函数提供简单的抽象类型来工作的,其他库可以帮助您完成这些工作。


哈斯凯尔新认识的人注意到的最明显的创新是,与外部世界交流有关的不纯世界与计算和算法的纯世界之间存在着分离。一个常见的初学者问题是:"我怎样才能摆脱IO,即将IO a转换成a"实现它的方法是使用monads(或其他抽象)来编写执行IO和连锁效果的代码。这段代码收集来自外部世界的数据,创建它的模型,进行一些计算,可能通过使用纯代码,并输出结果。

就上述模型而言,我认为在IOmonad中操纵gui没有任何严重错误。从这种风格中产生的最大问题是模块不再是可组合的,也就是说,我失去了关于程序中语句的全局执行顺序的大部分知识。为了恢复它,我必须在并发的命令式GUI代码中应用类似的推理。同时,对于不纯、非GUI代码,由于IOmonad的>==操作符的定义(至少只要只有一个线程),执行顺序是明显的。对于纯代码来说,除了在角落的情况下提高性能或避免导致的评估之外,根本不重要。

控制台和图形IO之间最大的哲学区别是实现前者的程序通常是以同步方式编写的。这是可能的,因为(撇开信号和其他打开的文件描述符)只有一个事件源:通常称为stdin的字节流。不过,gui本质上是异步的,必须对键盘事件和鼠标单击做出反应。

以功能方式进行异步IO的一种流行的原理被称为功能反应式编程(FRP)。最近,由于诸如reactivex之类的库和ELM之类的框架的存在,它在不纯的、非功能性语言中获得了很大的吸引力。简而言之,它就像是将GUI元素和其他东西(如文件、时钟、警报、键盘、鼠标)作为事件源(称为"可观察的")进行查看,从而发出事件流。这些事件使用熟悉的运算符(如mapfoldlzipfilterconcatjoin等)组合,以产生新的流。这是有用的,因为程序状态本身可以被视为程序的scanl . map reactToEvents $ zipN ,其中N等于程序曾经考虑过的可观测的数量。

使用FRP观测数据可以恢复可组合性,因为流中的事件是按时间顺序排列的。原因是,事件流抽象使得可以将所有可观察到的对象视为黑盒。最后,使用运算符组合事件流会在执行时返回一些本地顺序。这迫使我对程序实际上依赖的不变量更加诚实,类似于haskell中所有函数都必须具有引用透明性的方式:如果我想从程序的另一部分提取数据,我必须显式地为函数声明适当的类型。(IO Monad是一种专门用于编写不纯代码的领域语言,有效地规避了这一点)


函数式编程可能从我上大学的时候就开始了,但我记得,函数式编程系统的主要目的是阻止程序员产生任何"副作用"。但是,由于创建的副作用(例如更新用户界面),用户购买软件。