Slide 1

Slide 1 text

Type-safe URL Routing in Swift 2016/11/20 iOS AllStars 2 #eventdots Yasuhiro Inami / @inamiy

Slide 2

Slide 2 text

https://eventdots.jp/ iOS/AllStars/2

Slide 3

Slide 3 text

/iOS/AllStars/2

Slide 4

Slide 4 text

/iOS/{title}/{version}

Slide 5

Slide 5 text

URL routings everywhere! • ΫϥΠΞϯταΠυ • Universal Links (>= iOS 9) • Custom URL Schemes (<= iOS 8) • αʔόʔαΠυ • Perfect, Kitura, Zewo, Vapor, etc • Node.js Express෩

Slide 6

Slide 6 text

ΫϥΠΞϯταΠυ(ྫ) enum Sitemap { case event(title: String, version: Int) // "/iOS/{title}/{version}" case notFound } struct Router { func match(path: String) -> Sitemap { if let captures: [String] = myRegex("/iOS/(.+)/(\\d+)", path), captures.count == 2, let version = Int(captures[1]) { return .event(captures[0], version) } return .notFound } }

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

αʔόʔαΠυ(ྫ) // Perfect routes.add(method: .get, uri: "/iOS/{title}/{version}", handler: requestHandler) // Zewo route.get("/iOS/:title/:version") { request in guard let title: String = request.pathParameters["title"], let version: String = request.pathParameters["version"] else { ... } /* Warning: All pathComponents are captured in String! */ ... } // Vapor drop.get("iOS", String.self, Int.self) { (request, title: String, version: Int) in ... // ← ܕʹ஫໨ }

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 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 11

Slide 11 text

ϝλϓϩάϥϛϯά !

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

!

Slide 14

Slide 14 text

Functional Programming

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Parser Combinator 1จࣈ୯ҐͰύʔε

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

1จࣈ୯ҐͰύʔε จࣈྻ୯ҐͰύʔε (จࣈྻ = pathComponent)

Slide 19

Slide 19 text

Route ܕ /// ঢ়ଶมԽؔ਺ͷίϯςφ public struct Route { let parse: ([String]) -> ([String], Value)? } • Ҿ਺: pathComponents as [String] • ฦΓ஋: "݁Ռ" ͱ "࢒Γͷೖྗ" ͷ Optional tuple

Slide 20

Slide 20 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 21

Slide 21 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 22

Slide 22 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 23

Slide 23 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 24

Slide 24 text

// MARK: Monad // [ίϝϯτΞ΢τ] // Ϟφυ͸ΦʔόʔεϖοΫͳͷͰࠓճඞཁ͋Γ·ͤΜɻ // ͳͥͳΒɺURLϧʔςΟϯάͰpathComponentΛ1ͭͣͭύʔε͢Δࡍʹɺ // ݸʑͷॲཧ͸લޙؔ܎ʹґଘ͠ͳ͍͔ΒͰ͢ʢจ຺ࣗ༝จ๏ʣ // // ݴ͍׵͑Δͱɺ৽͍͠ Route ΛϥϯλΠϜதʹੜ੒͢Δඞཁ͕ͳ͘ɺ // ίϯύΠϧ࣌ʹఆٛͨ͠ Route ʹ͍ͭͯ // ΞϓϦΧςΟϒελΠϧͰஞ࣍ద༻͢Ε͹े෼͔ͭߴ଎Ͱ͢ɻ

Slide 25

Slide 25 text

Monadic Applicative URL Parser

Slide 26

Slide 26 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 27

Slide 27 text

Ϩοπߏจղੳʂ

Slide 28

Slide 28 text

/// "/iOS/{title}/{version}" Λύʔε let route: Route<(String, Int)> = curry { ($1, $2) } <^> match("iOS") <*> string() <*> int() // ΑΓΦγϟϨͳه๏: // let route = // /"iOS" > string() > int() <&!> flipCurry { ($1, $2) } let value1 = route.run(["iOS", "AllStars", "2"]) expect(value1?.0) == "AllStars" expect(value1?.1) == 2 let value2 = route.run(["P", "P", "A", "P"]) expect(value2).to(beNil())

Slide 29

Slide 29 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 30

Slide 30 text

// (ଓ͖) 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 31

Slide 31 text

No content

Slide 32

Slide 32 text

ຊ౰ʹʁ

Slide 33

Slide 33 text

// ໰୊ͷ͋Δ 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. "foo" + int ΛνΣοΫ 2. "bar" + double ΛνΣοΫ 3. "baz" + string ΛνΣοΫ 4. "foo" + "bar" ΛνΣοΫ ... "foo" Λ2ճνΣοΫ!? !

Slide 34

Slide 34 text

ސ٬͕ຊ౰ʹඞཁͩͬͨ΋ͷ // ࠷దԽ͞Εͨ choice choice([ match("foo") *> choice([ int() <&> Sitemap.foo, match("bar") *> pure(Sitemap.fooBar) ]), match("bar") *> double() <&> Sitemap.bar, match("baz") *> string() <&> Sitemap.baz ])

Slide 35

Slide 35 text

enumΛ࢖ͬͯ ߏ଄Λղऍ͢Δ

Slide 36

Slide 36 text

৽͍͠ Route ܕ (Enumϕʔε) /// Enumϕʔεͷ 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 ͸ "Ϛονจࣈྻ" ͱ "ޙଓͷ Route" ΛؚΉ • .capture ͸ "ύʔεॲཧ" ͱ "ޙଓ Route ม׵" ͷ2ͭͷؔ਺

Slide 37

Slide 37 text

extension Route { // ৽͘͠ run Λ࣮૷ʢؔ਺ϕʔεͰ͸ͳ͘ͳͬͨͨΊʣ 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 } } } // ͞Βʹɺલճͱಉ༷ɺFunctor, Applicative, Alternative ΋࣮૷͢Δ...

Slide 38

Slide 38 text

࠷దԽॲཧ

Slide 39

Slide 39 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 40

Slide 40 text

private func _optimizeChoice( _ routes: [Route] ) -> Route { if routes.isEmpty { return .zero } // ιʔτ ! ಉ͡ઌ಄ͷ Route ͰάϧʔϓԽ ! ϑϥοτԽ let sortedRoutes = routes.sorted(by: isAscending) let arrangedRoutes = groupBy(isSameRouteHead)(ArraySlice(sortedRoutes)) .map(flatten) return .choice(arrangedRoutes) }

Slide 41

Slide 41 text

Ϩοπ࠷దԽʂ

Slide 42

Slide 42 text

// ࠷దԽલ (`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) ]) // ࠷దԽޙ let optimizedRoute = optimize(naiveRoute)

Slide 43

Slide 43 text

݁Ռ: 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 44

Slide 44 text

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

Slide 45

Slide 45 text

·ͱΊ • จࣈྻϕʔεͷURLॲཧ͸؆୯͚ͩͲɺܕ҆શͰ͸ͳ͍ • Monadic Applicative Parser Λ࢝ΊΑ͏ • EnumΛ࢖͏ͱ࠷దԽ΋Ͱ͖ΔΑ • ⾠஫ҙ "Expression was too complex to be solved in reasonable time." Τϥʔ͕ग़ͯ΋ٽ͔ͳ͍ • Swift 4 Ͱվળ͞ΕΔ͜ͱΛظ଴͠·͠ΐ͏ !

Slide 46

Slide 46 text

Thanks!