Quatro - Step 2
As we converted our model to F#, we brought in some immutability. What we created can form the basis of a
fully operational F# application; but we can do better. For example, given "faad05f6-c539-44b7-9e94-1b68da4bba57" -
quick! Is it a post Id? A page Id? The text of a really lame blog post? Also, what is to prevent us from using a
CommentStatus
value in a spot where a PostStatus
should go? (Do you really want your own post to be able to be
flagged as spam?)
To be sure, these same problems exist in most OO realms, and developers manage to keep all the strings separate.
However, just as immutability gets rid of null
checks, F# has features that go even further, and can help us create a
model where invalid states cannot be represented. F# for Fun and Profit has a great series on
Designing with Types, and I highly recommend
reading it; it goes into way more depth that we're going to at this point.
The language feature we're going to use is called discriminated unions (or "DUs" for short). You've probably dealt
with enum
s in C#; that is the closest parallel to DUs, but there are significant differences. Like enum
s, DUs are
an exhaustive list of all expected/valid values. Unlike enum
s, though, they are not wrappers over another type; they
are their own type. Also, each condition does not have to have the same type; it's perfectly valid to have a DU with
one condition that has one type (or no type at all), and other condition with a completely different type. (We don't
use that with these types.)
OK, enough talking; some code will help it make sense. One of the forms of a DU is called a single-case discriminated union; it can be used to wrap primitives to make them more meaningful. We'll create the following single-case DUs:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
It may be confusing that we're using the same name twice; the name after the type
keyword defines the type, while the
one after the equals sign defines the constructor for this type (CategoryId "abc"
defines a category Id whose value
is the string "abc"). We'll look at these implemented in a bit; next, though, we'll convert our
static-classes-turned-modules into multi-case DUs.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
|
This is similar in concept to the single-case DUs, but there are no parameters required for the constructor.
What does a record look like updated with these types? Let's revisit the Page
type we dissected for Tres.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: |
|
The only primitives* we now have are the Title
and Text
fields (which are both free-form text) and the
ShowInPageList
field (for which yes/no is sufficient, although we could create a PageListVisibility
DU to constrain
the yes/no values and distinguish them from others). The compiler will prevent us from crossing boundaries on every
other field in this type!
Let's take a look at the Empty
property on the Post
type to see a multi-case DU in use.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: |
|
Status
is defined as type PostStatus
; to set its value, we simply have to write Draft
. No quotes, no dotted
access**, just Status = Draft
. (Status = Spam
does not compile.)
You can review the entire set of types to see where these various DUs were used. While we could certainly take this much further, these simple changes have made our types more meaningful, while eliminating a lot of the invalid states we could have assigned in our code.
* - string
is a primitive for our purposes here.
** - If our DU condition is not unique, it would need to be qualified. For example, if we were to add a "Draft"
CommentStatus
so we could auto-save comment text while the visitor was typing***, we would need to change the
Empty
property to assign PostStatus.Draft
instead. Again, though, the compiler would help us spot that right away.
*** - This is a really bad idea; don't do this.
{AsOf: int64;
Text: string;}
static member Empty : Revision
Full name: Quatro.Entities.Revision
val int64 : value:'T -> int64 (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int64
--------------------
type int64 = System.Int64
Full name: Microsoft.FSharp.Core.int64
--------------------
type int64<'Measure> = int64
Full name: Microsoft.FSharp.Core.int64<_>
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
Full name: Quatro.Entities.Revision.Empty
union case CategoryId.CategoryId: string -> CategoryId
--------------------
type CategoryId = | CategoryId of string
Full name: Quatro.Entities.CategoryId
union case CommentId.CommentId: string -> CommentId
--------------------
type CommentId = | CommentId of string
Full name: Quatro.Entities.CommentId
union case PageId.PageId: string -> PageId
--------------------
type PageId = | PageId of string
Full name: Quatro.Entities.PageId
union case PostId.PostId: string -> PostId
--------------------
type PostId = | PostId of string
Full name: Quatro.Entities.PostId
union case UserId.UserId: string -> UserId
--------------------
type UserId = | UserId of string
Full name: Quatro.Entities.UserId
union case WebLogId.WebLogId: string -> WebLogId
--------------------
type WebLogId = | WebLogId of string
Full name: Quatro.Entities.WebLogId
union case Permalink.Permalink: string -> Permalink
--------------------
type Permalink = | Permalink of string
Full name: Quatro.Entities.Permalink
union case Tag.Tag: string -> Tag
--------------------
type Tag = | Tag of string
Full name: Quatro.Entities.Tag
union case Ticks.Ticks: int64 -> Ticks
--------------------
type Ticks = | Ticks of int64
Full name: Quatro.Entities.Ticks
union case TimeZone.TimeZone: string -> TimeZone
--------------------
type TimeZone = | TimeZone of string
Full name: Quatro.Entities.TimeZone
union case Url.Url: string -> Url
--------------------
type Url = | Url of string
Full name: Quatro.Entities.Url
| Administrator
| User
Full name: Quatro.Entities.AuthorizationLevel
| Draft
| Published
Full name: Quatro.Entities.PostStatus
| Approved
| Pending
| Spam
Full name: Quatro.Entities.CommentStatus
{Id: PageId;
WebLogId: WebLogId;
AuthorId: UserId;
Title: string;
Permalink: Permalink;
PublishedOn: Ticks;
UpdatedOn: Ticks;
ShowInPageList: bool;
Text: string;
Revisions: Revision list;}
static member Empty : Page
Full name: Quatro.Entities.Page
type JsonPropertyAttribute =
inherit Attribute
new : unit -> JsonPropertyAttribute + 1 overload
member DefaultValueHandling : DefaultValueHandling with get, set
member IsReference : bool with get, set
member ItemConverterParameters : obj[] with get, set
member ItemConverterType : Type with get, set
member ItemIsReference : bool with get, set
member ItemReferenceLoopHandling : ReferenceLoopHandling with get, set
member ItemTypeNameHandling : TypeNameHandling with get, set
member NamingStrategyParameters : obj[] with get, set
member NamingStrategyType : Type with get, set
...
Full name: Newtonsoft.Json.JsonPropertyAttribute
--------------------
JsonPropertyAttribute() : unit
JsonPropertyAttribute(propertyName: string) : unit
Page.WebLogId: WebLogId
--------------------
type WebLogId = | WebLogId of string
Full name: Quatro.Entities.WebLogId
Page.Permalink: Permalink
--------------------
type Permalink = | Permalink of string
Full name: Quatro.Entities.Permalink
Full name: Microsoft.FSharp.Core.bool
Full name: Microsoft.FSharp.Collections.list<_>
Full name: Quatro.Entities.Page.Empty
{Id: PostId;
WebLogId: WebLogId;
AuthorId: UserId;
Status: PostStatus;
Title: string;
Permalink: string;
PublishedOn: Ticks;
UpdatedOn: Ticks;
Text: string;
CategoryIds: CategoryId list;
...}
static member Empty : Post
Full name: Quatro.Entities.Post
Post.WebLogId: WebLogId
--------------------
type WebLogId = | WebLogId of string
Full name: Quatro.Entities.WebLogId
Post.Permalink: string
--------------------
type Permalink = | Permalink of string
Full name: Quatro.Entities.Permalink
Full name: Quatro.Entities.Post.Empty