Skip to content

Functional routers & parsers #5

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

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

No description provided.


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

public protocol PatternState {}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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<") {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {
Copy link
Owner Author

@ilyapuchka ilyapuchka Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwrapped

infix operator >>>
infix operator <<<

public struct PartialIso<A, B> {
Copy link
Owner Author

@ilyapuchka ilyapuchka Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Owner Author

@ilyapuchka ilyapuchka Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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? {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Owner Author

@ilyapuchka ilyapuchka Nov 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) }))
Copy link
Owner Author

@ilyapuchka ilyapuchka Nov 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) }))
Copy link
Owner Author

@ilyapuchka ilyapuchka Nov 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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

extension String {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for cases without parameters we can just use ==

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

Successfully merging this pull request may close these issues.

1 participant