2017-04-14 198 views
2

F#記錄不能被繼承,但它們可以實現接口。例如,我想創建不同的控制器:不使用接口訪問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 

我有2個問題:

  1. 當我創建一個控制器記錄,我需要把它上溯造型到一個接口。有沒有更好的方法來編寫上面的init函數而沒有:> IController每條記錄​​?
  2. 我試過歧視的工會,但不知何故最終編寫interfance像這個例子。但接口是一個.NET的東西,我怎樣才能以功能的方式重寫這個例子,用組合而不是繼承?

回答

4

回答第一個問題:不,你不能每次都去掉上傳。 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

- 答:那麼,你打算如何使用界面呢?你是否將界面翻譯成特定的實現類型,然後訪問其字段?如果你打算這樣做,那麼你又回到了「封閉世界」,因爲現在每個處理你的人都需要知道所有實現類型以及如何使用它們。如果是這樣的話,你最好先與一個有歧視的工會談談。(就像我上面所說的,向下轉換並不是一個好主意)

另一方面,如果您對向下轉換爲特定類型不感興趣,這意味着您只想使用所有控制器實現的功能(這是接口的全部概念)。如果是這種情況,那麼功能記錄就足夠了。

最後,如果你感興趣的通用方法,你必須使用接口,但仍然沒有宣佈一切,類型,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 ... }語法),但是如果你絕對需要泛型方法,這是可能的。

+1

使用函數是greate。我不需要訪問'ram','rom'和'romMode',但是我可以在哪裏存儲這些數據以使用後者? – MiP

+2

您可以將它們存儲在您的函數可以訪問的閉包中。看看我的例子:看看我在'controlSomething'的實現中如何使用'ram'和'rom'?你甚至可以讓它們變化! (雖然我強烈建議不要) –

+1

我忘了我正在使用F#,謝謝。 – MiP

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