-
Notifications
You must be signed in to change notification settings - Fork 1
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
Conversation
DeeperFunc/Deeper.swift
Outdated
|
||
public typealias RouteComponents = (path: [String], query: [String: String]) | ||
|
||
public protocol PatternState {} |
There was a problem hiding this comment.
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 afterany
is applied
DeeperFunc/Deeper.swift
Outdated
|
||
func map<S>(_ iso: PartialIso<A, Any>) -> RoutePattern<Any, S> { | ||
return .init(parse: { | ||
guard let result = self.parse($0) else { return nil } |
There was a problem hiding this comment.
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<") { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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
DeeperFunc/PartialIso.swift
Outdated
) | ||
} | ||
|
||
func unwraped<A, B>(_ iso: PartialIso<A, B?>) -> PartialIso<A, B> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unwrapped
DeeperFunc/PartialIso.swift
Outdated
infix operator >>> | ||
infix operator <<< | ||
|
||
public struct PartialIso<A, B> { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
DeeperFunc/Router.swift
Outdated
} | ||
|
||
@discardableResult | ||
public func add4<A, B, C, D>(_ intent: @escaping (A, B, C, D) -> U, format: String) -> Router { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
DeeperFunc/Path.swift
Outdated
public let double: RoutePattern<Double, Path> = pathParam(.double) | ||
|
||
// drop left param | ||
infix operator /> : MultiplicationPrecedence |
There was a problem hiding this comment.
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? { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) })) |
There was a problem hiding this comment.
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) })) |
There was a problem hiding this comment.
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:)) |
There was a problem hiding this comment.
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.
DeeperFunc/String.swift
Outdated
return .init(parse: parseRight(lhs, rhs), print: printRight(lhs, rhs), template: templateAnd(lhs, rhs)) | ||
} | ||
|
||
extension String { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 })) |
There was a problem hiding this comment.
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 ==
No description provided.