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#.
Type Aliases allow defining types with a custom name (an Alias).
In Typescript, Type aliases give a type a new name. They are similar to interfaces in that they can be used to name primitives and any other kinds that you'd have to define by hand otherwise. Aliasing doesn't truly create a new type; instead, it gives that type a new name.
typedef is a reserved keyword in the programming languages C and C++. It is used to create an additional name (alias) for another data type, but does not create a new type, except in the obscure case of a qualified typedef of an array type where the typedef qualifiers are transferred to the array element type.
Type aliases are created using the keyword type followed by its name, an equals sign = , and a type definition. Any type can appear inside a type alias.
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} -> ...
I've read my fair 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With