2

F# records cannot be inherited, but they can implement interfaces. 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?
  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?
MiP
  • 5,846
  • 3
  • 26
  • 41

2 Answers2

5

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. 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:

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?

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

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.

Community
  • 1
  • 1
Fyodor Soikin
  • 78,590
  • 9
  • 125
  • 172
  • 1
    Using functions is greate. I don't need to access `ram`, `rom` and `romMode`, but where can I store these data in to use latter? – MiP Apr 14 '17 at 15:40
  • 2
    You can store them in closures to which your functions have access. Look at my example: see how I'm using `ram` and `rom` within the implementation of `controlSomething`? You can even make them mutable! (though I would strongly advise against it) – Fyodor Soikin Apr 14 '17 at 15:41
2

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

CaringDev
  • 8,391
  • 1
  • 24
  • 43