TDD with F# (since 2003)
Anton Moldovan (@antyadev)
SBTech
Twitter: https://twitter.com/antyaDev
Github: https://github.com/antyaDev
About me:
@AntyaDev
like types*
@ploeh
@ploeh
@ploeh
@ploeh
Free
monads






In OOP you by default thinking about
abstraction
extensibility
In FP you by default thinking about
purity
composability
correctness
In FP you build your ideal, private world
where
you know
everything
Pure
Domain
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,
• will use a lot of polymorphism (interfaces),
• 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
type UserName = {
firstName: string
lastName: string
}
type Shape =
| Circle of int
| Rectangle of int * int
type UserName = {
firstName: string;
lastName: string
}
let a = { firstName = "a"; lastName = "b" }
let b = { firstName = "a"; lastName = "b" }
if a = b then "equals" // true
type Money = {
amount: decimal
currency: Currency
}
let a = { amount = 10; currency = USD }
let b = { amount = 10; currency = USD }
if a = b then "equals" // true
type Shape =
| Rectangle = 0
| Circle = 1
| Prism = 2
type Shape =
| Rectangle of width:float * length:float
| Circle of radius:float
| Prism of width:float * float * height:float
let rectangle = Rectangle(width = 6.2, length = 5.5)
anyone can set this to ‘true’
Rule 1: if the email is changed, the verified flag must be reset to ‘false’.
Rule 2: the verified flag can only be set by a special verification service.
Rule 3: we have 5 services which works only for verified email
and 5 services which works for invalid email.
class EmailContact
{
public string EmailAddress { get; set; }
public bool IsEmailVerified { get; set; }
}
Rule 3: we have - 5 services which works only for verified email
and - 5 services which works for invalid email.
if (emailContract.IsEmailVerified)
void SendEmailToApprove(EmailContact emailContract)
{
if (emailContract.IsEmailVerified)
}
void SendEmailToReject(EmailContact emailContract)
{
if (emailContract.IsEmailVerified)
}
void SendEmailToConfirm(EmailContact emailContract)
{
if (emailContract.IsEmailVerified)
}
void SendEmailToLinkedin(EmailContact emailContract)
{
if (emailContract.IsEmailVerified)
}
type ValidEmail = { email: string }
type InvalidEmail = { email: string }
type Email =
| Valid of ValidEmail
| Invalid of InvalidEmail
let sendEmailToLinkedin (email: ValidEmail) = ...
You need only one dispatch in one place
•
•
•
public class NaiveShoppingCart<TItem>
{
private List<TItem> items;
private decimal paidAmount;
public NaiveShoppingCart()
{
this.items = new List<TItem>();
this.paidAmount = 0;
}
/// Is cart paid for?
public bool IsPaidFor => this.paidAmount > 0;
public IEnumerable<TItem> Items => this.items;
public void AddItem(TItem item)
{
if (!this.IsPaidFor) this.items.Add(item);
}
if (!this.IsPaidFor) { do something }
public class NaiveShoppingCart<TItem>
{
private List<TItem> items;
public bool IsPaidFor => this.paidAmount > 0;
public bool IsPaidFor => this.paidAmount > 0;
public bool IsConfirmedByUser => _isConfirmedByUser;
public bool IsApproved => IsPaidFor && _isValid;
public bool IsCanceledByUser => _isCanceled && _user != null;
public bool IsCanceledAuto => _isCanceled || _user == null && _system != null;
public bool IsCanceledAdmin => _isCanceled || _user == null && _system == null && _admin != null;
public Status GetStatus()
{
if (_isCanceled && items.Count > 0)
return Status.Invalid;
else if (items.Count > 0)
return Status.Active;
return Status.Empty;
}
public void Approve()
{
if (_isCanceled) throw new Exception();
if (items.Count > 0)
}
what about recently added IsCanceledByAdmin?
•
•
•
type CartItem = string // placeholder for a more complicated type
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal}
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal }
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
// =============================
// operations on empty state
// =============================
let addToEmptyState (item: CartItem) : Cart.Active =
Cart.Active { unpaidItems = [item] } // a new Active Cart
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal }
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
// =============================
// operation on empty state
// =============================
let addToEmptyState item =
{ unpaidItems = [item] } // returns a new Active Cart
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal }
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
// =============================
// operation on active state
// =============================
let addToActiveState (state: ActiveState, itemToAdd: CartItem) =
let newList = itemToAdd :: state.unpaidItems
Cart.Active { state with unpaidItems = newList }
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal }
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
// =============================
// operation on active state
// =============================
let addToActiveState state itemToAdd =
let newList = itemToAdd :: state.unpaidItems
{ state with unpaidItems = newList }
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal }
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
let removeFromActiveState state itemToRemove =
let newList = state.unpaidItems
|> List.filter (fun i -> i <> itemToRemove)
match newList with
| [] -> Cart.Empty NoItems
| _ -> Cart.Active { state with unpaidItems = newList }
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal }
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
let payForActiveState state amount =
Cart.PaidFor { paidItems = state.unpaidItems
payment = amount }
type EmptyState = NoItems
type ActiveState = { unpaidItems: CartItem list; }
type PaidForState = { paidItems: CartItem list; payment: decimal}
type Cart =
| Empty of EmptyState
| Active of ActiveState
| PaidFor of PaidForState
let item = “test_product”
let activeState = addToEmptyState(item)
let paidState = payForActiveState(activeState, 10)
// compile error, your state is not active anymore
let activeState = addToActiveState(paidState, item)
errors
let failingFunc num =
let x = raise (new System.Exception("fail!"))
try
let y = 42 + 5 + num
x + y
with
e -> 43
/// Represents the result of a computation
type Result<'ok, 'msg> =
| Ok of 'ok * 'msg list
| Fail of 'msg list
type Request = { name: string; email: string }
let validateInput input =
if input.name = ""
then Fail("Name must not be blank")
elif input.email = ""
then Fail("Email must not be blank")
else Ok(input)
type Request = { name: string; email: string }
let validateInput input =
if input.name = ""
then fail "Name must not be blank"
elif input.email = ""
then fail "Email must not be blank"
else ok input
let validate1 input =
if input.name = "" then fail "Name must not be blank“
else ok input
let validate2 input =
if input.name.Length > 50 then fail "Name must not be longer than 50 chars"
else ok input
let validate3 input =
if input.email = "" then fail "Email must not be blank"
else ok input
let validRequest = validate1 >>= validate2 >>= validate3 >>= validate4
In functional programming we strive to write side-effect free
applications. In other words, all the functions of the
application should be pure. However, completely side-effect
free applications are mostly useless, so the next best thing is
to minimize the amount of side-effects, make them
explicit and push them as close to the boundaries of the
application as possible.
Let’s see an example in invoicing domain. When changing a due date of
an invoice we want to check that the new due date is in the future. We
could implement it like this:
let changeDueDate (newDueDate:DateTime, invoice) =
if newDueDate > System.DateTime.Today
then ok { invoice with dueDate = newDueDate }
else fail "Due date must be in future."
let changeDueDate (newDueDate:DateTime,
currentDate:DateTime, invoice) =
if newDueDate > currentDate
then ok { invoice with dueDate = newDueDate }
else fail "Due date must be in future."
type PastDate = PastDate of DateTime
type CurrentDate = CurrentDate of DateTime
type FutureDate = FutureDate of DateTime
type Date =
| Past of PastDate
| Current of CurrentDate
| Future of FutureDate
let changeDueDate (newDueDate:FutureDate, invoice) =
{ invoice with DueDate = Date newDueDate }
Problem: Language do not integrate information
- We need to bring information into the language…
Антон Молдован "Type driven development with f#"
Антон Молдован "Type driven development with f#"
Антон Молдован "Type driven development with f#"

Антон Молдован "Type driven development with f#"

  • 1.
    TDD with F#(since 2003) Anton Moldovan (@antyadev) SBTech Twitter: https://twitter.com/antyaDev Github: https://github.com/antyaDev
  • 2.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 11.
  • 12.
    In OOP youby default thinking about abstraction extensibility
  • 13.
    In FP youby default thinking about purity composability correctness
  • 14.
    In FP youbuild your ideal, private world where you know everything
  • 15.
  • 17.
    If you arecoming 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, • will use a lot of polymorphism (interfaces), • 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
  • 18.
    type UserName ={ firstName: string lastName: string } type Shape = | Circle of int | Rectangle of int * int
  • 19.
    type UserName ={ firstName: string; lastName: string } let a = { firstName = "a"; lastName = "b" } let b = { firstName = "a"; lastName = "b" } if a = b then "equals" // true
  • 20.
    type Money ={ amount: decimal currency: Currency } let a = { amount = 10; currency = USD } let b = { amount = 10; currency = USD } if a = b then "equals" // true
  • 21.
    type Shape = |Rectangle = 0 | Circle = 1 | Prism = 2
  • 22.
    type Shape = |Rectangle of width:float * length:float | Circle of radius:float | Prism of width:float * float * height:float let rectangle = Rectangle(width = 6.2, length = 5.5)
  • 24.
    anyone can setthis to ‘true’ Rule 1: if the email is changed, the verified flag must be reset to ‘false’. Rule 2: the verified flag can only be set by a special verification service. Rule 3: we have 5 services which works only for verified email and 5 services which works for invalid email. class EmailContact { public string EmailAddress { get; set; } public bool IsEmailVerified { get; set; } }
  • 25.
    Rule 3: wehave - 5 services which works only for verified email and - 5 services which works for invalid email. if (emailContract.IsEmailVerified) void SendEmailToApprove(EmailContact emailContract) { if (emailContract.IsEmailVerified) } void SendEmailToReject(EmailContact emailContract) { if (emailContract.IsEmailVerified) } void SendEmailToConfirm(EmailContact emailContract) { if (emailContract.IsEmailVerified) } void SendEmailToLinkedin(EmailContact emailContract) { if (emailContract.IsEmailVerified) }
  • 26.
    type ValidEmail ={ email: string } type InvalidEmail = { email: string } type Email = | Valid of ValidEmail | Invalid of InvalidEmail let sendEmailToLinkedin (email: ValidEmail) = ... You need only one dispatch in one place
  • 28.
  • 29.
    public class NaiveShoppingCart<TItem> { privateList<TItem> items; private decimal paidAmount; public NaiveShoppingCart() { this.items = new List<TItem>(); this.paidAmount = 0; } /// Is cart paid for? public bool IsPaidFor => this.paidAmount > 0; public IEnumerable<TItem> Items => this.items; public void AddItem(TItem item) { if (!this.IsPaidFor) this.items.Add(item); }
  • 30.
    if (!this.IsPaidFor) {do something }
  • 31.
    public class NaiveShoppingCart<TItem> { privateList<TItem> items; public bool IsPaidFor => this.paidAmount > 0; public bool IsPaidFor => this.paidAmount > 0; public bool IsConfirmedByUser => _isConfirmedByUser; public bool IsApproved => IsPaidFor && _isValid; public bool IsCanceledByUser => _isCanceled && _user != null; public bool IsCanceledAuto => _isCanceled || _user == null && _system != null; public bool IsCanceledAdmin => _isCanceled || _user == null && _system == null && _admin != null; public Status GetStatus() { if (_isCanceled && items.Count > 0) return Status.Invalid; else if (items.Count > 0) return Status.Active; return Status.Empty; } public void Approve() { if (_isCanceled) throw new Exception(); if (items.Count > 0) } what about recently added IsCanceledByAdmin?
  • 33.
  • 34.
    type CartItem =string // placeholder for a more complicated type type EmptyState = NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal} type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState
  • 35.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal } type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState // ============================= // operations on empty state // ============================= let addToEmptyState (item: CartItem) : Cart.Active = Cart.Active { unpaidItems = [item] } // a new Active Cart
  • 36.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal } type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState // ============================= // operation on empty state // ============================= let addToEmptyState item = { unpaidItems = [item] } // returns a new Active Cart
  • 37.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal } type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState // ============================= // operation on active state // ============================= let addToActiveState (state: ActiveState, itemToAdd: CartItem) = let newList = itemToAdd :: state.unpaidItems Cart.Active { state with unpaidItems = newList }
  • 38.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal } type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState // ============================= // operation on active state // ============================= let addToActiveState state itemToAdd = let newList = itemToAdd :: state.unpaidItems { state with unpaidItems = newList }
  • 39.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal } type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState let removeFromActiveState state itemToRemove = let newList = state.unpaidItems |> List.filter (fun i -> i <> itemToRemove) match newList with | [] -> Cart.Empty NoItems | _ -> Cart.Active { state with unpaidItems = newList }
  • 40.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal } type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState let payForActiveState state amount = Cart.PaidFor { paidItems = state.unpaidItems payment = amount }
  • 41.
    type EmptyState =NoItems type ActiveState = { unpaidItems: CartItem list; } type PaidForState = { paidItems: CartItem list; payment: decimal} type Cart = | Empty of EmptyState | Active of ActiveState | PaidFor of PaidForState let item = “test_product” let activeState = addToEmptyState(item) let paidState = payForActiveState(activeState, 10) // compile error, your state is not active anymore let activeState = addToActiveState(paidState, item)
  • 42.
  • 43.
    let failingFunc num= let x = raise (new System.Exception("fail!")) try let y = 42 + 5 + num x + y with e -> 43
  • 44.
    /// Represents theresult of a computation type Result<'ok, 'msg> = | Ok of 'ok * 'msg list | Fail of 'msg list
  • 45.
    type Request ={ name: string; email: string } let validateInput input = if input.name = "" then Fail("Name must not be blank") elif input.email = "" then Fail("Email must not be blank") else Ok(input)
  • 46.
    type Request ={ name: string; email: string } let validateInput input = if input.name = "" then fail "Name must not be blank" elif input.email = "" then fail "Email must not be blank" else ok input
  • 47.
    let validate1 input= if input.name = "" then fail "Name must not be blank“ else ok input let validate2 input = if input.name.Length > 50 then fail "Name must not be longer than 50 chars" else ok input let validate3 input = if input.email = "" then fail "Email must not be blank" else ok input let validRequest = validate1 >>= validate2 >>= validate3 >>= validate4
  • 49.
    In functional programmingwe strive to write side-effect free applications. In other words, all the functions of the application should be pure. However, completely side-effect free applications are mostly useless, so the next best thing is to minimize the amount of side-effects, make them explicit and push them as close to the boundaries of the application as possible.
  • 50.
    Let’s see anexample in invoicing domain. When changing a due date of an invoice we want to check that the new due date is in the future. We could implement it like this: let changeDueDate (newDueDate:DateTime, invoice) = if newDueDate > System.DateTime.Today then ok { invoice with dueDate = newDueDate } else fail "Due date must be in future."
  • 51.
    let changeDueDate (newDueDate:DateTime, currentDate:DateTime,invoice) = if newDueDate > currentDate then ok { invoice with dueDate = newDueDate } else fail "Due date must be in future."
  • 52.
    type PastDate =PastDate of DateTime type CurrentDate = CurrentDate of DateTime type FutureDate = FutureDate of DateTime type Date = | Past of PastDate | Current of CurrentDate | Future of FutureDate let changeDueDate (newDueDate:FutureDate, invoice) = { invoice with DueDate = Date newDueDate }
  • 53.
    Problem: Language donot integrate information - We need to bring information into the language…