问题 使用F#中的类(可变性与不变性/成员与自由函数)


我目前正在做的 exercism.io F#轨道。对于每个不了解它的人来说,它正在解决TDD风格的小问题,以学习或改进编程语言。

最后两个任务是关于F#中类的使用(或者在F#中调用它们的类型)。其中一个任务使用具有余额和状态(打开/关闭)的BankAccount,可以使用函数进行更改。用法是这样的(取自测试代码):

let test () =
    let account = mkBankAccount () |> openAccount
    Assert.That(getBalance account, Is.EqualTo(Some 0.0)

我编写了代码,使用不可变的BankAccount类使测试通过,可以使用自由函数进行交互:

type AccountStatus = Open | Closed

type BankAccount (balance, status) =
    member acc.balance = balance
    member acc.status = status

let mkBankAccount () =
    BankAccount (0.0, Closed)

let getBalance (acc: BankAccount) =
    match acc.status with
    | Open -> Some(acc.balance)
    | Closed -> None

let updateBalance balance (acc: BankAccount) =
    match acc.status with
    | Open -> BankAccount (acc.balance + balance, Open)
    | Closed -> failwith "Account is closed!"

let openAccount (acc: BankAccount) =
    BankAccount (acc.balance, Open)

let closeAccount (acc: BankAccount) =
    BankAccount (acc.balance, Closed)

在开始学习F#之前做了很多OO,这让我感到疑惑。更有经验的F#开发人员如何使用类?为了更简单地回答这个问题,这里是我对F#中类/类型的主要关注:

  • 在F#中,以典型的OO方式使用课程是否令人沮丧?
  • 不可变类是首选吗? (我在上面的例子中发现它们令人困惑)
  • 在F#中访问/更改类数据的首选方法是什么? (类成员函数和获取/设置或允许管道的自由函数?静态成员如何允许管道并为函数提供适合的命名空间?)

如果问题含糊不清,我很抱歉。我不想在我的功能代码中养成糟糕的编码习惯,我需要一个关于良好实践的起点。


5365
2017-08-12 17:03


起源

更多的主观POV而不是答案;我倾向于在大多数时间(ab)使用类型(DU,记录)避免类,并且包含不变性。这里的例子BankAccount可能是一个记录。然后对于函数我坚持各种类型可用的“模式”(List,Seq,Map)并制作一个模块,其中包含具有自由函数的类型 - Sehnsucht
我喜欢以纯函数方式使用F#,主要使用Sequences及其典型函数(map,reduce,..)和针对DU和Active Patterns的模式匹配。但是与班级合作感觉非常笨重。要么感觉我没有从F#的功能性质中获得任何东西,要么感觉我不尊重类具有面向对象性质的事实。你做的记录非常好,我可能会在以后再做。 - Luca Fülbier
类不是(仍然对我来说)是F#的东西,而是一个.Net的一个,并且当F#与.Net生态系统集成时,它必须遵守它,因此它是功能和OO字之间的混合。除非你有特殊需要(例如与另一种.Net语言互操作),否则我认为你应该“永远”(可能有点强)使用类。但我会等待其他人说的话:) - Sehnsucht
@LucaFülbier使用F#中的类不必感到奇怪 - F#非常好地支持大多数OO概念(并且在某些方面,优于C#),但尝试在同一代码中使用OO概念和FP概念可以充其量只是笨重。这是一个完美的案例,你有效地将记录写成一个类而不是使用记录,这很痛苦。 - Reed Copsey


答案:


在F#中,以典型的OO方式使用课程是否令人沮丧?

不是 灰心,但这并不是最有经验的F#开发人员会去的第一个地方。大多数F#开发人员将避免使用子类和OO范例,而是使用记录或有区别的联合,以及对它们进行操作的函数。

不可变类是首选吗?

应尽可能选择不变性。话虽这么说,不可变类通常可以用其他方式表示(见下文)。

在F#中访问/更改类数据的首选方法是什么? (类成员函数和获取/设置或允许管道的自由函数?静态成员如何允许管道并为函数提供适合的命名空间?)

这通常通过允许管道的功能来完成,但也可以直接进行访问。


对于上面的代码,使用记录而不是类更常见,然后将记录上工作的函数放入模块中。像你这样的“不可变类”可以更简洁地写成一个记录:

type BankAccount = { balance : float ; status : AccountStatus }

完成此操作后,使用它会变得更容易 with 返回修改版本:

let openAccount (acc: BankAccount) =
    { acc with status = Open }

请注意,将这些函数放入模块中是很常见的:

module Account =
    let open acc =
       { acc with status = Open }
    let close acc =
       { acc with status = Closed }

7
2017-08-12 20:59



我很高兴看到我对这些问题的想法毕竟不是那么糟糕:-) - Sehnsucht
这看起来比我到目前为止所做的要好得多。整个记录和管道方法感觉很像Java / C#中的流畅接口。我只需要了解每一步创建一组新数据而不是操纵现有数据这一事实。 - Luca Fülbier


问题:在F#中,是否以典型的OO方式使用课程?

这并不违背F#的本性。我认为有些情况是合理的。

但是,如果开发人员希望充分利用F#优势(例如类型干扰,使用部分应用程序等功能模式的能力,简洁性)并且不受遗留系统和库的限制,则应限制类的使用。

F#的乐趣和利润给出了快速摘要 使用类的优点和缺点

Ouestion:不可变类是首选吗? (我在上面的例子中发现它们令人困惑)

有时是,有时不是。我认为类的不变性给了你很多优点(它更容易推理类型的不变量等)但有时候不可变类可能有点麻烦。

我认为这个问题有点过于宽泛 - 这有点类似于问题 流畅的界面 在面向对象设计中是首选 - 简短的答案是:它取决于。

在F#中访问/更改类数据的首选方法是什么? (类成员函数和获取/设置或允许管道的自由函数?静态成员如何允许管道并为函数提供适合的命名空间?)

管道是F#中的规范构造,所以我会选择静态成员。如果您的库是以其他语言使用的,那么您也应该在类中包含getter和setter。

编辑:

FSharp.org 有一个非常具体的设计指南清单,其中包括:

 根据标准,使用类来封装可变状态   面向对象方法。

 使用受歧视的联合作为类层次结构的替代方法   用于创建树状结构数据。


5
2017-08-12 20:58





有几种方法可以看待这个问题。

这可能意味着几件事。对于POCO,不可变的F# records 是优选的。然后他们的操作返回  必要字段的记录已更改。

type BankAccount { status: AccountStatus; balance: int }
let close acct = { acct with status = Closed } // returns a *new* acct record

所以这意味着你必须超越一个代表一个“事物”的帐户“对象”的想法。这只是您操作以创建不同数据的数据,并最终(可能)存储到某个地方的数据库中。

所以而不是OO范式 acct.Close(); acct.PersistChanges()你有 let acct' = close acct; db.UpdateRecord(acct')

然而,对于“面向服务的体系结构(SOA)”中的“服务”,接口和类在F#中是完全自然的。例如,如果你想要一个Twitter API,你可能会创建一个包装所有HTTP调用的类,就像在C#中一样。我在F#中看到过一些对“SOLID”意识形态的引用,它完全避开了SOA,但我从未想过如何在实践中完成这项工作。

就个人而言,我喜欢FP-OO-FP三明治,顶部是Suave的FP组合器,中间是使用Autofac的SOA,底部是FP记录。我发现它运作良好且可扩展。

FWIW你也可以想做你的 BankAccount 如果是歧视的工会 Closed 不能有平衡。在您的代码示例中尝试这一点。 F#的一个好处是它使不合逻辑的状态无法代表。

type BankAccount = Open of balance: int | Closed

3
2017-08-13 00:52