无需使用接口即可访问F#记录库属性

MiP*_*MiP 2 .net c# f# functional-programming record

F#记录不能被继承,但是它们可以实现接口。例如,我想创建不同的控制器:

type ControllerType =
    | Basic
    | Advanced1
    | Advanced1RAM
    | Advanced1RAMBattery
    | Advanced2

// base abstract class
type IController =
    abstract member rom : byte[]
    abstract member ``type`` : ControllerType

type BasicController =
    { rom : byte[]
      ``type`` : ControllerType }
    interface IController with
        member this.rom = this.rom
        member this.``type`` = this.``type``

type AdvancedController1 =
    { ram : byte[]
      rom : byte[]
      ``type`` : ControllerType }
    interface IController with
        member this.rom = this.rom
        member this.``type`` = this.``type``

type AdvancedController2 =
    { romMode : byte
      rom : byte[]
      ``type`` : ControllerType }
    interface IController with
        member this.rom = this.rom
        member this.``type`` = this.``type``

let init ``type`` =
    match ``type`` with
    | Basic ->
        { rom = Array.zeroCreate 0
          ``type`` = Basic } :> IController
    | Advanced1 | Advanced1RAM | Advanced1RAMBattery ->
        { ram = Array.zeroCreate 0
          rom = Array.zeroCreate 0
          ``type`` = ``type`` } :> IController
    | Advanced2 ->
        { romMode = 0xFFuy
          rom = Array.zeroCreate 0
          ``type`` = ``type`` } :> IController
Run Code Online (Sandbox Code Playgroud)

我有两个问题:

  1. 创建控制器记录时,需要将其上载到接口。有没有更好的方式编写init上面的函数而无需:> IController每条记录?
  2. 我尝试了区分工会,但最终还是像这样的例子写了一篇文章。但是接口是.NET的东西,我如何用功能而不是继承来以功能方式重写示例?

Fyo*_*kin 5

回答第一个问题:不,您不能每次都摆脱up弃。F#不执行自动类型强制(这是一件好事),并且所有match分支必须具有相同的类型。因此,唯一要做的就是手动强制。

对第二个问题的答案:有区别的工会代表“封闭世界的假设”-也就是说,当您事先知道不同案件的数量并且您对以后再扩展它们不感兴趣时​​(您的世界是“封闭的”),工会是很好的。在这种情况下,您可以让编译器帮助您确保所有处理您的事情的人都能处理所有情况。对于某些应用程序,此功能超级强大。

另一方面,有时您需要以某种方式设计您的东西,以便以后可以通过外部插件进行扩展。这种情况通常被称为“开放世界假设”。在这种情况下,接口起作用。但是它们不是唯一的方法。

接口只不过是函数的记录,方法通用性除外。如果您对通用方法不感兴趣,并且以后不打算向下转换到特定的实现(无论如何这样做都是一件坏事),您可以将“开放世界”表示为功能记录:

type Controller = { 
   ``type``: ControllerType
   controlSomething: ControllableThing -> ControlResult
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以通过提供不同的controlSomething实现来创建不同类型的控制器:

let init ``type`` =
    match ``type`` with
    | Basic ->
        let rom = Array.zeroCreate 0
        { ``type`` = Basic
          controlSomething = fun c -> makeControlResult c rom }

    | Advanced1 | Advanced1RAM | Advanced1RAMBattery ->
        let ram = Array.zeroCreate 0
        let rom = Array.zeroCreate 0
        { ``type`` = ``type`` 
          controlSomething = fun c -> makeControlResultWithRam c rom ram }

    | Advanced2 ->
        let romMode = 0xFFuy
        let rom = Array.zeroCreate 0
        { ``type`` = ``type`` 
          controlSomething = fun c -> /* whatever */ }
Run Code Online (Sandbox Code Playgroud)

Incidentally, this also gets rid of upcasting, since now everything is of the same type. Also incidentally, your code is much smaller now, since you don't have to explicitly define all different controllers as their own types.

Q: Wait, but now, how do I get access to ram and rom and romMode from outside?

A: Well, how were you going to do it with the interface? Were you going to downcast the interface to a specific implementation type, and then access its fields? If you were going to do that, then you're back to the "closed world", because now everybody who handles your IController needs to know about all implementation types and how to work with them. If this is the case, you would be better off with a discriminated union to begin with. (like I said above, downcasting is not a good idea)

On the other hand, if you're not interested in downcasting to specific types, it means that you're only interested in consuming the functionality that all controllers implement (this is the whole idea of interfaces). If this is the case, then a record of functions is sufficient.

Finally, if you are interested in generic methods, you have to use interfaces, but you still don't have to declare everything as types, for F# has inline interface implementations:

type Controller =  
   abstract member ``type``: ControllerType
   abstract member genericMethod: 'a -> unit

let init ``type`` =
    match ``type`` with
    | Basic ->
        let rom = Array.zeroCreate 0
        { new Controller with 
             member this.``type`` = Basic
             member this.genericMethod x = /* whatever */ }

    // similar for other cases
Run Code Online (Sandbox Code Playgroud)

This is a little more verbose than records, and you can't easily amend them (i.e. no { ... with ... } syntax for interfaces), but if you absolutely need generic methods, it's possible.

  • 您可以将它们存储在函数可以访问的闭包中。看我的例子:看看我如何在`controlSomething`的实现中使用`ram`和`rom`吗?您甚至可以使它们可变!(尽管我强烈建议不要这样做) (2认同)