Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional routers & parsers #5

Merged
merged 16 commits into from Nov 12, 2017
Merged

Functional routers & parsers #5

merged 16 commits into from Nov 12, 2017

Conversation

@ilyapuchka
Copy link
Owner

ilyapuchka commented Nov 2, 2017

No description provided.


public typealias RouteComponents = (path: [String], query: [String: String])

public protocol PatternState {}

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

Following phantom types represent pattern states:

  • pattern can have only path and no query, i.e. "users" /> :int
  • pattern can have path and query, i.e. "users" /> :int .? string("sort")
  • pattern can be "closed", meaning it does not expect any more pattern components (after any pattern used in the middle), i.e. any /> "users" or "users" /> any, or any simple pattern with just path or path and query
  • pattern can be "open", meaning it needs some subsequent pattern component to be closed, i.e. in "users" /> any /> "info" it's the state of the patter after any is applied

func map<S>(_ iso: PartialIso<A, Any>) -> RoutePattern<Any, S> {
return .init(parse: {
guard let result = self.parse($0) else { return nil }

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

not applying iso.apply here as then it will i.e. convert parsed int to string because it's the type of "type-erased" pattern wrapper used for parsing string format, and then router will not be able to cast in map. The only option is to use ExpressibleByStringLiteral or similar protocol to convert it back from string to type we need in router...

.replacingOccurrences(of: "Optional<", with: "")
.replacingOccurrences(of: ">", with: "")
.lowercased()
} else if typeString.contains("Either<") {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

actually this will never be used unless custom parameter type is used, but it's not currently exposed as public API

return .init(parse: parseRight(lhs, rhs), print: printRight(lhs, rhs), template: templateAnd(lhs, rhs))
}

public static func /?<B>(lhs: RoutePattern, rhs: RoutePattern<B, S>) -> RoutePattern<(A, B?), S> {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

maybe there should be /? and >/? so that it expresses that parameter from left part is carried on or not? but >/? feels like set of random symbols

)
}

func unwraped<A, B>(_ iso: PartialIso<A, B?>) -> PartialIso<A, B> {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

unwrapped

infix operator >>>
infix operator <<<

public struct PartialIso<A, B> {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

Does this really improve readability comparing with just passing apply/unapply function pairs and making some type casting in them? Especially with usage of >>> and <<< operators

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 4, 2017 Author Owner

Decided to throw this type away as just using pair of closures is easier to read and I don't need it as a standalone type

}

@discardableResult
public func add4<A, B, C, D>(_ intent: @escaping (A, B, C, D) -> U, format: String) -> Router {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

using add for all function variants results in ambiguity (why?), so had to add this 4 as a suffix

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 4, 2017 Author Owner

Solved by adding extra pair of brackets around method variant for single parameter

public struct RoutePattern<A/*pattern type*/, S: PatternState> {
public let parse: Parser<A> // parses path components exctracting underlying type of pattern
public let print: Printer<A> // converts pattern with passed in value to template component

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

print is actually not that useful in context of deep links as we don't need to produce urls in code, so probably can be dropped. Though it can be used for application wide navigation, so that openURL is used always instead of passing manually created intents around.

public let double: RoutePattern<Double, Path> = pathParam(.double)

// drop left param
infix operator /> : MultiplicationPrecedence

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 2, 2017 Author Owner

not fond of all these custom operators, but using / for all cases makes expressions too complex for swift compiler. In future can be probably solved with ExpressibleByStringLiteral conformance for Void type of generic parameter

case optionalParam(Int?)
case optionalSecondParam(Int, String?)

func deconstruct<A>(_ constructor: ((A) -> Intent)) -> A? {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

This method is used instead of PartialIso that would otherwise be created separately for each case, but will effectively do the same - match specific case with it's constructor and extract associated values.
It's boilerplate, but seems like less code to write comparing with PartialIso.

This code

if let values = value.deconstruct(Enum.somecase) {...}

is equivalent to this

if case let Enum.somecase(values) = value {...}

but can be mapped and used without if, when standard pattern matching can't be only used in if case/guard case statements.

}

private func add(_ route: RoutePattern<U, Path>) -> Router {
self.route = self.route.map({ oldValue in

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

self.route wraps all individual routes. When matching or printing using this route is equivalent to iterating through array of routes, but this will do it with recursion.


@discardableResult
public func add<A, S: ClosedPatternState>(_ intent: @escaping ((A)) -> U, route: RoutePattern<A, S>) -> Router {
return add(route.map({ intent($0) }, { $0.deconstruct(intent) }))

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

All routes are mapped to the same generic type, <U, Path>, so that they can be composed into one route.
Input parameter of second closure will be value of U, but router needs A, which is associated value of enum case (if enum is used of course). But we only have constructor of U, so we don't know with what case to match the value. This is done instead with deconstruct method that does pattern matching and returns associated values.
This is only needed to be able to print urls for specific values of U. We could print just using route itself (see this commit), but we can't easily get it back from router, so will need to store all routes in some constants. That might be a good solution, as it will not require boilerplate implementation of deconstruct, but it is not safe: route can be used to print without being added to router and then printed url will be not matched. With mapping and using deconstruct only routes that are added to router can be used to print urls, so it's guarantied that url will be matched.


@discardableResult
public func add<A, B, C, S: ClosedPatternState>(_ intent: @escaping ((A, B, C)) -> U, route: RoutePattern<((A, B), C), S>) -> Router {
return add(route.map({ intent(flatten($0)) }, { $0.deconstruct(intent).map(parenthesize) }))

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

Here we also need to regroup values using parenthesize from (A, B, C) to ((A, B), C) as this is what route expects

extension Router {

public func url(for route: U) -> URL? {
return self.route?.print(route).flatMap(url(from:))

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

now we can just use composite route to print url and we do it with concrete value of U, not a route pattern.

DeeperFunc/String.swift Outdated
return .init(parse: parseRight(lhs, rhs), print: printRight(lhs, rhs), template: templateAnd(lhs, rhs))
}

extension String {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

all these operators could be probably replaced with conditional conformance, then maybe / could be used without creating too complex expressions

return { components in
var components = components
var pathPatterns = [RoutePattern<Any, S>]()
while !components.isEmpty {

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

can we use the same approach as with routes, using composition and recursion instead of loop?


@discardableResult
public func add<S: ClosedPatternState>(_ intent: U, route: RoutePattern<Void, S>) -> Router {
return add(route.map({ intent }, { $0 == intent ? () : nil }))

This comment has been minimized.

@ilyapuchka

ilyapuchka Nov 5, 2017 Author Owner

for cases without parameters we can just use ==

@ilyapuchka ilyapuchka merged commit 19933ea into master Nov 12, 2017
@ilyapuchka ilyapuchka deleted the func branch Nov 12, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

1 participant
You can’t perform that action at this time.