val x : int
Full name: index.x
val y : int
Full name: index.y
Multiple items
val char : value:'T -> char (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.char
--------------------
type char = System.Char
Full name: Microsoft.FSharp.Core.char
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<_>
Multiple items
val int16 : value:'T -> int16 (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int16
--------------------
type int16 = System.Int16
Full name: Microsoft.FSharp.Core.int16
--------------------
type int16<'Measure> = int16
Full name: Microsoft.FSharp.Core.int16<_>
Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
module String
from Microsoft.FSharp.Core
Multiple items
val double : value:'T -> double (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.double
--------------------
type double = System.Double
Full name: Microsoft.FSharp.Core.double
type bool = System.Boolean
Full name: Microsoft.FSharp.Core.bool
Multiple items
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IEnumerable
interface IEnumerable<'T>
member GetSlice : startIndex:int option * endIndex:int option -> 'T list
member Head : 'T
member IsEmpty : bool
member Item : index:int -> 'T with get
member Length : int
member Tail : 'T list
static member Cons : head:'T * tail:'T list -> 'T list
static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
val id : x:'T -> 'T
Full name: Microsoft.FSharp.Core.Operators.id
module Option
from Microsoft.FSharp.Core
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val enum : value:int32 -> 'U (requires enum)
Full name: Microsoft.FSharp.Core.Operators.enum
type 'T option = Option<'T>
Full name: Microsoft.FSharp.Core.option<_>
Who am I?
Youenn Bouglouan
C# by day, F# by night
Why today's topic?
Waterfall vs Agile
Java vs C#
PC vs Mac (vs Linux)
Object Oriented vs Functional
Weakly Typed vs Strongly Typed
Who is this presentation for?
Developers -> get a better idea of what's out there
QA Specialists -> understand why there are bugs and issues
What's a Type?
A way to represent data or behavior within a programming language
- primitive types (int, bool, char, float...)
- product types (classes, records, objects, tuples...)
- sum types (unions but not only... we'll see those later!)
- sets (lists, arrays, maps, dictionaries...)
- functions(!)
- interfaces(!)
What's a Type System?
For a given language:
- defines the kind of types available
- maps types to the various constructs (expressions, variables, functions... )
- determines how types interact with each other
- sets the rules that make a type either valid or invalid
Typing:
Dynamic vs Static
Dynamic
types are checked at runtime -> runtime errors
1:
2:
3:
4:
5:
|
// Python
x = 10
y = x + 1 // Ok
z = x + "1" // Runtime error
// TypeError: unsupported operand type(s) for +: 'int' and 'str'
|
Static
Types are checked at compile time -> compilation errors
1:
2:
3:
4:
5:
|
// F#
let x = 10
let y = x + 1 // Ok
let z = x + "1" // Compiler error
// The type 'string' does not match the type 'int'
|
Typing:
Weak vs Strong
Not the same as Dynamic vs Static!
Weak
type checking is not (very) strict
implicit conversions (usually)
little guarantees on the program's correctness
Weak typing in a dynamic language
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
// JavaScript
'1' + '2' // returns the string "12"
'1' - '2' // returns the number -1
'1' * '2' // returns the number 2
var myObject = { valueOf: function () { return 3 }} // myObject is an Object
'1' + myObject // returns a string: "13"
1 + myObject // returns a string: 4
[] + {} // returns an Object
{} + [] // returns the number 0... wait whaaaaaaaaaaaaaaaaaaaat?
|
Weak typing in a static language - 1
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
// C#
var x = 2 + 2.0; // x is a double with value 4
var y = 2.1 + "2"; // y is a string with value "2.12"
char c = 'a';
int i = c; // i is an integer with value 97
int x = 100000;
int y = (short) x; // y is an integer with value -31072 due to integer overflow
|
Weak typing in a static language - 2
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
// C#
public class Animal {}
public class Reptile : Animal {}
public class Mammal : Animal {}
public void Test(Animal a)
{
var r = (Reptile) a;
}
Test(new Mammal());
// Run-time exception: Unable to cast object of type 'Mammal' to type 'Reptile'.
|
Strong
Type checking is strict(er)
program's correctness is easier to prove (well, in theory)
conversions must be explicit
Strong typing in a dynamic language
Back to our first example in Python!
1:
2:
3:
4:
5:
|
// Python
x = 10
y = x + 1 // Ok
z = x + "1" // Runtime error
// TypeError: unsupported operand type(s) for +: 'int' and 'str'
|
Strong typing in a static language
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
// F#
let x = 2 + 2.0 // Compiler error, The type 'float' does not match the type 'int'
let y = 2.1 + "2" // Compiler error, The type 'string' does not match the type 'float'
let c: char = 'a'
let i: int = c // Compiler error, expected to have type 'int' but here has type 'char'
let x: int = 100000
let y: int = int16 x // Compiler error, expected to have type 'int' but here has type 'int16'
let z = int16 x // this compiles and runs, but the result still is -31072
|
Typing:
Nominal vs Structural
applies to static typing
Nominal
Very popular in mainstream languages like C#, C++, or Java
Types are identified by their respective names
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
// C#
class Employee { public string Name; }
class Animal { public string Name; }
string Hire(Employee employee)
{
// some fancy logic here...
return employee.Name;
}
var name = Hire(new Employee()); // This works
var name = Hire(new Animal()); // This doesn't as 'Hire' explicitly expects an 'Employee'
|
Structural
Also called row polymorphism
Types are identified by their respective structures and properties
Present under different forms in Elm, Go, TypeScript, Scala, OCaml, Haskell...
1:
2:
3:
4:
5:
6:
7:
8:
|
// Elm
hire: { name: String } -> String
hire entity =
// Some fancy logic here...
entity.name
hire { name = "Tomek Nowak" } // This works, returns the string 'Tomek Nowak'
hire { name = "Garfield" } // This also works, returns the string 'Garfield'
|
But there's more...
Structural subtyping
Beware the awesomeness!
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
|
// Elm
type alias TypeWithName a = { a | name: String }
hire: TypeWithName a -> String
hire entity =
// Some fancy logic here...
entity.name
hire { name = "Tomek Nowak", age = 25, gender = "male" } // This still works!
hire { name = "Garfield", canFly = False, hasPaws = True } // And this works too!
|
The actual structure of the types is checked as compile-time,
making this super safe while giving a dynamic feel to the language
Structural typing
for implicit interface implementation
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
|
// Go
type Stringer interface {
String() string
}
import ("fmt")
type User struct {
name string
}
func (user User) String() string {
return fmt.Sprintf("User: name = %s", user.name)
}
func main() {
user := User{name: "Tomek Nowak"}
fmt.Println(user) // fmt.Println(...) takes a Stringer interface as parameter
//prints 'User: name = Tomek Nowak'
}
|
Duck Typing
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
basically the same as structural typing, but at runtime!
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
|
// Python
class Duck:
def quack(self):
print("Quack!")
class Dog:
def bark(self):
print("Woooof!")
def lets_quack(animal):
animal.quack()
donaldTusk = Duck()
rex = Dog()
lets_quack(donaldTusk) // prints 'Quack!'
lets_quack(rex) // runtime error!
|
Basic Type Inference
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
// C++
void withoutTypeInference(std::vector<std::complex<double>> & myVector)
{
std::vector<std::complex<double>>::const_iterator it = myVector.begin();
// ...
}
void withTypeInference(std::vector<std::complex<double>> & myVector)
{
auto it = myVector.begin();
// ...
}
|
Advanced Type Inference
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
|
// F#
type Customer = {
Id: Guid
Name: string
Age: int
}
// This is equivalent to:
// public Customer createCustomer(string name, int age) {...}
let createCustomer name age =
{
Id = Guid.NewGuid()
Name = name
Age = age
}
// This will generate a compiler error
let newCustomer = createCustomer 18 "Tomek Nowak"
|
https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system
Part II
So, do you even type??
Let's start with a simple example
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
|
public interface ICustomerService
{
Customer CreateCustomer(CustomerDto dto);
}
public class CustomerService : ICustomerService
{
public Customer CreateCustomer(CustomerDto dto)
{
// implementation here
}
}
|
A possible and plausible implementation
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
|
public Customer CreateCustomer(CustomerDto dto)
{
var isValid = Validate(dto);
if (isValid)
{
var newCustomer = _dbService.SaveCustomer(dto);
return newCustomer;
}
else
{
return null;
}
}
private bool Validate(CustomerDto dto)
{
return dto.Code != "" && dto.Contact.Email.IsValid() && dto.Age >= 18;
}
|
Possible outcomes
not null |
true |
new Customer |
not null |
false |
null |
null |
throws exception |
failure |
Other possible scenarios:
- _dbService is null -> throws exception
- _dbService throws exception -> failure
Let's take a step back
1:
2:
3:
4:
|
public interface ICustomerService
{
Customer CreateCustomer(CustomerDto dto);
}
|
Just another LOB app
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
public void DisplaySalesReport(DateRangeFilter dateRange)
{
var canGenerateReport = await authService.GetPermissions(_session.GetCurrentUser());
if (!canGenerateReport)
return;
var salesResults = await _salesService.GetSalesResults(dateRange, CurrentCountry);
var products = await _productsService.RetrieveProductCatalog(dateRange.Year, ProductCategory);
var salesReport = await ReportHelper.GenerateReport(salesResults, products, SelectedCustomers);
Show(salesReport);
}
|
A few bug reports later...
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
|
public void DisplaySalesReport(DateRangeFilter dateRange)
{
var canGenerateReport = await authService.GetPermissions(_session.GetCurrentUser());
if (!canGenerateReport)
return;
if (dateRange != null && dateRange.Year != null)
{
var salesResults = await _salesService.GetSalesResults(dateRange, CurrentCountry);
var products = await _productsService.RetrieveProductCatalog(dateRange.Year, ProductCategory);
if (salesResults == null || products == null)
return;
if (SelectedCustomers == null)
SelectedCustomers = new List<Customer>();
var salesReport = await ReportHelper.GenerateReport(salesResults, products, SelectedCustomers);
if (salesReport != null)
Show(salesReport);
}
}
|
Slapping a try-catch there too, just in case of!
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
|
public void DisplaySalesReport(DateRangeFilter dateRange)
{
try
{
var canGenerateReport = await authService.GetPermissions(_session.GetCurrentUser());
if (!canGenerateReport) return;
if (dateRange != null && dateRange.Year != null)
{
var salesResults = await _salesService.GetSalesResults(dateRange, CurrentCountry);
var products = await _productsService.RetrieveProductCatalog(dateRange.Year, ProductCategory);
if (salesResults == null || products == null) return;
if (SelectedCustomers == null) SelectedCustomers = new List<Customer>();
var salesReport = await ReportHelper.GenerateReport(salesResults, products, SelectedCustomers);
if (salesReport != null) Show(salesReport);
}
} catch (Exception)
{
Log.Error("oopsie... I guess we didn't handle all nulls and exceptions after all...")
}
}
|
So now we know what the problem is
exceptions and nulls make our code brittle and unreliable
What can we do about it?
Get rid of nulls and exceptions altogether!
Let's see how to achieve this in 3 little steps
Step 1
Introducing sum types and pattern matching
Sum types can be seen as enums on steroids!
Also called Choice Types, Discriminated Unions, Tagged Unions...
Available in F#, Elm, Haskell, TypeScript, Scala, Rust, Swift...
Very powerful when combined with exhaustive pattern matching
Demo - sum types and pattern matching
sum types and pattern matching in a nutshell
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
// F#
type EntityId =
| Customer of customerCode: string
| SalesReceipt of documentId: Guid
| Product of productId: integer
match entity with
| Customer code -> // the entity is a customer
| SalesReceipt id -> // the entity is a sales receipt
| Product id -> // the entity is a product
// The compiler makes sure you haven't forgotten a case!
|
Step 2
Getting rid of nulls using Option
Just a specific sum type with 2 cases!
Option makes the possible absence of value explicit
You have to deal with it at compile time
1:
2:
3:
4:
|
// F#
type Option<'a> =
| Some of 'a
| None
|
1:
2:
|
// Haskell and Elm
data Maybe a = Just a | Nothing
|
1:
2:
3:
4:
5:
|
// Rust
enum Option<T> {
None,
Some(T),
}
|
Demo - fix nulls using Option
Option in a nutshell
1:
2:
3:
4:
5:
6:
7:
8:
9:
|
// F#
type Contact = {
Email: string
PhoneNumber: string option // Explicilty mark the phone number as not mandatory
}
match contact.PhoneNumber with
| Some phoneNumber -> // deal with the phone number
| None -> // deal with the absence of phone number
|
Step 3
Getting rid of exceptions using Result
Yet another sum type!
Result makes both the success and error path explicit
1:
2:
3:
4:
|
// F#
type Result<'T, 'Error> =
| Ok of 'T
| Error of 'TError
|
1:
2:
|
// OCaml
type ('a, 'b) result = Ok of 'a | Error of 'b type
|
1:
2:
3:
4:
5:
|
// Rust
enum Result<T, E> {
Ok(T),
Err(E),
}
|
Demo - fix exceptions using Result
Result in a nutshell
1:
2:
3:
4:
5:
6:
7:
|
// F#
// Result<Customer, string> validateCustomer(Customer customer) {...}
let validateCustomer customer =
if customer.Code <> "" && customer.Age >= 18 then
Ok customer
else
Error "the customer is not valid!"
|
Enables Railway-Oriented Programming
https://fsharpforfunandprofit.com/rop/
That's it!
What did we achieve?
Direct outcomes
- no more null checks everywhere
- no more exceptions and try-catches everywhere
Indirect outcomes
- no more lies!
- our models are more expressive (Option)
- our code is more readable and explicit (Result)
- the compiler is our friend -> "if it compiles, then it works"
- TDD!
*Type Driven Development
Where to go from here?
Learn new languages (Elm, Elixir, F#, Rust, TypeScript)
Explore new concepts (FP, Actor Model...)