(video and more at http://fsharpforfunandprofit.com/fppatterns)
In object-oriented development, we are all familiar with design patterns such as the Strategy pattern and Decorator pattern, and design principles such as SOLID. The functional programming community has design patterns and principles as well. This talk will provide an overview of some of these patterns (such as currying, monads), and present some demonstrations of FP design in practice. We'll also look at some of the ways you can use these patterns as part of a domain driven design process, with some simple real world examples in F#. No jargon, no maths, and no prior F# experience necessary.
36. Set of
valid inputs
Set of
valid outputs
Function
This is type
"string"
"abc"
"but"
"cobol"
"double"
"end"
"float"
37. Set of
valid inputs
Set of
valid outputs
Function
This is type
"Person"
Donna Roy
Javier Mendoza
Nathan Logan
Shawna Ingram
Abel Ortiz
Lena Robbins
GordonWood
42. New types are built from smaller types by:
Composing with “AND”
Composing with “OR”
43. Example: tuples, structs, records
FruitSalad = One each of and and
“AND” types (Record types)
type FruitSalad = {
Apple: AppleVariety
Banana: BananaVariety
Cherry: CherryVariety
}
44. Snack = or or
“OR” types (Choice types)
type Snack =
| Apple of AppleVariety
| Banana of BananaVariety
| Cherry of CherryVariety
46. Example of some requirements:
We accept three forms of payment:
Cash, Check, or Card.
For Cash we don't need any extra information
For Checks we need a check number
For Cards we need a card type and card number
47. interface IPaymentMethod
{..}
class Cash() : IPaymentMethod
{..}
class Check(int checkNo): IPaymentMethod
{..}
class Card(string cardType, string cardNo) : IPaymentMethod
{..}
In OOP you would probably implement it as an
interface and a set of subclasses, like this:
48. type CheckNumber = int
type CardNumber = string
In FP you would probably implement by composing
types, like this:
49. type CheckNumber = ...
type CardNumber = …
type CardType = Visa | Mastercard
type CreditCardInfo = {
CardType : CardType
CardNumber : CardNumber
}
50. type CheckNumber = ...
type CardNumber = ...
type CardType = ...
type CreditCardInfo = ...
type PaymentMethod =
| Cash
| Check of CheckNumber
| Card of CreditCardInfo
51. type CheckNumber = ...
type CardNumber = ...
type CardType = ...
type CreditCardInfo = ...
type PaymentMethod =
| Cash
| Check of CheckNumber
| Card of CreditCardInfo
type PaymentAmount = decimal
type Currency = EUR | USD
52. type CheckNumber = ...
type CardNumber = ...
type CardType = ...
type CreditCardInfo = ...
type PaymentMethod =
| Cash
| Check of CheckNumber
| Card of CreditCardInfo
type PaymentAmount = decimal
type Currency = EUR | USD
type Payment = {
Amount : PaymentAmount
Currency: Currency
Method: PaymentMethod }
53. type CheckNumber = int
type CardNumber = string
type CardType = Visa | Mastercard
type CreditCardInfo = CardType * CardNumber
type PaymentMethod =
| Cash
| Check of CheckNumber
| Card of CreditCardInfo
type PaymentAmount = decimal
type Currency = EUR | USD
type Payment = {
Amount : PaymentAmount
Currency: Currency
Method: PaymentMethod }
54. Design principle:
Use static types for domain
modelling and documentation
Static types only!
Sorry Clojure and JS
developers
55. A big topic and not enough time
More on DDD and designing with types at
fsharpforfunandprofit.com/ddd
61. let printList anAction aList =
for i in aList do
anAction i
So parameterize the action as well:
We've decoupled the
behavior from the data.
Any list, any action!
63. public static int Product(int n)
{
int product = 1;
for (int i = 1; i <= n; i++)
{
product *= i;
}
return product;
}
public static int Sum(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
}
return sum;
}
64. public static int Product(int n)
{
int product = 1;
for (int i = 1; i <= n; i++)
{
product *= i;
}
return product;
}
public static int Sum(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
}
return sum;
}
65. public static int Product(int n)
{
int product = 1;
for (int i = 1; i <= n; i++)
{
product *= i;
}
return product;
}
public static int Sum(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
}
return sum;
}
67. public static int Aggregate(
int initialValue,
Func<int,int,int> action,
int n)
{
int totalSoFar = initialValue;
for (int i = 1; i <= n; i++)
{
totalSoFar = action(totalSoFar,i);
}
return totalSoFar;
}
69. interface IBunchOfMethods
{
int DoSomething(int x);
string DoSomethingElse(int x);
void DoAThirdThing(string x);
}
Let's take the
Single Responsibility Principle and the
Interface Segregation Principle
to the extreme...
Every interface should have
only one method!
71. type DoSomething: int -> int
*Any* function with that type is compatible with it
let add2 x = x + 2 // int -> int
let times3 x = x * 3 // int -> int
No interface declaration needed!
73. let isEven x = ... // int -> bool
isEvenint bool
Log the input Log the output
74. let isEven x = ... // int -> bool
isEvenint bool
isEvenint boollogint int logbool bool
Compose!
75. let isEven x = ... // int -> bool
isEvenint bool
logint log boolisEven
76. let isEven x = ... // int -> bool
isEvenint bool
int log bool
let isEvenWithLogging = // int -> bool
Substitutable for original isEven
isEvenWithLogging
81. let add x y = x + y
let add = (fun x y -> x + y)
let add x = (fun y -> x + y)
int-> int->int
int-> int->int
int-> (int->int)
Normal function (Two parameters)
82. let add x y = x + y
let add = (fun x y -> x + y)
let add x = (fun y -> x + y)
int-> int->int
int-> int->int
int-> (int->int)
92. let example input =
let x = doSomething input
if x <> null then
let y = doSomethingElse x
if y <> null then
let z = doAThirdThing y
if z <> null then
let result = z
result
else
null
else
null
else
null
I know you could do early
returns, but bear with me...
93. let taskExample input =
let taskX = startTask input
taskX.WhenFinished (fun x ->
let taskY = startAnotherTask x
taskY.WhenFinished (fun y ->
let taskZ = startThirdTask y
taskZ.WhenFinished (fun z ->
z // final result
)
)
)
94. let example input =
let x = doSomething input
if x <> null then
let y = doSomethingElse x
if y <> null then
let z = doAThirdThing y
if z <> null then
let result = z
result
else
null
else
null
else
null
Nulls are a code smell:
replace with Option!
Let's fix this!
95. let example input =
let x = doSomething input
if x.IsSome then
let y = doSomethingElse (x.Value)
if y.IsSome then
let z = doAThirdThing (y.Value)
if z.IsSome then
let result = z.Value
Some result
else
None
else
None
else
None
Much more elegant, yes?
No! This is fugly!
But there is a pattern we can exploit...
96. let example input =
let x = doSomething input
if x.IsSome then
let y = doSomethingElse (x.Value)
if y.IsSome then
let z = doAThirdThing (y.Value)
if z.IsSome then
// do something with z.Value
// in this block
else
None
else
None
else
None
97. let example input =
let x = doSomething input
if x.IsSome then
let y = doSomethingElse (x.Value)
if y.IsSome then
// do something with y.Value
// in this block
else
None
else
None
98. let example input =
let x = doSomething input
if x.IsSome then
// do something with x.Value
// in this block
else
None
Can you see the pattern?
100. let ifSomeDo f opt =
if opt.IsSome then
f opt.Value
else
None
101. let example input =
doSomething input
|> ifSomeDo doSomethingElse
|> ifSomeDo doAThirdThing
|> ifSomeDo ...
let ifSomeDo f opt =
if opt.IsSome then
f opt.Value
else
None
119. let example input =
let x = doSomething input
if x.IsSome then
let y = doSomethingElse (x.Value)
if y.IsSome then
let z = doAThirdThing (y.Value)
if z.IsSome then
let result = z.Value
Some result
else
None
else
None
else
None
Before
120. let bind f opt =
match opt with
| Some v -> f v
| None -> None
After
121. let example input =
doSomething input
|> bind doSomethingElse
|> bind doAThirdThing
|> bind ...
let bind f opt =
match opt with
| Some v -> f v
| None -> None
No pyramids!
Code is linear and clear.
This pattern is called “monadic bind”
After
124. let taskExample input =
let taskX = startTask input
taskX.WhenFinished (fun x ->
let taskY = startAnotherTask x
taskY.WhenFinished (fun y ->
let taskZ = startThirdTask y
taskZ.WhenFinished (fun z ->
z // final result
)
)
)
Before
125. let taskBind f task =
task.WhenFinished (fun taskResult ->
f taskResult)
let taskExample input =
startTask input
|> taskBind startAnotherTask
|> taskBind startThirdTask
|> taskBind ...
This pattern is also a “monadic bind”
After
128. string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated) {
return "Request is not valid"
}
canonicalizeEmail(request);
db.updateDbFromRequest(request);
smtpServer.sendEmail(request.Email)
return "OK";
}
129. string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated) {
return "Request is not valid"
}
canonicalizeEmail(request);
var result = db.updateDbFromRequest(request);
if (!result) {
return "Customer record not found"
}
smtpServer.sendEmail(request.Email)
return "OK";
}
130. string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated) {
return "Request is not valid"
}
canonicalizeEmail(request);
try {
var result = db.updateDbFromRequest(request);
if (!result) {
return "Customer record not found"
}
} catch {
return "DB error: Customer record not updated"
}
smtpServer.sendEmail(request.Email)
return "OK";
}
131. string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated) {
return "Request is not valid"
}
canonicalizeEmail(request);
try {
var result = db.updateDbFromRequest(request);
if (!result) {
return "Customer record not found"
}
} catch {
return "DB error: Customer record not updated"
}
if (!smtpServer.sendEmail(request.Email)) {
log.Error "Customer email not sent"
}
return "OK";
}
132. string UpdateCustomerWithErrorHandling()
{
var request = receiveRequest();
var isValidated = validateRequest(request);
if (!isValidated) {
return "Request is not valid"
}
canonicalizeEmail(request);
try {
var result = db.updateDbFromRequest(request);
if (!result) {
return "Customer record not found"
}
} catch {
return "DB error: Customer record not updated"
}
if (!smtpServer.sendEmail(request.Email)) {
log.Error "Customer email not sent"
}
return "OK";
}
135. Request SuccessValidate
Failure
let validateInput input =
if input.name = "" then
Error "Name must not be blank"
else if input.email = "" then
Error "Email must not be blank"
else
Ok input // happy path
140. FP terminology
• A monad is
– A data type
– With an associated bind/flatMap function (and
some other stuff)
– With a sensible implementation (monad laws).
• A monadic function is
– A switch/points function
– bind/flatMap is used to compose them
141. Tip:
Monads are a general purpose way
of composing functions with
complex outputs
171. • You start with a bunch of things, and some way of
combining them two at a time.
• Rule 1 (Closure):The result of combining two things is
always another one of the things.
• Rule 2 (Associativity):When combining more than
two things, which pairwise combination you do first
doesn't matter.
• Rule 3 (Identity element):There is a special thing
called "zero" such that when you combine any thing
with "zero" you get the original thing back. A monoid!
172. • Rule 1 (Closure):The result of combining two
things is always another one of the things.
• Benefit: converts pairwise operations into
operations that work on lists.
1 + 2 + 3 + 4
[ 1; 2; 3; 4 ] |> List.reduce (+)
We'll see
this a lot!
173. 1 * 2 * 3 * 4
[ 1; 2; 3; 4 ] |> List.reduce (*)
• Rule 1 (Closure):The result of combining two
things is always another one of the things.
• Benefit: converts pairwise operations into
operations that work on lists.
174. "a" + "b" + "c" + "d"
[ "a"; "b"; "c"; "d" ] |> List.reduce (+)
• Rule 1 (Closure):The result of combining two
things is always another one of the things.
• Benefit: converts pairwise operations into
operations that work on lists.
175. • Rule 2 (Associativity):When combining more
than two things, which pairwise combination
you do first doesn't matter.
• Benefit: Divide and conquer, parallelization, and
incremental accumulation.
178. • Rule 2 (Associativity):When combining more
than two things, which pairwise combination
you do first doesn't matter.
• Benefit: Divide and conquer, parallelization, and
incremental accumulation.
182. • How can I use reduce on an empty list?
• In a divide and conquer algorithm, what should I
do if one of the "divide" steps has nothing in it?
• When using an incremental algorithm, what
value should I start with when I have no data?
183. • Rule 3 (Identity element):There is a special
thing called "zero" such that when you combine
any thing with "zero" you get the original thing
back.
• Benefit: Initial value for empty or missing data
185. type OrderLine = {Qty:int; Total:float}
let orderLines = [
{Qty=2; Total=19.98}
{Qty=1; Total= 1.99}
{Qty=3; Total= 3.99} ]
How to add them up?
186. type OrderLine = {Qty:int; Total:float}
let addPair line1 line2 =
let newQty = line1.Qty + line2.Qty
let newTotal = line1.Total + line2.Total
{Qty=newQty; Total=newTotal}
orderLines |> List.reduce addPair
// {Qty=6; Total= 25.96}
Any combination
of monoids is
also a monoid
Write a pairwise combiner
Profit!
191. Monoids in the real world
Metrics guideline:
Use counters rather than rates
Alternative metrics guideline:
Make sure your metrics are monoids
• incremental updates
• can handle missing data
192. Is function composition a monoid?
>>
Function 1
apple -> banana
Function 2
banana -> cherry
New Function
apple -> cherry
Not the same
thing.
Not a monoid
193. Is function composition a monoid?
>>
Function 1
apple -> apple
Same thing
Function 2
apple -> apple
Function 3
apple -> apple
A monoid!
197. Monad laws
• Closure, Associativity, Identity
– The monad laws are just the monoid definitions in
diguise
• What happens if you break the monad laws?
– You go to jail
– You lose monoid benefits such as aggregation
199. "A monad is just a monoid in
the category of endofunctors"
200. Review
• Partial Application
– For composing functions with multiple parameters
• Bind/Monads
– For composing functions with effects
• Map/Functors
– For composing functions without leaving the other
world
• Monoids
– A general pattern for composing things
201. Review
• Partial Application
– For composing functions with multiple parameters
• Bind/Monads
– For composing functions with effects
• Map/Functor
– For composing functions without leaving the other
world
• Monoids
– A pattern for composing things
202. Slides and video here
fsharpforfunandprofit.com/fppatterns
Functional Design Patterns
Thank you!