Slide 1

Slide 1 text

Type-safe URL Routing in Swift 2016/10/01-02 #Functional Swift Conference Yasuhiro Inami / @inamiy

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

URL Routing

Slide 4

Slide 4 text

http://funswiftconf.com/ Hello/Budapest/2016

Slide 5

Slide 5 text

/Hello/Budapest/2016

Slide 6

Slide 6 text

/Hello/{city}/{year}

Slide 7

Slide 7 text

URL routings everywhere! • Client app • Universal Links (>= iOS 9) • Custom URL Schemes (<= iOS 8) • Server-side • Perfect, Kitura, Zewo, Vapor, etc • Mostly, Node.js Express-like

Slide 8

Slide 8 text

Client-side (example) enum Sitemap { case hello(city: String, year: Int) // "/Hello/{city}/{year}" case notFound } struct Router { func match(path: String) -> Sitemap { if let captures: [String] = myRegex("/Hello/(.+)/(\\d+)", path), captures.count == 2, let year = Int(captures[1]) { return .hello(captures[0], year) } return .notFound } }

Slide 9

Slide 9 text

func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> () ) -> Bool { // iOS Universal Links only guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } let sitemap = self.router.match(url.path) switch sitemap { case let .hello(city, year): self.presentAwesomeOpeningMovie(city, year) return true case .notFound: print("Invalid URL.") return false } }

Slide 10

Slide 10 text

Server-side (example) // Perfect routes.add(method: .get, uri: "/Hello/{city}/{year}", handler: requestHandler) // Zewo route.get("/Hello/:city/:year") { request in guard let city: String = request.pathParameters["city"], let year: String = request.pathParameters["year"] else { ... } /* Warning: All pathComponents are captured in String! */ ... } // Vapor drop.get("Hello", String.self, Int.self) { (request, city: String, year: Int) in ... // Wow, type-safe!? }

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

// Vapor routing APIs // ⚠ AUTOMATICALLY GENERATED CODE /// GET // public func get(_ p0: String = "", handler: (Request) throws -> ResponseRepresentable) /// GET /{wildcard}/ public func get(_ w0: W0.Type, handler: (Request, W0) throws -> ResponseRepresentable) /// GET //{wildcard}/ public func get(_ p0: String, _ w0: W0.Type, handler: (Request, W0) throws -> ResponseRepresentable) /// GET /// public func get(_ p0: String, _ p1: String, handler: (Request) throws -> ResponseRepresentable) /// GET /{wildcard}//{wildcard}/ public func get(_ w0: W0.Type, _ p0: String, _ w1: W1.Type, handler: (Request, W0, W1) throws -> ResponseRepresentable)

Slide 13

Slide 13 text

Metaprogramming !

Slide 14

Slide 14 text

GET /{wildcard}// {wildcard}//{wildcard}/...

Slide 15

Slide 15 text

!

Slide 16

Slide 16 text

Functional Programming

Slide 17

Slide 17 text

https://realm.io/news/ tryswift-yasuhiro-inami-parser-combinator

Slide 18

Slide 18 text

Parser Combinator Parse character by character

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

Parse string by string string = pathComponent

Slide 21

Slide 21 text

Route type /// Function-based URL parser. public struct Route { let parse: ([String]) -> ([String], Value)? } • Container of state-transforming function • Input: pathComponents as [String] • Output: Optional tuple of "Value" & "remaining input"

Slide 22

Slide 22 text

// MARK: Functor public func <^> ( f: @escaping (Value1) -> Value2, route: Route ) -> Route { return Route { pathComponents in let reply = route.parse(pathComponents) switch reply { case let .some(pathComponents2, value): return (pathComponents2, f(value)) case .none: return nil } } }

Slide 23

Slide 23 text

// MARK: Applicative public func pure(_ value: Value) -> Route { return Route { ($0, value) } } public func <*> ( route1: Route<(Value1) -> Value2>, route2: @autoclosure @escaping () -> Route ) -> Route { return Route { pathComponents in let reply = route1.parse(pathComponents) switch reply { case let .some(pathComponents2, f): return (f <^> route2()).parse(pathComponents2) case .none: return nil } } }

Slide 24

Slide 24 text

// MARK: Applicative public func <**> ( route1: Route, route2: @autoclosure @escaping () -> Route<(Value1) -> Value2> ) -> Route { return { a in { $0(a) } } <^> route1 <*> route2 } public func <* ( route1: Route, route2: @autoclosure @escaping () -> Route ) -> Route { return const <^> route1 <*> route2 } public func *> ( route1: Route, route2: @autoclosure @escaping () -> Route ) -> Route { return const(id) <^> route1 <*> route2 }

Slide 25

Slide 25 text

// MARK: Alternative public func empty() -> Route { return Route { _ in nil } } public func <|> ( route1: Route, route2: @autoclosure @escaping () -> Route ) -> Route { return Route { pathComponents in let reply = route1.parse(pathComponents) switch reply { case .some: return reply case .none: return route2().parse(pathComponents) } } }

Slide 26

Slide 26 text

// MARK: Monad // Comment-Out: // We don't need `Monad` because when we make // a URL route chaining, previous route doesn't // affect the next route (context-free grammar). // // In other word, there is no need to create // a new `Route` at runtime, // and only applying existing routes // one by one sequentially is sufficient.

Slide 27

Slide 27 text

Monadic Applicative URL Parser

Slide 28

Slide 28 text

public func capture(_ parse: @escaping (String) -> Value?) -> Route { return Route { pathComponents in if let (first, rest) = uncons(pathComponents), let parsed = parse(first) { return (Array(rest), parsed) } return nil } } public func int() -> Route { return capture { Int($0) } } public func match(_ string: String) -> Route<()> { return capture { $0 == string ? () : nil } } public func choice(_ routes: [Route]) -> Route { return routes.reduce(empty()) { $0 <|> $1 } }

Slide 29

Slide 29 text

Let's parse!

Slide 30

Slide 30 text

/// Parses "/Hello/{city}/{year}". let route: Route<(String, Int)> = curry { ($1, $2) } <^> match("Hello") <*> string() <*> int() // or, more fancy flipped way: // let route = // /"Hello" > string() > int() <&!> flipCurry { ($1, $2) } let value1 = route.run(["Hello", "Budapest", "2016"]) expect(value1?.0) == "Budapest" expect(value1?.1) == 2016 let value2 = route.run(["Ya", "tu", "sabes"]) expect(value2).to(beNil())

Slide 31

Slide 31 text

/// - "/R/foo/{int}" ==> `Sitemap.foo(int)` /// - "/R/bar/{double}" ==> `Sitemap.bar(double)` /// - "/R/baz/{string}" ==> `Sitemap.baz(string)` /// - "/R/foo/bar" ==> `Sitemap.fooBar` /// - otherwise ==> `Sitemap.notFound` let route: Route = match("R") *> choice([ match("foo") *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar), ]) <|> pure(Sitemap.notFound) // `<&>` = flipped map = `flip(<^>)`

Slide 32

Slide 32 text

// (continued) let route: Route = match("R") *> choice([ match("foo") *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar), ]) <|> pure(Sitemap.notFound) expect(route.run(["R", "foo", "123"])) == .foo(123) expect(route.run(["R", "bar", "4.5"])) == .bar(4.5) expect(route.run(["R", "baz", "xyz"])) == .baz("xyz") expect(route.run(["R", "foo", "bar"])) == .fooBar expect(route.run(["R", "foo", "xxx"])) == .notFound

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Really?

Slide 35

Slide 35 text

// Naive choice. choice([ match("foo") *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar), ]) 1. Check "foo" + int 2. Check "bar" + double 3. Check "baz" + string 4. Check "foo" + "bar" ... Checking "foo" twice! !

Slide 36

Slide 36 text

We rather want... // Optimized choice. choice([ match("foo") *> choice([ int() <&> Sitemap.foo, match("bar") *> pure(Sitemap.fooBar) ]), match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz ])

Slide 37

Slide 37 text

Interpret using enum

Slide 38

Slide 38 text

New Route type (Enum-based) /// Enum-based URL route. public indirect enum Route { case match(String, Route) case capture((String) -> Any?, (Any) -> Route) case choice([Route]) case term(Value) case zero } • .match contains next Route • .capture contains "parsing" and "converting" functions

Slide 39

Slide 39 text

extension Route { public func run(_ pathComponents: [String]) -> Value? { switch self { case let .match(string, route): ... case let .capture(parse, convert): ... case let .choice(routes): ... case let .term(value): return value case .zero: return nil } } } // Implement `run`, Functor, Applicative, Alternative...

Slide 40

Slide 40 text

Optimization

Slide 41

Slide 41 text

public func optimize(_ route: Route) -> Route { switch route { case let .match(string, route): return .match(string, optimize(route)) case let .capture(parse, convert): return .capture(parse, { optimize(convert($0)) }) case let .choice(routes): return _optimizeChoice(routes) case .term, .zero: return route } }

Slide 42

Slide 42 text

private func _optimizeChoice( _ routes: [Route] ) -> Route { if routes.isEmpty { return .zero } // Sort, group by the same route-head, and flatten. let sortedRoutes = routes.sorted(by: isAscending) let arrangedRoutes = groupBy(isSameRouteHead)(ArraySlice(sortedRoutes)) .map(flatten) return choice(arrangedRoutes) }

Slide 43

Slide 43 text

Let's optimize!

Slide 44

Slide 44 text

// Before optimization (duplicated `match("foo")`) let naiveRoute: Route = match("R") *> choice([ match("foo") *> int() <&> Sitemap.foo, match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz, match("foo") *> match("bar") *> pure(Sitemap.fooBar) ]) // After optimization let optimizedRoute = optimize(naiveRoute)

Slide 45

Slide 45 text

Result: naiveRoute = .match("R", .choice([ .match("foo", .capture(int)), .match("bar", .capture(double)), .match("baz", .capture(string)), .match("foo", .match("bar", .term(fooBar))) ])) optimizedRoute = .match("R", .choice([ .match("bar", .capture(double)), .match("baz", .capture(string)), .match("foo", .choice([ .capture(int), .match("bar", .term(fooBar)) ])) ]))

Slide 46

Slide 46 text

FunRouter https://github.com/inamiy/FunRouter

Slide 47

Slide 47 text

Summary • String-based routing is easy, but type-unsafe • Dive into Monadic Applicative parser • Optimization is possible with interpretable enum pattern • Caveat • Too easy to get error: "Expression was too complex to be solved in reasonable time." • Swift 4 will surely solve this problem!

Slide 48

Slide 48 text

Thanks!