В 2017 году мы начали активно использовать F# для построения high-load push-based queryable API, а также обработки больших потоков данных (stateful stream processing). На тот момент времени никто в наших командах не имел предыдущего опыта по внедрению и применению F# но мы решили попробовать. На этом докладе я расскажу о нашем опыте внедрения F#, его проблемах и недостатках, о том как мы его научились готовить, где имеет смысл его применять и как подружить C#/OOP с F#/FP в одном проекте.
Данный доклад нацелен на аудиторию не имеющую предыдущего опыта с FP/F#.
Agenda:
- Why did we choose F# over C#?
- A high-level overview of the architecture of our push-based queryable API.
- Adopting F# for C#/OOP developers (inconveniences, C# interoperability, code style, DDD, TDD)
4. - Biggest provider in sport offering
- Supports a lot of regulated markets
- About 1000 microservices (200 distinct types)
- 5 datacenters maintained fully by SBTech
- About 500 concurrent live events at pick time
- On average we handle about 100K+ RPS
8. If we have sexy C#
class Person
{
public string FirstName; // Not null
public string? MiddleName; // May be null
public string LastName; // Not null
}
9. If we have sexy C#
switch (shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Square s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
}
10. If we have sexy C#
public (string, string) LookupName(long id)
{
// some code..
return (first, middle);
}
var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");
13. Story of a new C# project
Get("/api/bets", async request =>
{
var result = Validator.Validate(request); // unit testable
if (result.IsOk)
{
var response = await _httpClient.Call(request);
return BetsMapper.MapResponse(response); // unit testable
}
else return Errors.MapError(result); // unit testable
});
16. public class BetsProviderService : IBetsProviderService
{
readonly IOpenBetsRepository _openBetsRepository;
readonly ISettledBetsRepository _settledBetsRepository;
public BetsProviderService(IOpenBetsRepository openBetsRepository,
ISettledBetsRepository settledBetsRepository)
{
_openBetsRepository = openBetsRepository;
_settledBetsRepository = settledBetsRepository;
}
}
Story of a new C# project
23. Recursive modules
type Plane() = member x.Start() = PlaneLogic.start(x)
// types and modules can't currently be mutually
// referential at all
module private PlaneLogic =
let start (x: Plane) = ...
29. Community growth
• To begin, F# has grown to be bigger than ever, at least as far as
we can measure, through product telemetry, twitter activity,
GitHub activity, and F# Software Foundation activity.
• Active unique users of F# we can measure are in the tens of
thousands.
• Measured unique users of Visual Studio Code
with Ionide increased by over 50% this year, to become far larger
than ever.
• Measured unique users of Visual Studio who use F# increased by
over 20% since last year to be larger than ever, despite quality
issues earlier in the year that we believe have inhibited growth.
• Much of the measured growth coincides with the release of .NET
Core 2.0, which has shown significant interest in the F#
community.
34. I need an API
to build
unique UI
The Problem
Oops?!
35. The Problem
Oops?!
1. well defined contracts (Event, Market, Selection)
2. flexible way to query data and subscribe on changes
3. push updates notifications about changes
4. near real time change delivery (1 sec delay)
We need to provide:
40. Subscribers
type NodeName = string
type LastHeartBeat = DateTime
type NodeSubscribers = Map<NodeName, LastHeartBeat>
type FeedStatus =
| ReadyForDeactivation
| HasSubscribers
| NoSubscribers
type ChangeFeed = {
Id: FeedId
View: FeedView
ChangeLog: ChangeLog
Subscribers: NodeSubscribers
Query: Query
Status: FeedStatus
}
Query
Feed View
2:2
1:0 Chelsea - Milan
Milan - Liverpool
Change Feed
Change Log
41. module ChangeFeed
let getSnapshot (feed: ChangeFeed, fastPreloadAmount: uint32) = ...
let getUpdates (feed: ChangeFeed, lastOffset: uint32) = ...
let getLatestUpdate (feed: ChangeFeed) = ...
let subscribe (feed: ChangeFeed, name: NodeName, currentTime: DateTime) = ...
let subscribeByOffset (feed: ChangeFeed, name: NodeName, lastOffset: uint32) = ...
let unsubscribe (feed: ChangeFeed, name: NodeName) = ...
let getInactiveNodes (feed: ChangeFeed, currentTime: DateTime, inactivePeriod: TimeSpan) = ...
let reloadView (feed: ChangeFeed, queryResults: QueryResult seq) = ...
42. Feed View
2:2
1:0 Chelsea - Milan
Milan - Liverpool
type PartialView = {
EntityType: EntityType
Entities: IEntity seq
}
type FeedView = {
OrderType: EntityType
OrderIds: string seq
OrderFilter: FilterFn option
Views: Map<EntityType, PartialView>
}
let tryCreate (queryResults: QueryResult seq, orderType: EntityType,
orderFilter: FilterFn option) =
match queryResults with
| NotMatchFor orderType -> fail <| Errors.OrderTypeIsNotMatch
| Empty -> ok <| { OrderType = orderType; OrderIds = Seq.empty
OrderFilter = orderFilter; Views = views }
| _ -> ok <| ...
43. [<Property>]
let ``empty ChangeLog should start with offset 0 and requested maxSize`` (maxSize: uint32) =
let changeLog = ChangeLog.create(maxSize)
Assert.Equal(0u, changeLog.Offset)
Assert.Equal(maxSize, changeLog.MaxSize)
[<Property(Arbitrary=[|typeof<Generators.Generator>|])>]
let ``all changes in changeLog.stream should have UpdateType.Update`` (payloads: Payload array) =
let mutable changeLog = ChangeLog.create(5u)
for p in payloads do
changeLog <- ChangeLog.append(changeLog, p)
let result = changeLog.Stream.All(fun change -> change.Type = UpdateType.Update)
Assert.True(result)
TDD without Mocks
44. We have our own frontend
but
we need a Data
The Next Challenge
Oops?!
49. Applied at the heart of the system
Zero Null Reference exceptions
Domain errors instead of exceptions
DDD with types (strong determinism)
Dependency Rejection
TDD without mocks (property based testing)
51. - Technology agnostic
(no dependency on HTTP, WebSockets, SSE)
- Very simple API
(to test Pull and Push scenarios)
- CI/CD integration
52. - Technology agnostic
(no dependency on HTTP, WebSockets, SSE)
- Very simple API
(to test Pull and Push scenarios)
- CI/CD integration
53. - Technology agnostic
(no dependency on HTTP, WebSockets, SSE)
- Very simple API
(to test Pull and Push scenarios)
- CI/CD integration
54. - Technology agnostic
(no dependency on HTTP, WebSockets, SSE)
- Very simple API
(to test Pull and Push scenarios)
- CI/CD integration
55. type Step =
| Request of RequestStep // to model Request-response pattern
| Listener of ListenerStep // to model Pub/Sub pattern
| Pause of TimeSpan // to model pause in your test flow
type TestFlow = {
FlowName: string
Steps: Step[]
ConcurrentCopies: int
}
type Scenario = {
ScenarioName: string
TestInit: Step option
TestFlows: TestFlow[]
Duration: TimeSpan
}
56. let execStep (step: Step, req: Request, timer: Stopwatch) = task {
timer.Restart()
match step with
| Request r -> let! resp = r.Execute(req)
timer.Stop()
let latency = Convert.ToInt64(timer.Elapsed.TotalMilliseconds)
return (resp, latency)
| Listener l -> let listener = l.Listeners.Get(req.CorrelationId)
let! resp = listener.GetResponse()
timer.Stop()
let latency = Convert.ToInt64(timer.Elapsed.TotalMilliseconds)
return (resp, latency)
| Pause time -> do! Task.Delay(time)
return (Response.Ok(req), 0L)
}
57. type TestFlowRunner(flow: TestFlow) =
let createActors (flow: TestFlow) =
flow.CorrelationIds
|> Set.toArray
|> Array.map(fun id -> TestFlowActor(id, flow))
let actors = createActors(flow)
member x.Run() = actors |> Array.iter(fun x -> x.Run())
member x.Stop() = actors |> Array.iter(fun x -> x.Stop())
member x.GetResult() = actors
|> Array.collect(fun actor -> actor.GetResults())
|> TestFlowStats.create(flow)
58. // C# example
var flow = new TestFlow(flowName: "READ Users",
steps: new[] {step1, step2, step3},
concurrentCopies: 10);
new ScenarioBuilder(scenarioName: "Test MongoDb")
.AddTestFlow(flow)
.Build(duration: TimeSpan.FromSeconds(10))
.RunInConsole();
69. F# vNext
Structural Type System
type User() =
member this.GetName() = "test user"
type Car() =
member this.GetName() = "test car"
let inline printName (x: ^T when ^T : (member GetName: unit -> string)) =
x.GetName()
|> Console.WriteLine