简体   繁体   English

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

[英]Access to F# record base properties without using interface

F# records cannot be inherited, but they can implement interfaces. F#记录不能继承,但可以实现接口。 For example, I want to create different controllers: 例如,我想创建不同的控制器:

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

I have 2 questions: 我有两个问题:

  1. When I create a controller record, I need to upcast it to an interface. 当我创建一个控制器记录时,我需要将它转发到一个接口。 Is there a better way to write the init function above without :> IController each record? 有没有更好的方法来编写上面的init函数:> IController每条记录?
  2. I tried discriminated unions but somehow end up writing interfance like this example. 我尝试过有区别的工会但不知何故最终会像这个例子一样写出对话。 But interface is a .NET thing, how can I rewrite the example in a functional way, with composition rather than inheritance? 但是接口是一个.NET的东西,我如何以功能的方式重写示例,使用组合而不是继承?

Answer to the first question: no, you cannot get rid of upcasting every time. 回答第一个问题:不,你不能每次都摆脱向上倾斜。 F# doesn't do automatic type coercion (which is a good thing), and all match branches must have the same type. F#不执行自动类型强制(这是一件好事),并且所有match分支必须具有相同的类型。 So the only thing to do is to coerce manually. 所以唯一要做的就是手动强制。

Answer to the second question: discriminated unions represent the "closed world assumption" - that is, they are good when you know the number of different cases upfront, and you're not interested in extending them later (your world is "closed"). 对第二个问题的回答:受歧视的工会代表“封闭的世界假设” - 也就是说,当你事先知道不同案件的数量时,它们是好的,而你以后不想扩展它们(你的世界是“封闭的”) 。 In this case, you can have the compiler help you make sure that everybody working with your thing handles all the cases. 在这种情况下,您可以让编译器帮助您确保每个使用您的东西的人处理所有情况。 This is super powerful for certain applications. 这对某些应用来说非常强大。

On the other hand, sometimes you need to design your thing in such a way that it can be extended later, possibly by an external plugin. 另一方面,有时你需要设计你的东西,以便以后可以通过外部插件进行扩展。 This situation is often referred to as "open world assumption". 这种情况通常被称为“开放世界假设”。 In this case, interfaces work. 在这种情况下,接口工作。 But they are not the only way. 但它们不是唯一的方法。

Interfaces are nothing more than records of functions, with the exception of method genericity . 接口只不过是函数的记录, 方法通用性除外。 If you're not interested in generic methods and you're not planning on downcasting to specific implementations later (which would be a bad thing to do anyway), you can just represent your "open world" thing as a record of functions: 如果你对泛型方法不感兴趣, 并且你不打算稍后向特定的实现进行向下转换(无论如何都要做坏事),你可以将你的“开放世界”事物表示为函数记录:

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

Now you can create the different types of controllers by providing different controlSomething implementation: 现在,您可以通过提供不同的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 */ }

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? 问:等等,但现在,如何从外部访问ramromromMode

A: Well, how were you going to do it with the interface? A:嗯,你打算怎么用界面做的? 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. 如果你打算这样做,那么你就回到了“封闭的世界”,因为现在每个处理你的IController人都需要知道所有的实现类型以及如何使用它们。 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: 最后 ,如果你兴趣在通用的方法,你必须使用接口,但仍然没有宣布一切,类型,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

This is a little more verbose than records, and you can't easily amend them (ie no { ... with ... } syntax for interfaces), but if you absolutely need generic methods, it's possible. 这比记录更冗长,你不能轻易修改它们(即没有{ ... with ... }语法接口),但如果你绝对需要通用方法,那就有可能。

Answer to first question : you can let the compiler do most of the work: 回答第一个问题 :你可以让编译器完成大部分工作:

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

That is, specify the return type once and then let the compiler fill it in for _ or use upcast 也就是说,指定一次返回类型,然后让编译器填入_或使用upcast

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM