objects () |> functions


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 enums in C#; that is the closest parallel to DUs, but there are significant differences. Like enums, DUs are an exhaustive list of all expected/valid values. Unlike enums, 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: 
type CategoryId = CategoryId of string
type CommentId  = CommentId  of string 
type PageId     = PageId     of string
type PostId     = PostId     of string
type UserId     = UserId     of string
type WebLogId   = WebLogId   of string

type Permalink = Permalink of string
type Tag       = Tag       of string
type Ticks     = Ticks     of int64
type TimeZone  = TimeZone  of string
type Url       = Url       of string

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: 
type AuthorizationLevel =
  | Administrator
  | User

type PostStatus =
  | Draft
  | Published

type CommentStatus =
  | Approved
  | Pending
  | Spam

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: 
type Page = {
  [<JsonProperty("id")>]
  Id : PageId
  WebLogId : WebLogId
  AuthorId : UserId
  Title : string
  Permalink : Permalink
  PublishedOn : Ticks
  UpdatedOn : Ticks
  ShowInPageList : bool
  Text : string
  Revisions : Revision list
  }
with
  static member Empty = 
    { Id             = PageId ""
      WebLogId       = WebLogId ""
      AuthorId       = UserId ""
      Title          = ""
      Permalink      = Permalink ""
      PublishedOn    = Ticks 0L
      UpdatedOn      = Ticks 0L
      ShowInPageList = false
      Text           = ""
      Revisions      = []
    }

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: 
  static member Empty =
    { Id              = PostId "new"
      WebLogId        = WebLogId ""
      AuthorId        = UserId ""
      Status          = Draft
      Title           = ""
      Permalink       = ""
      PublishedOn     = Ticks 0L
      UpdatedOn       = Ticks 0L
      Text            = ""
      CategoryIds     = []
      Tags            = []
      Revisions       = []
      }

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.

Back to Step 2


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

namespace Newtonsoft
namespace Newtonsoft.Json
type Revision =
  {AsOf: int64;
   Text: string;}
  static member Empty : Revision

Full name: Quatro.Entities.Revision
Revision.AsOf: int64
Multiple items
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<_>
Revision.Text: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
static member Revision.Empty : Revision

Full name: Quatro.Entities.Revision.Empty
Multiple items
union case CategoryId.CategoryId: string -> CategoryId

--------------------
type CategoryId = | CategoryId of string

Full name: Quatro.Entities.CategoryId
Multiple items
union case CommentId.CommentId: string -> CommentId

--------------------
type CommentId = | CommentId of string

Full name: Quatro.Entities.CommentId
Multiple items
union case PageId.PageId: string -> PageId

--------------------
type PageId = | PageId of string

Full name: Quatro.Entities.PageId
Multiple items
union case PostId.PostId: string -> PostId

--------------------
type PostId = | PostId of string

Full name: Quatro.Entities.PostId
Multiple items
union case UserId.UserId: string -> UserId

--------------------
type UserId = | UserId of string

Full name: Quatro.Entities.UserId
Multiple items
union case WebLogId.WebLogId: string -> WebLogId

--------------------
type WebLogId = | WebLogId of string

Full name: Quatro.Entities.WebLogId
Multiple items
union case Permalink.Permalink: string -> Permalink

--------------------
type Permalink = | Permalink of string

Full name: Quatro.Entities.Permalink
Multiple items
union case Tag.Tag: string -> Tag

--------------------
type Tag = | Tag of string

Full name: Quatro.Entities.Tag
Multiple items
union case Ticks.Ticks: int64 -> Ticks

--------------------
type Ticks = | Ticks of int64

Full name: Quatro.Entities.Ticks
Multiple items
union case TimeZone.TimeZone: string -> TimeZone

--------------------
type TimeZone = | TimeZone of string

Full name: Quatro.Entities.TimeZone
Multiple items
union case Url.Url: string -> Url

--------------------
type Url = | Url of string

Full name: Quatro.Entities.Url
type AuthorizationLevel =
  | Administrator
  | User

Full name: Quatro.Entities.AuthorizationLevel
union case AuthorizationLevel.Administrator: AuthorizationLevel
union case AuthorizationLevel.User: AuthorizationLevel
type PostStatus =
  | Draft
  | Published

Full name: Quatro.Entities.PostStatus
union case PostStatus.Draft: PostStatus
union case PostStatus.Published: PostStatus
type CommentStatus =
  | Approved
  | Pending
  | Spam

Full name: Quatro.Entities.CommentStatus
union case CommentStatus.Approved: CommentStatus
union case CommentStatus.Pending: CommentStatus
union case CommentStatus.Spam: CommentStatus
type Page =
  {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
Multiple items
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.Id: PageId
Multiple items
Page.WebLogId: WebLogId

--------------------
type WebLogId = | WebLogId of string

Full name: Quatro.Entities.WebLogId
Page.AuthorId: UserId
Page.Title: string
Multiple items
Page.Permalink: Permalink

--------------------
type Permalink = | Permalink of string

Full name: Quatro.Entities.Permalink
Page.PublishedOn: Ticks
Page.UpdatedOn: Ticks
Page.ShowInPageList: bool
type bool = System.Boolean

Full name: Microsoft.FSharp.Core.bool
Page.Text: string
Page.Revisions: Revision list
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
static member Page.Empty : Page

Full name: Quatro.Entities.Page.Empty
type Post =
  {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.Id: PostId
Multiple items
Post.WebLogId: WebLogId

--------------------
type WebLogId = | WebLogId of string

Full name: Quatro.Entities.WebLogId
Post.AuthorId: UserId
Post.Status: PostStatus
Post.Title: string
Multiple items
Post.Permalink: string

--------------------
type Permalink = | Permalink of string

Full name: Quatro.Entities.Permalink
Post.PublishedOn: Ticks
Post.UpdatedOn: Ticks
Post.Text: string
Post.CategoryIds: CategoryId list
Post.Tags: Tag list
Post.Revisions: Revision list
static member Post.Empty : Post

Full name: Quatro.Entities.Post.Empty
Fork me on GitHub