Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
val id : x:'T -> 'T
Full name: Microsoft.FSharp.Core.Operators.id
namespace Microsoft.FSharp.Data
namespace System
module Option
from Microsoft.FSharp.Core
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
type 'T option = Option<'T>
Full name: Microsoft.FSharp.Core.option<_>
val ignore : value:'T -> unit
Full name: Microsoft.FSharp.Core.Operators.ignore
Multiple items
module Result
from Microsoft.FSharp.Core
--------------------
type Result<'T,'TError> =
| Ok of ResultValue: 'T
| Error of ErrorValue: 'TError
Full name: Microsoft.FSharp.Core.Result<_,_>
Multiple items
val Failure : message:string -> exn
Full name: Microsoft.FSharp.Core.Operators.Failure
--------------------
active recognizer Failure: exn -> string option
Full name: Microsoft.FSharp.Core.Operators.( |Failure|_| )
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.decimal
--------------------
type decimal = System.Decimal
Full name: Microsoft.FSharp.Core.decimal
--------------------
type decimal<'Measure> = decimal
Full name: Microsoft.FSharp.Core.decimal<_>
val printfn : format:Printf.TextWriterFormat<'T> -> 'T
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Multiple items
type DateTime =
struct
new : ticks:int64 -> DateTime + 10 overloads
member Add : value:TimeSpan -> DateTime
member AddDays : value:float -> DateTime
member AddHours : value:float -> DateTime
member AddMilliseconds : value:float -> DateTime
member AddMinutes : value:float -> DateTime
member AddMonths : months:int -> DateTime
member AddSeconds : value:float -> DateTime
member AddTicks : value:int64 -> DateTime
member AddYears : value:int -> DateTime
...
end
Full name: System.DateTime
--------------------
System.DateTime()
(+0 other overloads)
System.DateTime(ticks: int64) : unit
(+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: System.DateTimeKind) : unit
(+0 other overloads)
property System.DateTime.Now: System.DateTime
System.DateTime.ToString() : string
System.DateTime.ToString(provider: System.IFormatProvider) : string
System.DateTime.ToString(format: string) : string
System.DateTime.ToString(format: string, provider: System.IFormatProvider) : string
namespace System.Threading
Multiple items
type Thread =
inherit CriticalFinalizerObject
new : start:ThreadStart -> Thread + 3 overloads
member Abort : unit -> unit + 1 overload
member ApartmentState : ApartmentState with get, set
member CurrentCulture : CultureInfo with get, set
member CurrentUICulture : CultureInfo with get, set
member DisableComObjectEagerCleanup : unit -> unit
member ExecutionContext : ExecutionContext
member GetApartmentState : unit -> ApartmentState
member GetCompressedStack : unit -> CompressedStack
member GetHashCode : unit -> int
...
Full name: System.Threading.Thread
--------------------
System.Threading.Thread(start: System.Threading.ThreadStart) : unit
System.Threading.Thread(start: System.Threading.ParameterizedThreadStart) : unit
System.Threading.Thread(start: System.Threading.ThreadStart, maxStackSize: int) : unit
System.Threading.Thread(start: System.Threading.ParameterizedThreadStart, maxStackSize: int) : unit
System.Threading.Thread.Sleep(timeout: System.TimeSpan) : unit
System.Threading.Thread.Sleep(millisecondsTimeout: int) : unit
Multiple items
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
type obj = System.Object
Full name: Microsoft.FSharp.Core.obj
module Seq
from Microsoft.FSharp.Collections
val filter : predicate:('T -> bool) -> source:seq<'T> -> seq<'T>
Full name: Microsoft.FSharp.Collections.Seq.filter
val map : mapping:('T -> 'U) -> source:seq<'T> -> seq<'U>
Full name: Microsoft.FSharp.Collections.Seq.map
What can go wrong?
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
|
public class CustomerService
{
public static Result CreateCustomer(
string id,
string username,
string email,
string name,
string lastName,
string phone,
string password)
{
//Validate
//Persist
}
}
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
//...
CustomerService.CreateCustomer(
"asdfgh-1234-1234",
"aprooks",
"aprooks@live.ru",
"Prooks",
"Alexander",
"somePass",
"79062190016");
|
Introduce Parameter Object (c) Fowler
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
|
public class CreateCustomerDto
{
public CreateCustomer(
string id,
string username,
string email,
string name,
string lastName,
string phone,
string password)
{
this.Id = id;
this.Username = username;
this.Name = name;
this.Surname = lastName;
this.Phone = phone;
this.Password = password;
}
public string ID {get;}
public string Username {get;}
//etc..
}
|
Uniform interface
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
var result = CustomerService.Handle(
new CreateCustomerDto(
id: "Id",
username: "Aprooks",
email: "aprooks@live.ru",
phone: "79062190016"
name: "Alexander",
lastName: "Prooks",
password: "helloWorld"
));
|
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
|
// middleware
public Result Handle<TService, T>(TService service, T request){
Log.Debug("Handled {request}",request);
var validator = ValidationFactory.GetValidator<T>();
if(validator!=null){
var validationResult = validator.Validate(request);
if(!result.IsValid)
return validationResult.ToError();
}
object result;
try{
result = Polly.Handle<TimeoutException>()
.Retry(5)
.Execute(()=> service.Handle(request));
}
catch(Exception ex)
{
return ex.ToError()
}
return Result.Handled(result);
}
|
Functional = data + (pure) functions
F# = types + functions + imperative fallback
Records types
- Flat data
- All fields are required => "AND type"
- Immutable by default (like everything else)
Definition
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
type CreateCustomer = {
Id: string
Username: string
Email: string
Phone: string
Name: string
LastName: string
Password: string
}
|
Generated C# code
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
public sealed class CreateCustomer {
IEquatable<CreateCustomer>,
IStructuralEquatable,
IComparable<CreateCustomer>,
IComparable,
IStructuralComparable
//props
//Constructor
//Interfaces implementations
}
|
Full comparison
Init with data
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
let dto = {
Id= "test"
Username= "aprooks"
Email= "aprooks@live.ru"
Phone= "79062190016"
Name= "Alexander"
LastName= "Prooks"
Password= "secret"
}
|
Syntax sugar
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
let copyPasted = {
Id= "test"
Username= "aprooks"
Email= "aprooks@live.ru"
Phone= "79062190016"
Name= "Alexander"
LastName= "Prooks"
Password= "secret"
}
copyPasted = dto //true
let b = {a with Id="Test2"} //copy
b = a //false
|
Aliases aka document your types
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
// I'm prototyping and not sure what it will be
type Id = NotImplementedException
type Email = string
type Username = string
type CreateCustomer2 = {
Id: Id
Username: Username
Email: Email
Phone: string
Name: string
LastName: string
Password: string
}
|
Discriminated union
- Pick only one of: "OR" type
1:
2:
3:
4:
5:
6:
|
// Choose strictly one
type ``Enum on steroids`` =
| ``I am a valid case without data``
| SomethingElse
| ``I have data`` of Data
| ``I am recursion`` of ``Enum on steroids``
|
Single-case aka data wrapper
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
type Id =
| Id of string
type Email = Email of string
type Username = Username of string
type Customer = {
id: Id
username: Username
email: Email
phone: string
name: string
lastName: string
password: string
}
|
Compile time validation
1:
2:
3:
4:
|
let id = Id "test"
let username = Username "test"
//id = username //compile error
|
Multiple case type (DU)
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
// I wish it never happened
type SystemError =
| DatabaseTimeout
| Unauthorised
// service module
type CustomerServiceError =
| UserAlreadyExists
// Composition root level
type ApplicationErrors =
| System of SystemError
| CustomerService of CustomerServiceError
| OtherService of OtherServiceError
|
Pattern matching to handle them all
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
let toErrorMessage error =
match error with
| System err ->
match err with
| DatabaseTimeout err ->
(HttpStatus.InternalServerError, "Ooops :(")
| Unauthorised err ->
(HttpStatus.Unauthorised, "Go away")
| CustomerService of err ->
match err with
| UserAlreadyExists ->
(HttpStatus.Conflict, "You are already registered")
| OtherService of err ->
OtherService.ToErrorMessage err
|
Option: Empty, but not null
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
type Option<`a> =
| Some of `a
| None
type User = {
Id : UserId
Address : Address option
}
let OnUserRegistered user =
/// blabla
match user.Address with
| Some addr -> sendPostcard addr
| None -> ignore()
|
Result: Done or error?
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
type Result<'TSuccess,'TFailure> =
| Success of 'TSuccess
| Failure of 'TFailure
let registerUser (load, save) user =
let dbUser = load user.Id
match dbUser with
| None ->
save user
Success(user.Id)
| Some _ ->
Error(UserService.AlreadyRegistered)
|
Railway oriented programming
Data everyone else can trust
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
type Id =
private Id of string
module Id =
let create (input : string) =
if (input.Length > 10)
// Imperative style:
failwith new ArgumentException()
else
Id ( input.ToUpperInvariant() )
type UserId = UserId of Id
|
DDD
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
|
type Percent = Percent of decimal
type Amount = Amount of decimal
type NumberOfNights = NumberOfNights of uint
type Discount =
| ``Monetary per night`` of Amount
| ``Percent per night`` of Percent
| ``Monetary per stay`` of Amount
| ``Monetary for extra guest per night`` of uint * Amount
| ``Percent for extra stay`` of NumberOfNights * Percent
// C#: public class MonetaryPerNight: IDiscount
// blah blah
|
Types conclusion
- No boilerplate
- Readability
- Type safety for free
- Design with types
- Unit test only interactions (functions)
Reading signatures
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
|
// string -> string
let append (tail:string) string = "Hello " + tail
// inferred types:
let append tail = "Hello " + tail
// append 10 //compile error
append "world" //"Hello world"
// string -> string -> string
let concat a b = a + b
// unit -> int
let answer() = 42
// string -> unit
let devnull _ = ignore()
|
Function as params
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
|
// (string -> unit) -> (unit->'a)
let sample logger f =
logger "started"
let res = f()
logger "ended"
res
let consoleLogger output =
printfn "%s: %s" (System.DateTime.Now.ToString("HH:mm:ss.f")) output
let result = sample consoleLogger (
fun () ->
System.Threading.Thread.Sleep(500)
42
)
|
'a -> 'a -> 'a
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
|
// int -> int -> int
let sumInts (a:int) (b:int) = a+b
// static int sum(decimal a, decimal b) = { return a+b}
// etc
// 'a -> 'b -> 'c
// when ( ^a or ^b) : (static member ( + ) : ^a * ^b -> ^c)
let inline sum a b = a + b //WAT??
let d = 10m + 10m
let c = "test" + "passed"
let d = 100 + "test" //error
|
Currying
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
// string -> string -> string
let concat x y = string.Concat(x,y)
// <=>
// string -> (string -> string)
// string -> string
let greet = concat "Hello"
// <=>
let greetVerbose w = concat "Hello" w
|
DI F# way
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
module Persistence =
let saveToDb connString (id,obj) =
// blah blah
Success
module CompositionRoot =
let connectionString = loadFromConfig("database")
let save = saveToDb connectionString
let result = save ("123",customer)
|
Data pipe [0]
1:
2:
3:
4:
5:
6:
|
let evenOnly = Seq.filter (fun n -> n%2=0) {0..1000}
let doubled = Seq.map ((*) 2) evenOnly
let stringified = Seq.map (fun d-> d.ToString()) doubled
let greeted = Seq.map greet stringified
// ["Hello 0","Hello 4", ...]
|
Data pipes [1]
1:
2:
3:
4:
5:
|
let inline (|>) f x = x f
let evenF = (|>) ( {0..1000} ) ( Seq.filter (fun n -> n%2=0) )
let evenInfix = {0..1000} |> ( Seq.filter (fun n -> n%2=0) )
|
Piped data!
1:
2:
3:
4:
5:
|
{0..1000}
|> Seq.filter (fun n -> n%2=0) //numbers
|> Seq.map ((*) 2) //evenOnly
|> Seq.map (fun d-> d.ToString()) //doubled
|> Seq.map greet //stringified
|
Real world like
1:
2:
3:
4:
5:
6:
7:
|
let handlingWrapper myHandler request =
request
|> Log "Handling {request}"
|> Validator.EnsureIsValid
|> Deduplicator.EnsureNotDuplicate
|> Throttle (Times 5) myHandler
|> Log "Handling finished with {result}"
|
How to migrate
- Utilities [Paket, Fake]
- Contracts
- Helpers
- Tests [FsCheck, Expecto]
- Code as client
Marketing
- 2-20 times less code
- Better reuse
- Safer code => less bugs
- Human readable code => faster feedback
F# in UI