From Options to Observables: a monadic journey

Miłosz Piechocki

Speaker

Miłosz Piechocki

"From Options to Observables: a monadic journey" [EN]

2018-03-14

twitter.com/miloszpp

Definition

Monad is just a monoid in the category of endofunctors.

Saunders Mac Lane

WTF?

Let's start from the beginning..

Model

            
                interface Employee {
                    id: number;
                    name: string;
                    supervisorId?: number;
                }
            
        

Repository

            
                class EmployeeRepository {
                    private employees: Employee[] = [ /* ... */ ];
                
                    get(id: number): Employee {
                        return this.employees.find(e => e.id === id);
                    }
                }
            
        

Dealing with empty results

            
                function getEmployeeName(id: number) {
                    const employee = repository.get(id);
                    if (employee !== undefined) {
                        return employee.name;
                    }
                    return undefined;
                }
            
        

Dealing with empty results

            
                function getEmployeeSupervisor(id: number) {
                    const employee = repository.get(id);
                    if (employee !== undefined && 
                        employee.supervisorId !== undefined) {
                        return repository.get(employee.supervisorId);
                    }
                    return undefined;
                }
            
        

Dealing with empty results

            
                function getEmployeeSupervisorName(id: number) {
                    const employee = repository.get(id);
                    if (employee !== undefined && 
                        employee.supervisorId !== undefined) {
                        const supervisor = repository.get(employee.supervisorId);
                        if (supervisor !== undefined)
                            return supervisor.name;
                    }
                    return undefined;
                }
            
        
            
                function getEmployeeSupervisorNameSafe(id: number) {
                    if (id !== undefined) {
                        const employee = repository.get(id);
                        if (employee !== undefined && 
                            employee.supervisorId !== undefined) {
                            const supervisor = repository.get(employee.supervisorId);
                            if (supervisor !== undefined)
                                return supervisor.name;
                        }
                    }
                    return undefined;
                }
            
        

Option to the rescue!

Enter Option

            
                    interface Option<T> {
                        getOrElse(defaultValue: T): T;
                    }
            
        

Some result is present!

            
                    class Some<T> implements Option<T> {
                        constructor(private value: T) {}
                    
                        getOrElse(defaultValue: T): T {
                            return this.value;
                        }
                    }
            
        

None result is present...

            
                    class None<T> implements Option<T> {
                        getOrElse(defaultValue: T): T {
                            return defaultValue;
                        }
                    }
            
        

Updated Model

            
                interface Employee {
                    id: number;
                    name: string;
                    supervisorId: Option<number>;
                }
            
        

Updated Repository

            
                class EmployeeRepository {
                    private employees: Employee[] = [ /* ... */ ];
                
                    get(id: number): Option<Employee> {
                        const result = this.employees.find(e => e.id === id);
                        return result ? new Some(result) : new None();
                    }
                }
            
        

Transforming content: map

            
                    interface Option<T> {
                        getOrElse(defaultValue: T): T;
                        map<R>(project: (inner: T) => R)
                            : Option<R>;
                    }
            
        

Transforming content: map for Some

            
                    class Some<T> implements Option<T> {                                            
                        map<R>(project: (inner: T) => R)
                            : Option<R> {
                            return new Some(project(this.value));
                        }
                        // ...
                    }
            
        

Transforming content: map for None

            
                    class None<T> implements Option<T> {
                        map<R>(project: (inner: T) => R)
                            : Option<R> {
                            return new None<R>();
                        }
                        // ...
                    }
            
        

Remember this?

            
                function getEmployeeName(id: number) {
                    const employee = repository.get(id);
                    if (employee !== undefined) {
                        return employee.name;
                    }
                    return undefined;
                }
            
        

It's a one-liner now

            
                function getEmployeeName(id: number): Option<string> {
                    return repository.get(id).map(e => e.name);
                }
            
        

How about this one?

            
                function getEmployeeSupervisor(id: number)
                    : Option<Option<number>> {
                    return repository.get(id)
                        .map(e => e.supervisorId);
                }
            
        

We need flatMap!

Enter flatMap

            
                export interface Option<T> {
                    getOrElse(defaultValue: T): T;
                    map<R>(project: (inner: T) => R): Option<R>;
                    flatMap<R>(project: (inner: T) => Option<R>)
                        : Option<R>;
                }
            
        
            
                function getEmployeeSupervisorNameSafe(id: number) {
                    if (id !== undefined) {
                        const employee = repository.get(id);
                        if (employee !== undefined && 
                            employee.supervisorId !== undefined) {
                            const supervisor = repository.get(employee.supervisorId);
                            if (supervisor !== undefined)
                                return supervisor.name;
                        }
                    }
                    return undefined;
                }
            
        

flatMap power!

            
                function getEmployeeSupervisorNameSafe(idOption: Option<number>) {
                    return idOption
                        .flatMap(id => repository.get(id))
                        .flatMap(employee => employee.supervisorId)
                        .flatMap(supervisorId => repository.get(supervisorId))
                        .map(supervisor => supervisor.name);
                }
            
        

How about errors?

Updated Model

            
                interface Employee {
                    id: number;
                    name: string;
                    supervisorId: Result<number, string>;
                }
            
        

Updated Repository

            
                get(id: number): Result<Employee, string> {
                    const result = this.employees.find(e => e.id === id);
                    return result 
                        ? new Success(result) 
                        : new Failure("No employee found");
                }
            
        

This code still works!

            
                function getEmployeeSupervisorNameSafe(idResult)
                    : Result<string, string> {
                    return idResult
                        .flatMap(id => repository.get(id))
                        .flatMap(employee => employee.supervisorId)
                        .flatMap(supervisorId => repository.get(supervisorId))
                        .map(supervisor => supervisor.name);
                }
            
        

Employee hierarchy

            
                private employees: Employee[] = [
                    { id: 1, name: "Piotr Kowalski", 
                        supervisorId: new Success(2) },
                    { id: 2, name: "Katarzyna Grabowska", 
                        supervisorId: new Failure("No supervisor") },
                    { id: 3, name: "Piotr Zientara", 
                        supervisorId: new Success(2) },
                ];
            
        

Sample calls

            
                    getEmployeeSupervisorNameSafe(new Success(1));
                    // Success {value: "Katarzyna Grabowska"}
                    getEmployeeSupervisorNameSafe(new Success(2));
                    // Failure {failure: "No supervisor"}
                    getEmployeeSupervisorNameSafe(new Success(5));
                    // Failure {failure: "No employee found"}
            
        

Did we just invent exceptions?

Railway-oriented programming

Results

flatMap (a.k.a. bind)

https://fsharpforfunandprofit.com/rop/

Handling future results

Updated Model

            
                interface Employee {
                    id: number;
                    name: string;
                    supervisorId: Promise<number>;
                }
            
        

Updated Repository

            
                get(id: number): Promise<Employee> {
                    const result = this.employees.find(e => e.id === id);
                    return result 
                        ? Promise.resolve(result) 
                        : Promise.reject("No employee found");
                }
            
        

Still (almost) the same code!

            
                function getEmployeeSupervisorNameSafe(idPromise) 
                    : Promise<number>{
                    return idPromise
                        .then(id => repository.get(id))
                        .then(employee => employee.supervisorId)
                        .then(supervisorId => repository.get(supervisorId))
                        .then(supervisor => supervisor.name);
                }
            
        

And finally... Observables!

Updated Model

            
                    interface Employee {
                        id: number;
                        name: string;
                        supervisorId: Observable<number>;
                    }
            
        

Updated Repository

            
                get(id: number): Observable<Employee> {
                    const result = this.employees.find(e => e.id === id);
                    return result 
                        ? Observable.of(result) 
                        : Observable.throw("No employee found");
                }
            
        

Exactly the same code as in Result!

            
                function getEmployeeSupervisorNameSafe(id$): Observable<number> {
                    return id$
                        .flatMap(id => repository.get(id))
                        .flatMap(employee => employee.supervisorId)
                        .flatMap(supervisorId => repository.get(supervisorId))
                        .map(supervisor => supervisor.name);
                }
            
        

Monad

  1. Has flatMap (a.k.a. bind)

Monad

  1. Amazing abstraction

Miłosz Piechocki

From Options to Observables: a monadic journey

2018-03-14

twitter.com/miloszpp

codewithstyle.info