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

Luc*_*ier 13 f#

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

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

let test () =
    let account = mkBankAccount () |> openAccount
    Assert.That(getBalance account, Is.EqualTo(Some 0.0)
Run Code Online (Sandbox Code Playgroud)

我编写了代码,使用不可变的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)
Run Code Online (Sandbox Code Playgroud)

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

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

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

Ree*_*sey 10

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

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

不可变类是首选吗?

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

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

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


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

type BankAccount = { balance : float ; status : AccountStatus }
Run Code Online (Sandbox Code Playgroud)

完成此操作后,使用它会变得更容易,因为您可以使用它with来返回修改后的版本:

let openAccount (acc: BankAccount) =
    { acc with status = Open }
Run Code Online (Sandbox Code Playgroud)

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

module Account =
    let open acc =
       { acc with status = Open }
    let close acc =
       { acc with status = Closed }
Run Code Online (Sandbox Code Playgroud)

  • 我很高兴看到我对这些问题的想法毕竟不是那么糟糕:-) (2认同)

Tom*_*ski 6

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

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

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

F#for fun and profit提供了使用类优缺点的快速摘要.

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

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

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

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

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

编辑:

FSharp.org列出了非常具体的设计指南,其中包括:

✔根据标准OO方法,使用类来封装可变状态.

✔使用有区别的联合作为类层次结构的替代方法来创建树形结构数据.


Dax*_*ohl 5

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

这可能意味着几件事。对于 POCO,records首选不可变 F# 。然后对它们的操作返回记录,并更改了必需的字段。

type BankAccount { status: AccountStatus; balance: int }
let close acct = { acct with status = Closed } // returns a *new* acct record
Run Code Online (Sandbox Code Playgroud)

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

因此acct.Close(); acct.PersistChanges(),您将拥有let acct' = close acct; db.UpdateRecord(acct').

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

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

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

type BankAccount = Open of balance: int | Closed
Run Code Online (Sandbox Code Playgroud)