tame
|> (cloud <| complexity)
|> with
|> (fsharp >> powered >> DSLs)
hi,I’mYanCui
AWS user since 2009
image by nerovivo license : https://creativecommons.org/licenses/by-sa/2.0/
Under-Abstraction
Oversimplification
Impedance Mismatch
Amazon
DynamoDB
Amazon
SimpleWorkflow
Amazon
CloudWatch
CASE STUDY
F# DSLs. Awesome!
Amazon
DynamoDB
Amazon
SimpleWorkflow
Amazon
CloudWatch
CASE STUDY
managed
key-value store
redundancy
9-9s guarantee
great performance
name your
throughput
can be changed on-the-fly
infinitely scalable
(but you still have to pay for it)
Hash Key
Range Key
Query:
given a hash key
filter on
range key, or
local secondary index
Hash Key Range Key
Local Secondary Index
Global Secondary Index
Scan:
FULL TABLE search
(performance + cost concern)
Hash Key Range Key
Local Secondary Index
Global Secondary Index
Hash Key Range Key
Local Secondary Index
Who are the TOP 3 players in “Starship
X” with a score of at least 1000?
Global Secondary Index
select GameTitle, UserId, TopScore
from GameScores
where GameTitle = “Starship X”
and TopScore >= 1000
order desc
limit 3
with (NoConsistentRead, Index(GameTitleIndex, true))
DynamoDB.SQL
github.com/fsprojects/DynamoDb.SQL
GOAL
Disguise
complexity
GOAL
Prevent
abstraction leak
GOAL
SELECT UserId, TopScore
FROM GameScore
WHERE GameTitle CONTAINS “Zelda”
ORDER DESC
LIMIT 3
WITH (NoConsistentRead)
Query
AST
Execution
F# & FParsec*
*www.quanttec.com/fparsec
External DSL
via
SELECT * FROM GameScore
Abstract Syntax Tree (AST)
FParsec
SELECT * FROM GameScore
keyword keyword
* | attribute, attribute, …
table name
SELECT * FROM GameScore
type Attributes =
| Asterisk
| Attributes of string[]
SELECT * FROM GameScore
type Query =
{
Attributes : Attributes
Table : string
}
SELECT * FROM GameScore
Parser for “SELECT” keyword
pSelect
SELECT * FROM GameScore
pSelect
let pSelect = skipStringCI "select"
SELECT * FROM GameScore
pSelect
let pSelect = skipStringCI "select"
matches the string “select” (Case
Insensitive) and ignores it
SELECT * FROM GameScore
pFrom
let pFrom = skipStringCI "from"
SELECT * FROM GameScore
Parser for a string that
represents the table name
pTableName
SELECT * FROM GameScore
pTableName
let isTableName = isLetter <||> isDigit
let pTableName =
many1Satisfy isTableName
SELECT * FROM GameScore
pTableName
let isTableName = isLetter <||> isDigit
let pTableName =
many1Satisfy isTableName
SELECT * FROM GameScore
pTableName
let isTableName = isLetter <||> isDigit
let pTableName =
many1Satisfy isTableName
SELECT * FROM GameScore
pTableName
let isTableName = isLetter <||> isDigit
let pTableName =
many1Satisfy isTableName
SELECT * FROM GameScore
pTableName
let isTableName = isLetter <||> isDigit
let pTableName =
many1Satisfy isTableName
parses a sequence of one or
more chars that satisfies the
predicate function
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
let pAsterisk = stringCIReturn "*" Asterisk
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
let pAsterisk = stringCIReturn "*" Asterisk
matches the specified string
and return the given value
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
let isAttributeName = isLetter <||> isDigit
let pAttributeName =
many1Satisfy isAttributeName
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pCommapAsterisk
*
let pComma = skipStringCI ","
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pCommapAsterisk
*
let pAttributeNames =
sepBy1 pAttributeName pComma
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pCommapAsterisk
*
let pAttributeNames =
sepBy1 pAttributeName pComma
parses one or more occurrences of
pAttributeName separated by pComma
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pComma
sepBy1
pAttributeNames
pAsterisk
*
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
let pAttribute =
pAsterisk <|> pAttributeNames
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
let pAttribute =
pAsterisk <|> pAttributeNames
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
choice
pAttribute
SELECT * FROM GameScore
pAttribute
SELECT * FROM GameScore
pAttribute pTableNamepFrompSelect
SELECT * FROM GameScore
pAttribute pTableNamepFrompSelect
let pQuery =
tuple4 pSelect pAttribute
pFrom pTableName
|>> (fun (_, attributes, _, table) ->
{ Attributes = attributes
Table = table })
SELECT * FROM GameScore
pAttribute pTableNamepFrompSelect
let pQuery =
tuple4 pSelect pAttribute
pFrom pTableName
|>> (fun (_, attributes, _, table) ->
{ Attributes = attributes
Table = table })
SELECT * FROM GameScore
pAttribute pTableNamepFrompSelect
let pQuery =
tuple4 pSelect pAttribute
pFrom pTableName
|>> (fun (_, attributes, _, table) ->
{ Attributes = attributes
Table = table })
SELECT * FROM GameScore
pAttribute pTableNamepFrompSelect
let pQuery =
tuple4 pSelect pAttribute
pFrom pTableName
|>> (fun (_, attributes, _, table) ->
{ Attributes = attributes
Table = table })
Query
SELECT * FROM GameScore
pAttribute pTableNamepFrompSelect
tuple4
pQuery
< 50 lines of code
Amazing
F# + FParsec
=
Recap
select GameTitle, UserId, TopScore
from GameScores
where GameTitle = “Starship X”
and TopScore >= 1000
order desc
limit 3
with (NoConsistentRead, Index(GameTitleIndex, true))
Amazon
DynamoDB
Amazon
SimpleWorkflow
Amazon
CloudWatch
CASE STUDY
Decision Worker
Decision Worker
Poll
Decision Worker
Decision
Task
Decision WorkerDecide
Activity Worker
Decision Worker
Activity Worker
Decision Worker
Poll
Activity Worker
Decision Worker
Activity
Task
Activity Worker
Decision Worker
Complete
Workers can run from
anywhere
input = “Yan”
result = “Hello Yan!”
Start
Finish
Activity
image by Ryan Hageman license : https://creativecommons.org/licenses/by-sa/2.0/
SWF-based Application
API
Heartbeats
Error
Handling
Polling
API
Activity
Worker
Decision
Worker
Heartbeats
Error
Handling
Polling
API
Activity
Worker
Decision
Worker
Heartbeats
Error
Handling
Polling
API
Boilerplate
– Kris Jordan
“Good simplicity is less with
leverage, not less with less.
Good simplicity is complexity
disguised, not complexity denied.”
http://bit.ly/1pOLeKl
Start
Finish
Activity
?
the workflow is
implied by decision
worker logic…
instead..
the workflow should
drive decision
worker logic
the workflow should
driveautomate
decision worker logic
Amazon
.SimpleWorkflow
.Extensions
github.com/fsprojects/Amazon.SimpleWorkflow.Extensions
GOAL
Remove
boilerplates
GOAL
Code that matches
the way you think
Start
Finish
Activity
Start
Finish
Activity
Workflows can be
nested
input
result
Recap
Activity
Worker
Decision
Worker
Heartbeats
Error
Handling
Polling
API
Amazon
DynamoDB
Amazon
SimpleWorkflow
Amazon
CloudWatch
CASE STUDY
wanna find
correlations?
wanna find
correlations?
you can DIY it!
;-)
“what latencies spiked
at the same time as
payment service?”
Amazon
.CloudWatch
.Selector
github.com/fsprojects/Amazon.CloudWatch.Selector
Find metrics whose 5
min average exceeded
1 second during last
12 hours
cloudWatch.Select(
unitIs “milliseconds” +
average (>) 1000.0
@ last 12 hours
|> intervalOf 5 minutes)
cloudWatch.Select(“
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes”)
“did any cache
nodes’ CPU spike
yesterday?”
cloudWatch.Select(
namespaceLike “elasticache” +
nameLike “cpu” +
max (>) 80.0
@ last 24 hours
|> intervalOf 15 minutes)
cloudWatch.Select(
namespaceLike “elasticache” +
nameLike “cpu” +
max (>) 80.0
@ last 24 hours
|> intervalOf 15 minutes)
Regex
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
Filters
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
TimeFrame
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
Period
type Query =
{
Filter : Filter
TimeFrame : TimeFrame
Period : Period option
}
Query
Internal
DSL
External
DSL
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
type MetricTerm = Namespace | Name
type Filter =
| MetricFilter of MetricTerm * (string -> bool)
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
type MetricTerm = Namespace | Name
type Unit = | Unit
type Filter =
| MetricFilter of MetricTerm * (string -> bool)
| UnitFilter of Unit * (string -> bool)
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
type MetricTerm = Namespace | Name
type Unit = | Unit
type StatsTerm =
| Average | Min | Max | Sum | SampleCount
type Filter =
| MetricFilter of MetricTerm * (string -> bool)
| UnitFilter of Unit * (string -> bool)
| StatsFilter of StatsTerm * (float -> bool)
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
type MetricTerm = Namespace | Name
type Unit = | Unit
type StatsTerm =
| Average | Min | Max | Sum | SampleCount
type Filter =
| MetricFilter of MetricTerm * (string -> bool)
| UnitFilter of Unit * (string -> bool)
| StatsFilter of StatsTerm * (float -> bool)
| CompositeFilter of Filter * Filter
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
type TimeFrame =
| Last of TimeSpan
| Since of DateTime
| Between of DateTime * DateTime
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
unitIs ‘milliseconds’ and
average > 1000.0
duringLast 12 hours
at intervalOf 5 minutes
type Period = | Period of TimeSpan
type Query =
{
Filter : Filter
TimeFrame : TimeFrame
Period : Period option
}
Active Patterns
a primer on
allow patterns to be
abstracted away into
named functions
Single-Case Patterns
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
| _ ->
failwithf “not a float [%s]” input
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
| _ ->
failwithf “not a float [%s]” input
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
| _ ->
failwithf “not a float [%s]” input
Float : string -> float
match someString with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
match someString with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
match someString with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
match someString with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
match someString with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
let (|Float|) input =
match Double.TryParse input with
| true, n -> n
match “42” with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
match “boo” with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
Error!!!
Partial Patterns
let (|Float|_|) input =
match Double.TryParse input with
| true, n -> Some n
| _ -> None
let (|Float|_|) input =
match Double.TryParse input with
| true, n -> Some n
| _ -> None
Float : string -> float option
match “boo” with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
| _ -> “not a float”
match “boo” with
| Float 42.0 -> “ftw”
| Float 11.0 -> “palprime”
| Float x -> sprintf “just %f” x
| _ -> “not a float”
Multi-Case Patterns
let (|Prime|NotPrime|NaN|) input =
match Double.TryParse input with
| true, n when isPrime n -> Prime n
| true, n -> NotPrime n
| _ -> NaN
let (|Prime|NotPrime|NaN|) input =
match Double.TryParse input with
| true, n when isPrime n -> Prime n
| true, n -> NotPrime n
| _ -> NaN
let (|Prime|NotPrime|NaN|) input =
match Double.TryParse input with
| true, n when isPrime n -> Prime n
| true, n -> NotPrime n
| _ -> NaN
let (|Prime|NotPrime|NaN|) input =
match Double.TryParse input with
| true, n when isPrime n -> Prime n
| true, n -> NotPrime n
| _ -> NaN
let (|Prime|NotPrime|NaN|) input =
match Double.TryParse input with
| true, n when isPrime n -> Prime n
| true, n -> NotPrime n
| _ -> NaN
match someString with
| Prime n -> …
| NotPrime n -> …
| NaN -> …
match someString with
| Prime n & Float 11.0 -> …
| Prime n -> …
| Float 42.0 | Float 8.0 -> …
| NotPrime n -> …
| NaN -> …
match someString with
| Prime n & Float 11.0 -> …
| Prime n -> …
| Float 42.0 | Float 8.0 -> …
| NotPrime n -> …
| NaN -> …
match someString with
| Prime (IsPalindrome n) -> …
| Prime (IsEven n) -> …
| _ -> …
Tokenise
(string -> string list)
[
“namespaceIs”; “‘JustEat’”; “and”;
“nameLike”; “‘cpu’”; “and”;
…
]
“namespaceIs” “and” …
F# List = LinkedList
“‘JustEat’”
“namespaceIs” “and” …
F# List = LinkedList
“namespaceIs” “‘JustEat’” “and”:::: :: tail
cons (::) operator to prepend
“‘JustEat’”
“namespaceIs” “and” …
F# List = LinkedList
“namespaceIs” “‘JustEat’” “and”:::: :: tail
match aList with
|
“‘JustEat’”
works as a pattern too!
let (|StringCI|_|) (str : string) (input : string) =
if str.ToLower() = input.ToLower()
then Some ()
else None
let (|Float|_|) input =
match System.Double.TryParse input with
| true, n -> Some n
| _ -> None
let (|EmptyString|_|) input =
if String.IsNullOrWhiteSpace input
then Some ()
else None
let (|StartsWith|_|) char (input : string) =
if input.[0] = char then Some () else None
let (|EndsWith|_|) char (input : string) =
if input.[input.Length-1] = char
then Some ()
else None
let (|QuotedString|_|) (input : string) =
match input with
| EmptyString -> None
| str when str.Length < 2 -> None
| StartsWith ''' & EndsWith ''' ->
Some <| input.Substring(1, input.Length - 2)
| _ -> None
let (|QuotedString|_|) (input : string) =
match input with
| EmptyString -> None
| str when str.Length < 2 -> None
| StartsWith ''' & EndsWith ''' ->
Some <| input.Substring(1, input.Length - 2)
| _ -> None
let (|NamespaceIs|_|) = function
| StringCI “NamespaceIs" :: QuotedString ns :: tl
-> Some (eqFilter MetricFilter Namespace ns, tl)
| _ -> None
let (|NamespaceIs|_|) = function
| StringCI “NamespaceIs" :: QuotedString ns :: tl
-> Some (eqFilter MetricFilter Namespace ns, tl)
| _ -> None
namespaceIs ‘JustEat’ and
nameLike ‘cpu’ and
…
let (|NamespaceIs|_|) = function
| StringCI “NamespaceIs" :: QuotedString ns :: tl
-> Some (eqFilter MetricFilter Namespace ns, tl)
| _ -> None
“namespaceIs” “‘JustEat’” :::: …
let (|NamespaceIs|_|) = function
| StringCI “NamespaceIs" :: QuotedString ns :: tl
-> Some (eqFilter MetricFilter Namespace ns, tl)
| _ -> None
type MetricTerm = Namespace | Name
type Filter =
| MetricFilter of MetricTerm * (string -> bool)
let (|NamespaceIs|_|) = function
| StringCI “NamespaceIs" :: QuotedString ns :: tl
-> Some (eqFilter MetricFilter Namespace ns, tl)
| _ -> None
“and” :: “nameLike” :: “‘cpu’” :: “and” :: …
let rec loop acc = function
| NamespaceIs (filter, tl)
| NamespaceLike (filter, tl)
| NameIs (filter, tl) | NameLike (filter, tl)
| UnitIs (filter, tl)
| Average (filter, tl) | Sum (filter, tl)
| Min (filter, tl) | Max (filter, tl)
| SampleCount (filter, tl)
-> match tl with
| And tl -> loop (filter::acc) tl
| _ -> flatten (filter::acc), tl
| _ -> failwith “No filters?!?!?”
let rec loop acc = function
| NamespaceIs (filter, tl)
| NamespaceLike (filter, tl)
| NameIs (filter, tl) | NameLike (filter, tl)
| UnitIs (filter, tl)
| Average (filter, tl) | Sum (filter, tl)
| Min (filter, tl) | Max (filter, tl)
| SampleCount (filter, tl)
-> match tl with
| And tl -> loop (filter::acc) tl
| _ -> flatten (filter::acc), tl
| _ -> failwith “No filters?!?!?”
let rec loop acc = function
| NamespaceIs (filter, tl)
| NamespaceLike (filter, tl)
| NameIs (filter, tl) | NameLike (filter, tl)
| UnitIs (filter, tl)
| Average (filter, tl) | Sum (filter, tl)
| Min (filter, tl) | Max (filter, tl)
| SampleCount (filter, tl)
-> match tl with
| And tl -> loop (filter::acc) tl
| _ -> flatten (filter::acc), tl
| _ -> failwith “No filters?!?!?”
let rec loop acc = function
| NamespaceIs (filter, tl)
| NamespaceLike (filter, tl)
| NameIs (filter, tl) | NameLike (filter, tl)
| UnitIs (filter, tl)
| Average (filter, tl) | Sum (filter, tl)
| Min (filter, tl) | Max (filter, tl)
| SampleCount (filter, tl)
-> match tl with
| And tl -> loop (filter::acc) tl
| _ -> flatten (filter::acc), tl
| _ -> failwith “No filters?!?!?”
let rec loop acc = function
| NamespaceIs (filter, tl)
| NamespaceLike (filter, tl)
| NameIs (filter, tl) | NameLike (filter, tl)
| UnitIs (filter, tl)
| Average (filter, tl) | Sum (filter, tl)
| Min (filter, tl) | Max (filter, tl)
| SampleCount (filter, tl)
-> match tl with
| And tl -> loop (filter::acc) tl
| _ -> flatten (filter::acc), tl
| _ -> failwith “No filters?!?!?”
let parse (input : string) =
input
|> tokenize
|> parseFilter
|> parseTimeFrame
|> parsePeriod
Amazing
F#
=
usable from anywhere you
can run F# code
e.g. F# REPL, executable, ..
Internal DSL
useful for building tools
e.g. CLI, …
External DSL
Recap
Amazon
DynamoDB
Amazon
SimpleWorkflow
Amazon
CloudWatch
CASE STUDY
@theburningmonk
theburningmonk.com
github.com/theburningmonk

Tame cloud complexity with F# powered DSLs (build stuff)