Question

Items in tuples don't have names, which means that you often don't have a clear way to document the meanings of each item.

For instance, in this discriminated union:

type NetworkEvent =
| Message of string * string * string
| ...

I want to make it clear that the first and second items are the sender and recipient names respectively. Is it good practice to do something like this:

type SenderName = string
type RecipientName = string

type NetworkEvent =
| Message of SenderName * RecipientName * string
| ...

A lot of C/C++ libraries have a proliferation of types (e.g. win32.h), but in those languages, even though parameter names are optional in many cases, it can still be done. That isn't the case with F#.

Était-ce utile?

La solution

I think that using type aliases for documentation purposes is a good and simple way to document your discriminated unions. I use the same approach in many of my demos (see for example this one) and I know that some people use it in production applications too. I think there are two ways to make the definition more self-explanatory:

Use type aliases: This way, you add some documentation that is visible in the IntelliSense, but it doesn't propagate through the type system - when you used a value of the aliased type, the compiler will treat it as string, so you don't see the additional documentation everywhere.

Use single-case unions This is a pattern that has been used in some places of the F# compiler. It makes the information more visible than using type-aliases, because a type SenderName is actually a different type than string (on the other hand, this may have some small performance penalty):

type SenderName = SenderName of string
type RecipientName = RecipientName of string
type NetworkElement =
  | Message of SenderName * RecipietName * string

match netelem with
| Message(SenderName sender, RecipientName recipiet, msg) -> ...

Use records: This way, you explicitly define a record to carry the information of a union case. This is more syntactically verbose, but it probably adds the additional information in the most accessible way. You can still use pattern matching on records, or you can use dot notation to access elements. It is also easier to add new fields during the development:

type MessageData = 
  { SenderName : string; RecipientName : string; Message : string }
type NetworkEvent = 
  | Message of MessageData

match netelem with
| Message{ SenderName = sender; RecipientName = recipiet; Message = msg} -> ...

Autres conseils

I've read my fare share of F#, both on the internet and in books but have never seen anyone use aliases as a form of documentation. So I'm going to say it's not standard practice. It could also be viewed as a form of code duplication.

In general a specific tuple representation should only be used as a temporary data structure within a function. If you're storing a tuple for a long time or passing it between different classes, then it's time to make a record.

If you're going to use a discriminated union across multiple classes then use records as you suggested or keep all the methods scoped to the discriminated union like below.

type NetworkEvent =
    | Message of string * string * string

    static member Create(sender, recipient, message) =
        Message(sender, recipient, message)

    member this.Send() =
        math this with
        | Message(sender, recipient, message) -> 
            printf "Sent: %A" message

let message = NetworkEvent.Create("me", "you", "hi")

You can use records in pattern matching, so tuples are really a matter of convenience and should be replaced by records as the code grows.

If a discriminated union has a bunch of tuples with the same signature then it's time to break it into two discriminated unions. This will also prevent you from having multiple records with the same signature.

type NetworkEvent2 =
    | UDPMessage of string * string * string
    | Broadcast of string * string * string
    | Loopback of string * string * string
    | ConnectionRequest of string
    | FlushEventQueue

into

type MessageType =
    | UDPMessage
    | Broadcast
    | Loopback

type NetworkEvent =
    | Message of MessageType * string * string * string
    | ConnectionRequest of string
    | FlushEventQueue

I think In this case you would be better off using a record type for the first two elements of the tuple to reinforce the fact that the order matters.

For numbers, you can do something slightly more elegant using units of measure, but this doesn't work for strings

I don't see anything wrong with this, but it might get annoying having 10 aliases for string, for example. If that doesn't bother you, I say, go ahead. Personally, I wish something like the following was supported:

type NetworkEvent =
| Message of SenderName:string * RecipientName:string * string
| ...

Then Intellisense could offer some help. (EDIT: Vote on this suggestion here.)

If your cases have more than a few fields, or several of the same type, you might consider using a class hierarchy instead.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top