函数式GUI编程可行吗?

我最近发现了FP错误(试图学习Haskell),到目前为止,我对我所看到的(一流函数、惰性求值和所有其他好东西)印象深刻。我还不是专家,但我已经开始发现对基本算法进行“功能性”推理比命令式推理更容易(而且我很难回到我必须回到的地方)。

然而,当前FP似乎无法实现的一个领域是GUI编程。Haskell方法似乎只是包装命令式GUI工具包(如GTK+或wxWidgets),并使用“do”块来模拟命令式样式。我没有使用过f#,但我的理解是它对。net类使用OOP做了类似的事情。显然,这有一个很好的理由——当前的GUI编程都是关于IO和副作用的,所以纯函数式编程在大多数当前框架中是不可能的。

我的问题是,是否可能有一种函数式的GUI编程方法?我很难想象这在实践中会是什么样子。有人知道有什么框架,实验性的或其他的,尝试过这类事情(甚至有什么框架是为函数式语言从头设计的)吗?或者解决方案只是使用一种混合的方法,用面向对象的GUI部分和FP的逻辑?(我只是出于好奇而问——我很乐意认为FP是“未来”,但GUI编程似乎是一个相当大的洞需要填补。)

77828 次浏览

无论你是在f#或OCaml这样的混合函数/OO语言中,还是在像Haskell这样的纯函数语言中,副作用被归为IO单例,它都是主要是的情况,管理GUI所需的大量工作更像是“副作用”而不是纯函数算法。

也就是说,已经有一些非常扎实的研究投入功能gui。甚至有一些(主要)功能工具包,如FudgetsFranTk

Haskell方法似乎只是包装命令式GUI工具包(如GTK+或wxWidgets),并使用“do”块来模拟命令式样式

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

有几种比较成熟的,或者更具实验性的纯函数式/声明式gui方法,主要是在Haskell中,主要使用函数式响应式编程。

一些例子是:

对于那些不熟悉Haskell, Flapjax的人来说,http://www.flapjax-lang.org/是一个基于JavaScript的函数式响应式编程的实现。

你可以看看Don Syme在f#上的系列,他演示了如何创建一个gui。下面的链接指向本系列的第三部分(您可以从那里链接到其他两部分)。

使用f#进行WPF开发将是一个非常有趣的GUI范例……

http://channel9.msdn.com/shows/Going+Deep/C9-Lectures-Dr-Don-Syme-Introduction-to-F-3-of-3/

我的问题是,是否可能有一种函数式的GUI编程方法?

你要找的关键词是“功能响应式编程”;(玻璃钢)。

Conal Elliott和其他一些人试图为FRP找到正确的抽象,这有点像家庭手工业。在Haskell中有几个FRP概念的实现。

你可以考虑从Conal最近的“推挽式功能响应式编程”;论文开始,但还有其他几个(较旧的)实现,其中一些链接自haskell.org网站。Conal有一个覆盖整个领域的诀窍,他的论文可以在不参考以前的情况下阅读。

为了感受这种方法如何用于GUI开发,你可能想看看Fudgets,虽然它现在变得有点老了,是在90年代中期设计的,但确实为GUI设计提供了坚实的FRP方法。

实际上,我想说函数式编程(f#)对于用户界面编程来说是比c#更好的工具。你只需要稍微换个角度思考问题。

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

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#技术,这是非常容易的。

像XUL这样的标记语言允许您以声明式的方式构建GUI。

Windows Presentation Foundation证明了函数式方法对于GUI编程非常有效。它有许多功能方面,“好的”WPF代码(搜索MVVM模式)强调功能方法而不是命令式方法。我可以勇敢地宣称WPF是现实世界中最成功的功能GUI工具包:-)

WPF在XAML中描述了用户界面(尽管你也可以把它重写成函数式的c#或f#),所以要创建一些用户界面,你可以这样写:

<!-- 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#代码):

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

事实上,我认为WPF与Haskell的FRP有很多共同之处(尽管我相信WPF的设计者并不知道FRP,这有点不幸——如果你使用函数的观点,WPF有时会感觉有点奇怪和不清楚)。

函数式响应式编程背后的一个开放思想是让事件处理函数同时产生对事件的反应和下一个事件处理函数。因此,一个进化的系统被表示为事件处理函数的序列。

对我来说,学习Yampa成为正确理解函数生成函数的关键。有一些关于扬帕的不错的论文。我推荐The Yampa Arcade:

http://www.cs.nott.ac.uk/~nhn/Talks/HW2003-YampaArcade.pdf(幻灯片,PDF) http://www.cs.nott.ac.uk/~nhn/Publications/hw2003.pdf(全文,PDF)

在Haskell.org上有一个关于Yampa的维基页面

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

原Yampa首页:

http://www.haskell.org/yampa(不幸的是目前已损坏)

为了解决这个问题,我发布了一些我在使用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/ < / p >

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

这里我只是在f#上下文中讨论。

法赫德

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

Elliot关于FRP的演讲可以找到在这里

除此之外,并不是真正的回答,而是一个评论和一些想法:不知怎么的,术语“功能性GUI”似乎有点像一个矛盾修饰法(纯粹性和IO在同一个术语中)。

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

换句话说,这个函数像微分方程一样声明式地定义,而不是由算法命令式地使用可变状态定义。

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

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

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

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

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

-- | 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 ()

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

截至2016年,Haskell还有几个相对成熟的FRP框架,如Sodium和Reflex(还有Netwire)。

Manning关于函数式响应式编程的书展示了Java版本的Sodium,作为工作示例,并说明了FRP GUI代码库与基于命令和基于Actor的方法相比是如何行为和扩展的。

最近还有一篇关于箭头化FRP的论文,以及将副作用、IO和突变纳入遵守法律的纯FRP设置的前景:http://haskell.cs.yale.edu/wp-content/uploads/2015/10/dwc-yale-formatted-dissertation.pdf

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

自从第一次提出这个问题以来,函数式响应式编程已经被Elm变得更加主流。

我建议在http://elm-lang.org上查看它,那里也有一些关于如何制作一个功能齐全的浏览器内GUI的真正优秀的交互式教程。

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

刚接触Haskell的人注意到的最明显的创新是,在与外界通信的不纯世界与计算和算法的纯世界之间存在分离。初学者经常遇到的问题是:“如何摆脱IO,即将IO a转换为a?”实现它的方法是使用单子(或其他抽象)来编写执行IO和链式效果的代码。这段代码从外部世界收集数据,创建一个模型,进行一些计算,可能是通过使用纯代码,然后输出结果。

就上面的模型而言,我认为在IO单子中操纵gui没有什么可怕的错误。这种风格产生的最大问题是模块不再是可组合的,也就是说,我失去了大部分关于程序中语句的全局执行顺序的知识。为了恢复它,我必须应用与并发的命令式GUI代码类似的推理。同时,对于不纯的非gui代码,由于IO单子的>==操作符的定义,执行顺序很明显(至少只要只有一个线程)。对于纯代码,这根本无关紧要,除非在极端情况下,以提高性能或避免导致的计算。

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

以函数式方式执行异步IO的一种流行理念称为函数式响应式编程(FRP)。由于ReactiveX这样的库和Elm这样的框架,它最近在不纯的非函数性语言中得到了很多关注。简而言之,这就像将GUI元素和其他东西(如文件、时钟、闹钟、键盘、鼠标)视为事件源,称为“可观察对象”,它们发出事件流。这些事件使用熟悉的操作符组合,如mapfoldlzipfilterconcatjoin等,以产生新的流。这很有用,因为程序状态本身可以被视为程序的scanl . map reactToEvents $ zipN <eventStreams>,其中N等于程序所考虑的可观察对象的数量。

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