F# for Fun and Profit
  • Introduction
  • Getting started
    • Contents of the book
    • "Why use F#?" in one page
    • Installing and using F#
    • F# syntax in 60 seconds
    • Learning F#
    • Troubleshooting F#
    • Low-risk ways to use F# at work
      • Twenty six low-risk ways to use F# at work
      • Using F# for development and devops scripts
      • Using F# for testing
      • Using F# for database related tasks
      • Other interesting ways of using F# at work
  • Why use F#?
    • The "Why use F#?" Series
      • Introduction to the 'Why use F#' series
      • F# syntax in 60 seconds
      • Comparing F# with C#: A simple sum
      • Comparing F# with C#: Sorting
      • Comparing F# with C#: Downloading a web page
      • Four Key Concepts
      • Conciseness
      • Type inference
      • Low overhead type definitions
      • Using functions to extract boilerplate code
      • Using functions as building blocks
      • Pattern matching for conciseness
      • Convenience
      • Out-of-the-box behavior for types
      • Functions as interfaces
      • Partial Application
      • Active patterns
      • Correctness
      • Immutability
      • Exhaustive pattern matching
      • Using the type system to ensure correct code
      • Worked example: Designing for correctness
      • Concurrency
      • Asynchronous programming
      • Messages and Agents
      • Functional Reactive Programming
      • Completeness
      • Seamless interoperation with .NET libraries
      • Anything C# can do...
      • Why use F#: Conclusion
  • Thinking Functionally
    • The "Thinking Functionally" Series
      • Thinking Functionally: Introduction
      • Mathematical functions
      • Function Values and Simple Values
      • How types work with functions
      • Currying
      • Partial application
      • Function associativity and composition
      • Defining functions
      • Function signatures
      • Organizing functions
      • Attaching functions to types
      • Worked example: A stack based calculator
  • Understanding F# ###
    • The "Expressions and syntax" Series
      • Expressions and syntax: Introduction
      • Expressions vs. statements
      • Overview of F# expressions
      • Binding with let, use, and do
      • F# syntax: indentation and verbosity
      • Parameter and value naming conventions
      • Control flow expressions
      • Exceptions
      • Match expressions
      • Formatted text using printf
      • Worked example: Parsing command line arguments
      • Worked example: Roman numerals
    • The "Understanding F# types" Series
      • Understanding F# types: Introduction
      • Overview of types in F#
      • Type abbreviations
      • Tuples
      • Records
      • Discriminated Unions
      • The Option type
      • Enum types
      • Built-in .NET types
      • Units of measure
      • Understanding type inference
    • Choosing between collection functions
    • The "Object-oriented programming in F#" Series
      • Object-oriented programming in F#: Introduction
      • Classes
      • Inheritance and abstract classes
      • Interfaces
      • Object expressions
    • The "Computation Expressions" Series
      • Computation expressions: Introduction
      • Understanding continuations
      • Introducing 'bind'
      • Computation expressions and wrapper types
      • More on wrapper types
      • Implementing a builder: Zero and Yield
      • Implementing a builder: Combine
      • Implementing a builder: Delay and Run
      • Implementing a builder: Overloading
      • Implementing a builder: Adding laziness
      • Implementing a builder: The rest of the standard methods
    • Organizing modules in a project
    • The "Dependency cycles" Series
      • Cyclic dependencies are evil
      • Refactoring to remove cyclic dependencies
      • Cycles and modularity in the wild
    • The "Porting from C#" Series
      • Porting from C# to F#: Introduction
      • Getting started with direct porting
  • Functional Design ###
    • The "Designing with types" Series
      • Designing with types: Introduction
      • Single case union types
      • Making illegal states unrepresentable
      • Discovering new concepts
      • Making state explicit
      • Constrained strings
      • Non-string types
      • Designing with types: Conclusion
    • Algebraic type sizes and domain modelling
    • Thirteen ways of looking at a turtle
      • Thirteen ways of looking at a turtle (part 2)
      • Thirteen ways of looking at a turtle - addendum
  • Functional Patterns ###
    • How to design and code a complete program
    • A functional approach to error handling (Railway oriented programming)
      • Railway oriented programming: Carbonated edition
    • The "Understanding monoids" Series
      • Monoids without tears
      • Monoids in practice
      • Working with non-monoids
    • The "Understanding Parser Combinators" Series
      • Understanding Parser Combinators
      • Building a useful set of parser combinators
      • Improving the parser library
      • Writing a JSON parser from scratch
    • The "Handling State" Series
      • Dr Frankenfunctor and the Monadster
      • Completing the body of the Monadster
      • Refactoring the Monadster
    • The "Map and Bind and Apply, Oh my!" Series
      • Understanding map and apply
      • Understanding bind
      • Using the core functions in practice
      • Understanding traverse and sequence
      • Using map, apply, bind and sequence in practice
      • Reinventing the Reader monad
      • Map and Bind and Apply, a summary
    • The "Recursive types and folds" Series
      • Introduction to recursive types
      • Catamorphism examples
      • Introducing Folds
      • Understanding Folds
      • Generic recursive types
      • Trees in the real world
    • The "A functional approach to authorization" Series
      • A functional approach to authorization
      • Constraining capabilities based on identity and role
      • Using types as access tokens
  • Testing
    • An introduction to property-based testing
    • Choosing properties for property-based testing
  • Examples and Walkthroughs
    • Worked example: Designing for correctness
    • Worked example: A stack based calculator
    • Worked example: Parsing command line arguments
    • Worked example: Roman numerals
    • Commentary on 'Roman Numerals Kata with Commentary'
    • Calculator Walkthrough: Part 1
      • Calculator Walkthrough: Part 2
      • Calculator Walkthrough: Part 3
      • Calculator Walkthrough: Part 4
    • Enterprise Tic-Tac-Toe
      • Enterprise Tic-Tac-Toe, part 2
    • Writing a JSON parser from scratch
  • Other
    • Ten reasons not to use a statically typed functional programming language
    • Why I won't be writing a monad tutorial
    • Is your programming language unreasonable?
    • We don't need no stinking UML diagrams
    • Introvert and extrovert programming languages
    • Swapping type-safety for high performance using compiler directives
Powered by GitBook
On this page
  • Single case unions
  • Constraints on integers
  • Embedding business rules in the type
  • Constraints on dates
  • Union types vs. units of measure

Was this helpful?

  1. Functional Design ###
  2. The "Designing with types" Series

Non-string types

Working with integers and dates safely

In this series we've seen a lot of uses of single case discriminated unions to wrap strings.

There is no reason why you cannot use this technique with other primitive types, such as numbers and dates. Let's look a few examples.

Single case unions

In many cases, we want to avoid accidentally mixing up different kinds of integers. Two domain objects may have the same representation (using integers) but they should never be confused.

For example, you may have an OrderId and a CustomerId, both of which are stored as ints. But they are not really ints. You cannot add 42 to a CustomerId, for example. And CustomerId(42) is not equal to OrderId(42). In fact, they should not even be allowed to be compared at all.

Types to the rescue, of course.

type CustomerId = CustomerId of int
type OrderId = OrderId of int

let custId = CustomerId 42
let orderId = OrderId 42

// compiler error
printfn "cust is equal to order? %b" (custId = orderId)

Similarly, you might want avoid mixing up semantically different date values by wrapping them in a type. (DateTimeKind is an attempt at this, but not always reliable.)

type LocalDttm = LocalDttm of System.DateTime
type UtcDttm = UtcDttm of System.DateTime

With these types we can ensure that we always pass the right kind of datetime as parameters. Plus, it acts as documentation as well.

let SetOrderDate (d:LocalDttm) = 
    () // do something

let SetAuditTimestamp (d:UtcDttm) = 
    () // do something

Constraints on integers

Just as we had validation and constraints on types such as String50 and ZipCode, we can use the same approach when we need to have constraints on integers.

For example, an inventory management system or a shopping cart may require that certain types of number are always positive. You might ensure this by creating a NonNegativeInt type.

module NonNegativeInt = 
    type T = NonNegativeInt of int

    let create i = 
        if (i >= 0 )
        then Some (NonNegativeInt i)
        else None

module InventoryManager = 

    // example of NonNegativeInt in use
    let SetStockQuantity (i:NonNegativeInt.T) = 
        //set stock
        ()

Embedding business rules in the type

Just as we wondered earlier whether first names could ever be 64K characters long, can you really add 999999 items to your shopping cart?

Is it worth trying to avoid this issue by using constrained types? Let's look at some real code.

Here is a very simple shopping cart manager using a standard int type for the quantity. The quantity is incremented or decremented when the related buttons are clicked. Can you find the obvious bug?

module ShoppingCartWithBug = 

    let mutable itemQty = 1  // don't do this at home!

    let incrementClicked() = 
        itemQty <- itemQty + 1

    let decrementClicked() = 
        itemQty <- itemQty - 1

If you can't quickly find the bug, perhaps you should consider making any constraints more explicit.

Here is the same simple shopping cart manager using a typed quantity instead. Can you find the bug now? (Tip: paste the code into a F# script file and run it)

module ShoppingCartQty = 

    type T = ShoppingCartQty of int

    let initialValue = ShoppingCartQty 1

    let create i = 
        if (i > 0 && i < 100)
        then Some (ShoppingCartQty i)
        else None

    let increment t = create (t + 1)
    let decrement t = create (t - 1)

module ShoppingCartWithTypedQty = 

    let mutable itemQty = ShoppingCartQty.initialValue

    let incrementClicked() = 
        itemQty <- ShoppingCartQty.increment itemQty

    let decrementClicked() = 
        itemQty <- ShoppingCartQty.decrement itemQty

You might think this is overkill for such a trivial problem. But if you want to avoid being in the DailyWTF, it might be worth considering.

Constraints on dates

Not all systems can handle all possible dates. Some systems can only store dates going back to 1/1/1980, and some systems can only go into the future up to 2038 (I like to use 1/1/2038 as a max date to avoid US/UK issues with month/day order).

As with integers, it might be useful to have constraints on the valid dates built into the type, so that any out of bound issues are dealt with at construction time rather than later on.

type SafeDate = SafeDate of System.DateTime

let create dttm = 
    let min = new System.DateTime(1980,1,1)
    let max = new System.DateTime(2038,1,1)
    if dttm < min || dttm > max
    then None
    else Some (SafeDate dttm)

Union types vs. units of measure

Yes and no. Units of measure can indeed be used to avoid mixing up numeric values of different type, and are much more powerful than the single case unions we've been using.

On the other hand, units of measure are not encapsulated and cannot have constraints. Anyone can create a int with unit of measure <kg> say, and there is no min or max value.

In many cases, both approaches will work fine. For example, there are many parts of the .NET library that use timeouts, but sometimes the timeouts are set in seconds, and sometimes in milliseconds. I often have trouble remembering which is which. I definitely don't want to accidentally use a 1000 second timeout when I really meant a 1000 millisecond timeout.

To avoid this scenario, I often like to create separate types for seconds and milliseconds.

Here's a type based approach using single case unions:

type TimeoutSecs = TimeoutSecs of int
type TimeoutMs = TimeoutMs of int

let toMs (TimeoutSecs secs)  = 
    TimeoutMs (secs * 1000)

let toSecs (TimeoutMs ms) = 
    TimeoutSecs (ms / 1000)

/// sleep for a certain number of milliseconds
let sleep (TimeoutMs ms) = 
    System.Threading.Thread.Sleep ms

/// timeout after a certain number of seconds    
let commandTimeout (TimeoutSecs s) (cmd:System.Data.IDbCommand) = 
    cmd.CommandTimeout <- s

And here's the same thing using units of measure:

[<Measure>] type sec 
[<Measure>] type ms

let toMs (secs:int<sec>) = 
    secs * 1000<ms/sec>

let toSecs (ms:int<ms>) = 
    ms / 1000<ms/sec>

/// sleep for a certain number of milliseconds
let sleep (ms:int<ms>) = 
    System.Threading.Thread.Sleep (ms * 1<_>)

/// timeout after a certain number of seconds    
let commandTimeout (s:int<sec>) (cmd:System.Data.IDbCommand) = 
    cmd.CommandTimeout <- (s * 1<_>)

Which approach is better?

If you are doing lots of arithmetic on them (adding, multiplying, etc) then the units of measure approach is much more convenient, but otherwise there is not much to choose between them.

PreviousConstrained stringsNextDesigning with types: Conclusion

Last updated 5 years ago

Was this helpful?

You might be asking at this point: What about ? Aren't they meant to be used for this purpose?

units of measure
State transition diagram: Package Delivery