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

Post on 05-Apr-2017

173 Views

Category:

Technology

7 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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…

top related