Post on 05-Apr-2017
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 composabilitycorrectness
In FP you build your ideal, private worldwhere
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: stringlastName: 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: decimalcurrency: 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); }
/// remove item only if not paid for
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.unpaidItemsCart.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 anymorelet activeState = addToActiveState(paidState, item)
errors
let failingFunc num =let x = raise (new System.Exception("fail!"))try
let y = 42 + 5 + numx + y
withe -> 43
/// Represents the result of a computationtype 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.Todaythen ok { invoice with dueDate = newDueDate }
else fail "Due date must be in future."
let changeDueDate (newDueDate:DateTime,currentDate:DateTime, invoice) =
if newDueDate > currentDatethen ok { invoice with dueDate = newDueDate }
else fail "Due date must be in future."
type PastDate = PastDate of DateTimetype CurrentDate = CurrentDate of DateTimetype 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…