【问题标题】:Access to F# record base properties without using interface在不使用接口的情况下访问 F# 记录库属性
【发布时间】:2017-09-10 20:35:06
【问题描述】:

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

我有两个问题:

  1. 当我创建一个控制器记录时,我需要将它上传到一个接口。没有:> IController每条记录,有没有更好的方法来编写上面的init函数?
  2. 我尝试了有区别的联合,但不知何故最终编写了像这个例子这样的接口。但是接口是 .NET 的东西,我怎样才能以功能的方式重写示例,使用组合而不是继承?

【问题讨论】:

    标签: c# .net f# functional-programming record


    【解决方案1】:

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

    回答第二个问题: 可区分联合代表“封闭世界假设” - 也就是说,当您预先知道不同案例的数量并且您对扩展不感兴趣时​​,它们是好的他们以后(你的世界是“封闭的”)。在这种情况下,您可以让编译器帮助您确保使用您的事物的每个人都能处理所有情况。这对于某些应用程序来说非常强大。

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

    接口只不过是函数的记录,method genericity 除外。如果您对泛型方法不感兴趣并且您不打算稍后向下转换为特定实现(无论如何这将是一件坏事),您可以只代表您的“开放世界”事物作为功​​能记录:

    type Controller = { 
       ``type``: ControllerType
       controlSomething: ControllableThing -> ControlResult
    }
    

    现在您可以通过提供不同的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 */ }
    

    顺便说一句,这也摆脱了向上转换,因为现在一切都是相同的类型。顺便说一句,您的代码现在要小得多,因为您不必将所有不同的控制器显式定义为它们自己的类型。

    问:等等,但是现在,我如何从外部访问 ramromromMode

    答:好吧,你打算如何处理界面?您是否要将接口向下转换为特定的实现类型,然后访问其字段?如果您打算这样做,那么您将回到“封闭世界”,因为现在处理您的IController 的每个人都需要了解所有实现类型以及如何使用它们。如果是这种情况,您最好从有歧视的工会开始。 (就像我上面说的,向下转换不是一个好主意)

    另一方面,如果您对向下转换为特定类型不感兴趣,则意味着您只对使用所有控制器实现的功能感兴趣(这就是接口的全部概念)。如果是这种情况,那么函数记录就足够了。

    最后,如果您对泛型方法感兴趣,则必须使用接口,但您仍然不必将所有内容都声明为类型,因为 F# 具有内联接口实现:

    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
    

    这比记录更冗长,你不能轻易修改它们(即没有{ ... with ... } 接口语法),但如果你绝对需要泛型方法,这是可能的。

    【讨论】:

    • 使用功能更好。我不需要访问ramromromMode,但是我可以将这些数据存储在哪里以供后者使用?
    • 您可以将它们存储在您的函数可以访问的闭包中。看看我的例子:看看我如何在controlSomething 的实现中使用ramrom?你甚至可以让它们可变! (尽管我强烈建议不要这样做)
    【解决方案2】:

    第一个问题的答案:你可以让编译器完成大部分工作:

    let init = function
    | Basic ->
        { rom = [||]; ``type`` = Basic } :> IController
    | Advanced1 | Advanced1RAM | Advanced1RAMBattery as t ->
        { ram = [||]; rom = [||]; ``type`` = t } :> _
    | Advanced2 ->
        upcast { romMode = 0xFFuy; rom = [||]; ``type`` = Advanced2 }
    

    即指定一次返回类型,然后让编译器填写为_或使用upcast

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-31
      • 1970-01-01
      • 2018-06-25
      • 2022-01-02
      • 2013-05-10
      • 2016-01-21
      • 1970-01-01
      相关资源
      最近更新 更多