在以“功能”风格进行编程时,您是否在应用程序逻辑中编织了一个应用程序状态?


12

如何构建具有以下所有功能的系统

  1. 对不可变对象使用纯函数。
  2. 只传递给它所需功能的数据,不再传递(即没有大的应用程序状态对象)
  3. 避免对函数使用过多的参数。
  4. 避免仅出于将参数打包和拆包到函数的目的而构造新对象的方法,只是避免将过多的参数传递给函数。如果我要将多个项目作为一个对象打包到一个函数中,则我希望该对象成为该数据的所有者,而不是临时构造的东西

在我看来,State monad违反了规则2,虽然它并不明显,因为它是通过monad编织而成的。

我觉得我需要以某种方式使用Lenses,但是关于非功能语言的文章很少。

背景

作为练习,我将现有的应用程序之一从面向对象的样式转换为功能样式。我要尝试做的第一件事是尽可能多地利用应用程序的内核。

我听到的一件事是,如何用一种纯函数式的语言来管理“状态”,而我相信这是由State monad完成的,从逻辑上讲,您称一个纯函数为“传递状态”。世界原样”,那么当函数返回时,它会返回给您变化后的世界状态。

为了说明这一点,您可以用一种纯粹的功能性方式来创建“ hello world”的方式有点像,您将程序的屏幕状态传递给程序,然后返回印有“ hello world”状态的屏幕状态。因此,从技术上讲,您要调用一个纯函数,并且没有副作用。

基于此,我遍历了我的应用程序,并且:1.首先将我的所有应用程序状态放入单个全局对象(GameState)中。2.其次,使GameState不可变。您无法更改。如果需要更改,则必须构造一个新的。我通过添加一个复制构造函数来做到这一点,该复制构造函数可以选择接受一个或多个已更改的字段。3.对于每个应用程序,我都将GameState作为参数传递。在函数内,在完成将要执行的操作后,它将创建一个新的GameState并返回它。

我如何拥有纯功能核心,以及外部的循环,该循环将GameState馈入应用程序的主工作流程循环。

我的问题:

现在,我的问题是,GameState有大约15个不同的不可变对象。最低级别的许多功能仅对其中一些对象起作用,例如保持得分。因此,假设我有一个计算得分的函数。今天,GameState传递给此函数,该函数通过使用新分数创建新的GameState来修改分数。

似乎有些错误。该功能不需要完整的GameState。它只需要Score对象。所以我更新了它以传递分数,并仅返回分数。

这似乎很有意义,所以我进一步介绍了其他功能。有些功能需要我从GameState传入2、3或4个参数,但是随着我一直在应用程序的外部核心中使用该模式,我传入了越来越多的应用程序状态。就像在工作流循环的顶部,我将调用一个方法,该方法将调用将调用一个方法的方法,依此类推,一直到计算分数为止。这意味着当前分数会通过所有这些层,只是因为最底层的函数将要计算分数。

所以现在我有了带有数十个参数的函数。我可以将这些参数放入一个对象中以减少参数数量,但是然后我希望该类成为状态应用程序状态的主位置,而不是在调用时简单地构造以避免重复传递的对象输入多个参数,然后解压缩它们。

所以现在我想知道我的问题是否是我的函数嵌套得太深了。这是因为希望具有较小的功能,所以我在一个功能变大时进行重构,然后将其拆分为多个较小的功能。但是这样做会产生更深的层次结构,即使外部函数未直接在这些对象上运行,传递给内部函数的所有内容也都必须传递给外部函数。

似乎只是在避免这种问题的过程中传入了GameState。但是我回到了原来的问题,即向函数传递比函数所需更多的信息。


1
我不是设计专家,也不是功能专家,但是由于您的游戏天生具有发展的状态,因此您确定函数式编程是适合您应用程序所有层的范例吗?
Walfrat

沃尔夫拉特,我想如果您与函数式编程专家交谈,您可能会发现他们会说函数式编程范式具有管理不断发展的状态的解决方案。
Daisha Lynn

对于我来说,您的问题似乎更广,只能说明。如果仅是管理状态,这是一个开始:请参见stackoverflow.com/questions/1020653/…中
Walfrat

2
@DaishaLynn我不认为您应该删除问题。它已经被批准,并且没有人试图关闭它,所以我认为它不在本网站的讨论范围之内。到目前为止,缺少答案可能仅仅是因为它需要一些相对专业的知识。但这并不意味着最终不会找到并回答它。
Ben Aaronson'7

2
在没有大量语言帮助的情况下,在复杂的纯功能程序中管理可变状态是一个巨大的痛苦。在Haskell中,由于monad,简洁的语法,非常好的类型推断,因此它是可管理的,但是仍然很烦人。在C#中,我认为您会遇到更多麻烦。
恢复莫妮卡

Answers:


2

我不确定是否有好的解决方案。这可能不是答案,但评论太久了。我在做类似的事情,以下技巧也有所帮助:

  • GameState分层拆分,因此可以得到3-5个较小的部分,而不是15个。
  • 让它实现接口,以便您的方法仅看到所需的部分。切勿退缩它们,因为您会对真实类型撒谎。
  • 让部件也实现接口,以便您可以很好地控制所传递的内容。
  • 使用参数对象,但要谨慎使用,并尝试将它们变成具有自己行为的真实对象。
  • 有时传递的数量比需要的数量略多,而不是冗长的参数列表。

所以现在我想知道我的问题是否是我的函数嵌套得太深了。

我不这么认为。重构为小函数是正确的,但是也许您可以更好地对其进行重组。有时,这是不可能的,有时只需要第二(或第三)次就可以解决问题。

将您的设计与可变设计进行比较。是否存在因重写而变得更糟的情况?如果是这样,您是否不能像最初一样改善它们?


有人告诉我更改设计,以便函数只能使用一个参数,以便可以使用curring。我尝试了一个功能,所以现在不再调用DeleteEntity(a,b,c),而是调用DeleteEntity(a)(b)(c)。所以这很可爱,而且应该可以使它变得更容易组合,但是我还没有明白。
Daisha Lynn

@DaishaLynn我正在使用Java,并且没有甜美的语法糖,因此(对我而言)不值得尝试。对于在我们的案例中可能使用高阶函数,我持怀疑态度,但请告诉我它是否对您有用。
maaartinus

2

我无法与C#对话,但是在Haskell中,您最终会传递整个状态。您可以显式地或使用State monad来执行此操作。要解决函数接收更多信息然后需要解决的问题,您可以做的一件事就是使用Has类型类。(如果您不熟悉,Haskell类型类有点像C#接口。)对于状态的每个元素E,您可以定义一个类型类HasE,它需要一个函数getE来返回E的值。成为所有这些类型类的实例。然后,在您的实际函数中,您不需要显式地要求您使用State monad,而是需要您需要的元素具有Has类型类的任何monad;限制了函数可以使用其使用的monad的功能。有关此方法的更多信息,请参见Michael Snoyman的发表关于ReaderT设计模式的文章

您可能会在C#中复制类似的内容,具体取决于您定义传递状态的方式。如果你有类似的东西

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

你可以定义接口IHasMyInt,并IHasMyString用方法GetMyIntGetMyString分别。状态类如下所示:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

那么您的方法可能需要IHasMyInt,IHasMyString或整个MyState(如果适用)。

然后,您可以在函数定义上使用where约束,以便可以传递状态对象,但是状态对象只能到达string和int,不能到达double。

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}

那很有意思。因此,当前,我通过传递调用函数并按值传递10个参数的方式,我传递了10次“ gameSt'ate”,但传递了10种不同的参数类型,例如“ IHasGameScore”,“ IHasGameBoard”等。我希望在那里是传递函数可以指示必须以一种类型实现所有接口的单个​​参数的方法。我想知道是否可以使用“通用约束”来做。让我尝试一下。
Daisha Lynn

1
有效。它在这里工作:dotnetfiddle.net/cfmDbs
Daisha Lynn

1

我认为您最好了解Redux或Elm以及它们如何处理此问题。

基本上,您具有一个纯函数,它可以接收整个状态和用户执行的操作并返回新状态。

然后,该函数调用其他纯函数,每个纯函数处理状态的特定部分。根据操作的不同,这些功能中的许多功能可能什么也不做,只能返回原始状态不变。

要了解更多信息,请访问Elm Architecture或Redux.js.org。


我不认识Elm,但我相信它类似于Redux。在Redux中,难道不是所有的reducer都因状态改变而被调用吗?听起来效率极低。
Daisha Lynn

当涉及低级优化时,不要假设,衡量。实际上,它足够快。
Daniel T.

谢谢丹尼尔,但这对我不起作用。我已经做了大量的开发工作,以知道它不会在任何数据更改时都通知UI中的每个组件,无论控件是否关心该控件。
Daisha Lynn

-2

我认为您想要做的是使用一种不应使用的面向对象语言,就好像它是一种纯函数式语言一样。并不是说OO语言就是邪恶。每种方法都有其优点,这就是为什么我们现在可以将OO样式与功能样式相结合,并且有机会使某些代码段具有功能,而其他代码仍保持面向对象,以便我们可以利用所有可重用性,继承性或多态性。幸运的是,我们不再局限于这两种方法,所以您为什么要限制自己使用其中一种方法?

回答您的问题:不,我不通过应用程序逻辑来编织任何特定的状态,而是使用适合当前用例的内容,并以最适当的方式应用可用的技术。

C#尚未准备好(尚未)用作功能。


3
既不为这个答案而激动,也不为语气。我没有滥用任何东西。我正在推动C#的极限,以将其用作一种更具功能性的语言。这不是一件罕见的事情。您似乎在哲学上反对它,这很好,但是在那种情况下,请不要考虑这个问题。您的评论对任何人都没有用。继续。
Daisha Lynn

@DaishaLynn,你错了,我不反对它,事实上我经常使用它……但是在自然和可能的情况下,而不是因为它很时髦,所以不打算将一种OO语言变成一种实用的语言。这样做。您不必同意我的回答,但这不会改变您没有正确使用工具的事实。
t3chb0t

我没有这样做,因为这样做很时髦。C#本身正朝着使用功能风格迈进。安德斯·海斯伯格本人已表明了这一点。我了解您仅对主流使用该语言感兴趣,并且我了解为什么和何时合适。我就是不知道为什么像您这样的人也加入了这个话题。
Daisha Lynn

@DaishaLynn如果您无法应对批评您的问题或方法的答案,那么您可能不应该在这里提出问题,或者下次您应该添加免责声明,说您只对100%支持您的想法的答案感兴趣,因为您不会想要听到真相,而是获得支持意见。
t3chb0t

请彼此更加亲切。可以发表批评而不贬低语言。尝试以功能风格对C#进行编程当然不是“滥用”或极端情况。这是许多C#开发人员用来学习其他语言的常用技术。
zumalifeguard
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.