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
  • What are types for?
  • What kinds of types are there?
  • Sum and Product types
  • How types are defined
  • Constructing and deconstructing types
  • Field guide to the "type" keyword

Was this helpful?

  1. Understanding F# ###
  2. The "Understanding F# types" Series

Overview of types in F#

A look at the big picture

Before we dive into all the specific types, let's look at the big picture.

What are types for?

If you are coming from an object-oriented design background, one of the paradigm shifts involved in "thinking functionally" is to change how you think about types.

A well designed object-oriented program will have a strong focus on behavior rather than data, so it will use a lot of polymorphism, either using "duck-typing" or explicit interfaces, and will try to avoid having explicit knowledge of the actual concrete classes being passed around.

A well designed functional program, on the other hand, will have a strong focus on data types rather than behavior. F# puts much more emphasis on designing types correctly than an imperative language such as C#, and many of the examples in this series and later series will focus on creating and refining type definitions.

So what is a type? Types are surprisingly hard to define. One definition from a well known textbook says:

"A type system is a tractable syntactic method of proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute" (Benjamin Pierce, Types and Programming Languages)

Ok, that definition is a bit technical. So let's turn it around -- what do we use types for in practice? In the context of F#, you can think of types as being used in two main ways:

  • Firstly, as an annotation to a value that allows certain checks to be made, especially at compile time. In other words, types allow you to have "compile time unit tests".

  • Second, as domains for functions to act upon. That is, a type is a sort of data modeling tool that allows you to represent a real world domain in your code.

These two definitions interact. The better the type definitions reflect the real-world domain, the better they will statically encode the business rules. And the better they statically encode the business rules, the better the "compile time unit tests" work. In the ideal scenario, if your program compiles, then it really is correct!

What kinds of types are there?

F# is a hybrid language, so it has a mixture of types: some from its functional background, and some from its object-oriented background.

Generally, the types in F# can be grouped into the following categories:

  • Common .NET types. These are types that conform to the .NET Common Language Infrastructure (CLI), and which are easily portable to every .NET language.

  • F# specific types. These are types that are part of the F# language and are designed for pure functional programming.

If you are familiar with C#, you will know all the CLI types. They include:

  • Built-in value types (int, bool, etc).

  • Built-in reference types (string, etc).

  • User-defined value types (enum and struct).

  • Classes and interfaces

  • Delegates

  • Arrays

The F# specific types include:

  • Lists (not the same as the .NET List class)

I strongly recommend that when creating new types you stick with the F# specific types rather than using classes. They have a number of advantages over the CLI types, such as:

  • They are immutable

  • They cannot be null

  • They have built-in structural equality and comparison

  • They have built-in pretty printing

Sum and Product types

The key to understanding the power of types in F# is that most new types are constructed by from other types using two basic operations: sum and product.

That is, in F# you can define new types almost as if you were doing algebra:

define typeZ = typeX "plus" typeY
define typeW = typeX "times" typeZ

I will hold off explaining what sum and product mean in practice until we get to the detailed discussion of tuples (products) and discriminated union (sum) types later in this series.

The key point is that an infinite number of new types can be made by combining existing types together using these "product" and "sum" methods in various ways. Collectively these are called "algebraic data types" or ADTs (not to be confused with abstract data types, also called ADTs). Algebraic data types can be used to model anything, including lists, trees, and other recursive types.

The sum or "union" types, in particular, are very valuable, and once you get used to them, you will find them indispensible!

How types are defined

Every type definition is similar, even though the specific details may vary. All type definitions start with a "type" keyword, followed by an identifier for the type, followed by any generic type parameters, followed by the definition. For example, here are some type definitions for a variety of types:

type A = int * int
type B = {FirstName:string; LastName:string}
type C = Circle of int | Rectangle of int * int
type D = Day | Month | Year
type E<'a> = Choice1 of 'a | Choice2 of 'a * 'a

type MyClass(initX:int) =
   let x = initX
   member this.Method() = printf "x=%i" x

Types can only be declared in namespaces or modules. But that doesn't mean you always have to create them at the top level -- you can create types in nested modules if you need to hide them.

module sub = 
    // type declared in a module
    type A = int * int

    module private helper = 
        // type declared in a submodule
        type B = B of string list

        //internal access is allowed
        let b = B ["a";"b"]

//outside access not allowed
let b = sub.helper.B ["a";"b"]

Types cannot be declared inside functions.

let f x = 
    type A = int * int  //unexpected keyword "type"
    x * x

Constructing and deconstructing types

After a type is defined, instances of the type are created using a "constructor" expression that often looks quite similar to the type definition itself.

let a = (1,1)
let b = { FirstName="Bob"; LastName="Smith" } 
let c = Circle 99
let c' = Rectangle (2,1)
let d = Month
let e = Choice1 "a"
let myVal = MyClass 99
myVal.Method()

What is interesting is that the same "constructor" syntax is also used to "deconstruct" the type when doing pattern matching:

let a = (1,1)                                  // "construct"
let (a1,a2) = a                                // "deconstruct"

let b = { FirstName="Bob"; LastName="Smith" }  // "construct"
let { FirstName = b1 } = b                     // "deconstruct" 

let c = Circle 99                              // "construct"
match c with                                   
| Circle c1 -> printf "circle of radius %i" c1 // "deconstruct"
| Rectangle (c2,c3) -> printf "%i %i" c2 c3    // "deconstruct"

let c' = Rectangle (2,1)                       // "construct"
match c' with                                   
| Circle c1 -> printf "circle of radius %i" c1 // "deconstruct"
| Rectangle (c2,c3) -> printf "%i %i" c2 c3    // "deconstruct"

As you read through this series, pay attention to how the constructors are used in both ways.

Field guide to the "type" keyword

The same "type" keyword is used to define all the F# types, so they can all look very similar if you are new to F#. Here is a quick list of these types and how to tell the difference between them.

Type

Example

Distinguishing features

Abbrev (Alias)

Uses equal sign only.

Tuple

Always available to be used and are not explicitly defined with the type keyword. Usage indicated by comma (with optional parentheses).

Record

Curly braces. Uses semicolon to separate fields.

Discriminated Union

Vertical bar character. Uses "of" for types.

Enum

Similar to Unions, but uses equals and an int value

Class

Has function-style parameter list after name for use as constructor. Has "member" keyword. Has "new" keyword for secondary constructors.

Interface

Same as class but all members are abstract. Abstract members have colon and type signature rather than a concrete implementation.

Struct

Has "struct" keyword. Uses "val" to define fields. Can have constructor.

PreviousUnderstanding F# types: IntroductionNextType abbreviations

Last updated 5 years ago

Was this helpful?

(not the same as delegates or C# lambdas)

(now part of .NET 4.0)

As we said in a , there is a special syntax for defining new types that is different from the normal expression syntax. So do be aware of this difference.

Function types
Tuples
Records
Discriminated Unions
Option types
previous post
The unit type