learn you to tame complex apis with f#-powered dsls
DESCRIPTION
In this talk, I discussed the different forms of complexities that can arise when integrating with APIs, and how DSLs can be used to tackle these complexities. I demonstrated that F# can be a very effective tool for creating both internal and external DSLs using both FParsec and active patterns.TRANSCRIPT
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
@theburningmonk
select GameTitle, UserId, TopScore from GameScores where GameTitle = “Starship X” and TopScore >= 1000 order desc limit 3 with (NoConsistentRead, Index(GameTitleIndex, true))
@theburningmonk
GOAL
SELECT UserId, TopScore FROM GameScore WHERE GameTitle CONTAINS “Zelda” ORDER DESCLIMIT 3 WITH (NoConsistentRead)
@theburningmonk
SELECT * FROM GameScorepSelect
let pSelect = skipStringCI "select"matches the string “select” (Case
Insensitive) and ignores it
@theburningmonk
SELECT * FROM GameScore
Parser for a string that represents the table name
pTableName
@theburningmonk
SELECT * FROM GameScorepTableName
let isTableName = isLetter <||> isDigit let pTableName = many1Satisfy isTableName
@theburningmonk
SELECT * FROM GameScorepTableName
let isTableName = isLetter <||> isDigit let pTableName = many1Satisfy isTableName
@theburningmonk
SELECT * FROM GameScorepTableName
let isTableName = isLetter <||> isDigit let pTableName = many1Satisfy isTableName
@theburningmonk
SELECT * FROM GameScorepTableName
let isTableName = isLetter <||> isDigit let pTableName = many1Satisfy isTableName
@theburningmonk
SELECT * FROM GameScorepTableName
let isTableName = isLetter <||> isDigit let pTableName = many1Satisfy isTableName
parses a sequence of one or more chars that satisfies the
predicate function
@theburningmonk
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
let pAsterisk = stringCIReturn "*" Asterisk
@theburningmonk
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
let pAsterisk = stringCIReturn "*" Asteriskmatches the specified string and return the given value
@theburningmonk
SELECT * FROM GameScore
pAsterisk
*
pAttributeName
UserId, GameTitle, TopScore, …
let isAttributeName = isLetter <||> isDigit let pAttributeName = many1Satisfy isAttributeName
@theburningmonk
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pCommapAsterisk
*
let pComma = skipStringCI ","
@theburningmonk
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pCommapAsterisk
*
let pAttributeNames = sepBy1 pAttributeName pComma
@theburningmonk
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pCommapAsterisk
*
let pAttributeNames = sepBy1 pAttributeName pComma
parses one or more occurrences of pAttributeName separated by pComma
@theburningmonk
SELECT * FROM GameScore
UserId, GameTitle, TopScore, …
pAttributeName pComma
sepBy1
pAttributeNames
pAsterisk
*
@theburningmonk
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
let pAttribute = pAsterisk <|> pAttributeNames
@theburningmonk
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
let pAttribute = pAsterisk <|> pAttributeNames
@theburningmonk
SELECT * FROM GameScore
pAsterisk
*
pAttributeNames
UserId, GameTitle, TopScore, …
choice
pAttribute
@theburningmonk
SELECT * FROM GameScorepAttribute pTableNamepFrompSelect
let pQuery = tuple4 pSelect pAttribute pFrom pTableName |>> (fun (_, attributes, _, table) -> { Attributes = attributes Table = table })
@theburningmonk
SELECT * FROM GameScorepAttribute pTableNamepFrompSelect
let pQuery = tuple4 pSelect pAttribute pFrom pTableName |>> (fun (_, attributes, _, table) -> { Attributes = attributes Table = table })
@theburningmonk
SELECT * FROM GameScorepAttribute pTableNamepFrompSelect
let pQuery = tuple4 pSelect pAttribute pFrom pTableName |>> (fun (_, attributes, _, table) -> { Attributes = attributes Table = table })
@theburningmonk
SELECT * FROM GameScorepAttribute pTableNamepFrompSelect
let pQuery = tuple4 pSelect pAttribute pFrom pTableName |>> (fun (_, attributes, _, table) -> { Attributes = attributes Table = table })
Query
@theburningmonk
select GameTitle, UserId, TopScore from GameScores where GameTitle = “Starship X” and TopScore >= 1000 order desc limit 3 with (NoConsistentRead, Index(GameTitleIndex, true))
– Kris Jordan
“Good simplicity is less with leverage, not less with less. Good simplicity is complexity
disguised, not complexity denied.”
Amazon.SimpleWorkflow.Extensions
github.com/fsprojects/Amazon.SimpleWorkflow.Extensions
Amazon.CloudWatch.Selector
github.com/fsprojects/Amazon.CloudWatch.Selector
@theburningmonk
cloudWatch.Select( unitIs “milliseconds” + average (>) 1000.0 @ last 12 hours |> intervalOf 5 minutes)
@theburningmonk
cloudWatch.Select(“ unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes”)
@theburningmonk
cloudWatch.Select( namespaceLike “elasticache” + nameLike “cpu” + max (>) 80.0 @ last 24 hours |> intervalOf 15 minutes)
@theburningmonk
cloudWatch.Select( namespaceLike “elasticache” + nameLike “cpu” + max (>) 80.0 @ last 24 hours |> intervalOf 15 minutes)
Regex
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
Filters
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
TimeFrame
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
Period
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
type MetricTerm = Namespace | Name
type Filter = | MetricFilter of MetricTerm * (string -> bool)
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
type MetricTerm = Namespace | Name
type Unit = | Unit
type Filter = | MetricFilter of MetricTerm * (string -> bool) | UnitFilter of Unit * (string -> bool)
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
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)
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
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
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
type TimeFrame = | Last of TimeSpan | Since of DateTime | Between of DateTime * DateTime
@theburningmonk
namespaceIs ‘JustEat’ and nameLike ‘cpu’ and unitIs ‘milliseconds’ and average > 1000.0 duringLast 12 hours at intervalOf 5 minutes
@theburningmonk
let (|Float|) input = match Double.TryParse input with | true, n -> n | _ -> failwithf “not a float [%s]” input
@theburningmonk
let (|Float|) input = match Double.TryParse input with | true, n -> n | _ -> failwithf “not a float [%s]” input
@theburningmonk
let (|Float|) input = match Double.TryParse input with | true, n -> n | _ -> failwithf “not a float [%s]” input
Float : string -> float
@theburningmonk
match someString with | Float 42.0 -> “ftw” | Float 11.0 -> “palprime” | Float x -> sprintf “just %f” x
@theburningmonk
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
@theburningmonk
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
@theburningmonk
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
@theburningmonk
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
@theburningmonk
match “42” with | Float 42.0 -> “ftw” | Float 11.0 -> “palprime” | Float x -> sprintf “just %f” x
@theburningmonk
match “boo” with | Float 42.0 -> “ftw” | Float 11.0 -> “palprime” | Float x -> sprintf “just %f” x
Error!!!
@theburningmonk
let (|Float|_|) input = match Double.TryParse input with | true, n -> Some n | _ -> None
@theburningmonk
let (|Float|_|) input = match Double.TryParse input with | true, n -> Some n | _ -> None
Float : string -> float option
@theburningmonk
match “boo” with | Float 42.0 -> “ftw” | Float 11.0 -> “palprime” | Float x -> sprintf “just %f” x | _ -> “not a float”
@theburningmonk
match “boo” with | Float 42.0 -> “ftw” | Float 11.0 -> “palprime” | Float x -> sprintf “just %f” x | _ -> “not a float”
@theburningmonk
let (|Prime|NotPrime|NaN|) input = match Double.TryParse input with | true, n when isPrime n -> Prime n | true, n -> NotPrime n | _ -> NaN
@theburningmonk
let (|Prime|NotPrime|NaN|) input = match Double.TryParse input with | true, n when isPrime n -> Prime n | true, n -> NotPrime n | _ -> NaN
@theburningmonk
let (|Prime|NotPrime|NaN|) input = match Double.TryParse input with | true, n when isPrime n -> Prime n | true, n -> NotPrime n | _ -> NaN
@theburningmonk
let (|Prime|NotPrime|NaN|) input = match Double.TryParse input with | true, n when isPrime n -> Prime n | true, n -> NotPrime n | _ -> NaN
@theburningmonk
let (|Prime|NotPrime|NaN|) input = match Double.TryParse input with | true, n when isPrime n -> Prime n | true, n -> NotPrime n | _ -> NaN
@theburningmonk
match someString with | Prime n & Float 11.0 -> … | Prime n -> … | Float 42.0 | Float 8.0 -> … | NotPrime n -> … | NaN -> …
@theburningmonk
match someString with | Prime n & Float 11.0 -> … | Prime n -> … | Float 42.0 | Float 8.0 -> … | NotPrime n -> … | NaN -> …
@theburningmonk
match someString with | Prime (IsPalindrome n) -> … | Prime (IsEven n) -> … | _ -> …
@theburningmonk
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?!?!?”
@theburningmonk
let (|NamespaceIs|_|) = function | StringCI "NamespaceIs"::QuotedString ns::tl -> (eqFilter MetricFilter Namespace ns, tl) |> Some | _ -> None
let (|NamespaceLike|_|) = function | StringCI "NamespaceLike"::QuotedString pattern::tl -> (regexFilter MetricFilter Namespace pattern, tl) |> Some | _ -> None
@theburningmonk
let parse (input : string) = input |> tokenize |> parseFilter |> parseTimeFrame |> parsePeriod