39
Vincent Pradeilles - CocoaHeads Lyon - 21/09/17 Advanced functional programming Pragmatic use cases for Monads

Advanced functional programing in Swift

Embed Size (px)

Citation preview

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Advanced functional programmingPragmatic use cases for Monads

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Functional Programming

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

First-class and higher-order functionslet double: (Int) -> Int = { $0 * 2 }

func apply(value: Int, function: (Int) -> Int) -> Int { return function(value) }

let result = apply(value: 4, function: double) // result == 8

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Some properties• A function is said to be pure if it produces no side-effects and its

return value only depends on its arguments

• An expression is said to be referentially transparent if it can be replaced by its value without altering the program’s behavior

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Optionals

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Some very familiar codevar data: [String]?

// ...

let result = data?.first?.uppercased()

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Some very familiar codevar data: [String]?

// ...

let result = data?.first?.uppercased()

The operator ?. allows us to declare a workflow that will be executed in order, and will prematurely stop if a nil value is encountered

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Let’s try to write the code for ?.extension Optional { func ?.<U>(lhs: Wrapped?, rhs: (Wrapped) -> U) -> U? { switch self { case .some(let value): return .some(rhs(value)) case .none: return nil } } }

Disclaimer : this is a simplified case for methods with no

arguments

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Arrays

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Let’s manipulate an Arraylet data: [Int] = [0, 1, 2]

let result = data.map { $0 * 2 }.map { $0 * $0 }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Let’s manipulate an Arraylet data: [Int] = [0, 1, 2]

let result = data.map { $0 * 2 }.map { $0 * $0 }

Same thing here: the function map allows us to declare a workflow

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

A possible implementation for mapextension Array { func map<U>(_ transform: (Element) -> U) -> [U] { var result: [U] = [] for e in self { result.append(transform(e)) } return result } }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Functions

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Let’s compose functionslet double: (Int) -> Int = { x in x * 2 } let square: (Int) -> Int = { x in x * x }

infix operator • : AdditionPrecedence

func •<T, U, V>(lhs: (U) -> V, rhs: (T) -> U) -> ((T) -> V) { return { t in lhs(rhs(t)) } }

let result = (double • square)(4) // result == 32

Disclaimer : @escaping attributes have been omitted

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Let’s compose functionslet double: (Int) -> Int = { x in x * 2 } let square: (Int) -> Int = { x in x * x }

infix operator • : AdditionPrecedence

func •<T, U, V>(lhs: (U) -> V, rhs: (T) -> U) -> ((T) -> V) { return { t in lhs(rhs(t)) } }

let result = (double • square)(4) // result == 32

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

We have three similar behaviors, yet backed by very different

implementation

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

What are the common parts?

• They contain value(s) inside a context

• They add new features to existing types

• They provide an interface to transform/map the inner value

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Monad: intuitive definition• Wraps a type inside a context

• Provides a mechanism to create a workflow of transforms

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Minimal Monadstruct Monad<T> { let value: T // additional data static func just(_ value: T) -> Monad<T> { return self.init(value: value) } }

infix operator >>> : AdditionPrecedence

func >>> <U, V>(lhs: Monad<U>, rhs: (U) -> Monad<V>) -> Monad<V> { // specific combination code }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Let’s look at an application

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Writer Monad• We have an application that does a lot of numerical calculation

• It’s hard to keep track of how values have been computed

• We would like to have a way to store a value along with a log of all the transformation it went through

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Writer Monadstruct Logged<T> { let value: T let logs: [String] private init(value: T) { self.value = value self.logs = ["initialized with value: \(self.value)"] } static func just(_ value: T) -> Logged<T> { return Logged(value: value) } }

func >>> <U, V>(lhs: Logged<U>, rhs: (U) -> Logged<V>) -> Logged<V> { let computation = rhs(lhs.value) return Logged<V>(value: computation.value, logs: lhs.logs + computation.logs) }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Writer Monadfunc square(_ value: Int) -> Logged<Int> { let result = value * value return Logged(value: result, log: "\(value) was squared, result: \(result)") }

func halve(_ value: Int) -> Logged<Int> { let result = value / 2 return Logged(value: result, log: "\(value) was halved, result: \(result)") }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Writer Monadlet computation = .just(4) >>> square >>> halve

print(computation)

// Logged<Int>(value: 8, logs: ["initialized with value: 4", "4 was squared, result: 16", "16 was halved, result: 8"])

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Now let’s think architecture

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Now let’s think architectureWe can model the current state of an app with a struct

struct AppState { let userName: String let currentSong: URL? let favoriteSongs: [URL] }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Now let’s think architectureWe can model the action our app emits, in a declarative fashion

protocol Action { }

struct UpdateUserNameAction: Action { let newUserName: String }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Now let’s think architectureWe can react to those actions in a functional way

infix operator >>> : AdditionPrecedence

func >>>(appstate: AppState?, action: Action) -> AppState { var appstate = appstate ?? AppState() switch action { case let newUserNameAction as UpdateUserNameAction: appstate.userName = newUserNameAction.newUserName default: break } return appstate }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Now let’s think architectureAnd finally we put everything together

AppState() >>> UpdateUserNameAction(newUserName: "Vincent")

// AppState(userName: Optional("Vincent"), currentSong: nil, favoriteSongs: [])

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

ReSwift

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

ReSwift

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Minimal exampleimport ReSwift

struct AppState: StateType { // ... app state properties here ... }

func appReducer(action: Action, state: AppState?) -> AppState { // ... }

let store = Store( reducer: appReducer, state: AppState(), // You may also start with `nil` middleware: []) // Middlewares are optional

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

A more « real life » example

func appReducer(action: Action, state: AppState?) -> AppState { return AppState( navigationState: navigationReducer(state?.navigationState, action: action), authenticationState: authenticationReducer(state?.authenticationState, action: action), repositories: repositoriesReducer(state?.repositories, action: action), bookmarks: bookmarksReducer(state?.bookmarks, action: action) ) }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

A more « real life » examplefunc authenticationReducer(state: AuthenticationState?, action: Action) -> AuthenticationState { var state = state ?? initialAuthenticationState() switch action { case _ as ReSwiftInit: break case let action as SetOAuthURL: state.oAuthURL = action.oAuthUrl case let action as UpdateLoggedInState: state.loggedInState = action.loggedInState default: break } return state }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

A more « real life » examplelet store: Store<AppState> = Store(reducer: appReducer, state: nil) // in AppDelegate file

class BookmarksViewController: UIViewController, StoreSubscriber { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) store.subscribe(self) { subcription in return subcription.select { state in return state.bookmarks } } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated)

store.unsubscribe(self) } func newState(state: StateType?) { // update view } }

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Advantages

• Reducers are pure (stateless) functions, easy to test

• Code review of business logic is easier

• A « demo » mode of the app is easy to implement (UI Testing)

• Deep-linking becomes a trivial use-case

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Drawbacks

• Not a silver bullet, a bit overkill for web-service driven apps

• Requires to create a lot of types (but at least not ObjC types)

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Questions ?

Vincent Pradeilles - CocoaHeads Lyon - 21/09/17

Bibliography• https://en.wikipedia.org/wiki/Monad_(functional_programming)

• https://www.youtube.com/watch?v=ZhuHCtR3xq8 (Brian Beckman: Don't fear the Monad)

• https://academy.realm.io/posts/slug-raheel-ahmad-using-monads-functional-paradigms-in-practice-functors-patterns-swift/ (Using Monads and Other Functional Paradigms in Practice)

• https://github.com/orakaro/Swift-monad-Maybe-Reader-and-Try

• https://academy.realm.io/posts/benji-encz-unidirectional-data-flow-swift/ (Unidirectional Data Flow: Shrinking Massive View Controllers)