Similar discussions existed before, but they were made either long before any functionality this idea is based on was introduced to Swift and so they died out. So here goes nothing - let's talk about Python decorators again =)
Decorators in Python are implemented as functions wrapping other functions + some syntactic sugar that looks similar to Swift property wrappers and other attributes. For example in Flask one can implement a following decorator:
from functools import wraps
from flask import g, request, redirect, url_for
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
return redirect(url_for('login', next=request.url))
return f(*args, **kwargs)
return decorated_function
https://flask.palletsprojects.com/en/1.1.x/patterns/viewdecorators/#login-required-decorator
This decorator allows to easily add additional behaviour to decorated functions, i.e. in this case a requirement to be logged in or to be redirected to login page when accessing a specific endpoint of server-side app:
@app.route('/secret_page')
@login_required
def secret_page():
pass
In fact there is also another decorator here, @app.route
, which allows to bind function to the endpoint, which might be as well very desirable for Swift on Server.
Thinking about how this can be implemented in Swift nowadays I came up with a following idea that is based on using dynamicCallable in conjunction with something similar to property wrappers in some way.
Considering above examples we can imagine it to be implemented in Swift like this:
@Route("/secret_page")
@LoginRequired
func secretPage() -> HTML {
HTML(...)
}
Now to implement this LoginRequired
decorator we could write something similar to property wrapper which would at the same time implement @dynamicCallable
:
struct LoginRequired {
let wrappedFunc: (KeyValuePairs<String, Any>) -> HTML
init(wrappedFunc: @escaping (KeyValuePairs<String, Any>) -> HTML) {
self.wrappedFunc = wrappedFunc
}
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Any>) -> HTML {
guard Current.user != nil else {
return Current.app.redirect(to: "login", next: Current.request.url)
}
return wrappedFunc(args)
}
}
(Current
here is just some global context, like global g
, request
in Flask)
This looks very similar to Python code for decorator with KeyValuePairs<String, Any>
instead of *kwarg
. This makes decorator agnostic of wrapped function parameters, their types and labels, as on practice each wrapped function will have different parameters. (As with dynamic callable KeyValueParis is not required, any array literal convertible type for parameters should work)
Python also allows to define decorators as classes which makes the code even more alike (with dynamicallyCall
instead of __call__
).
Now for this to really work compiler would need to rewrite original function declaration to something like this:
private var _secretPage: LoginRequired {
// decorated function moved inside decorator property getter
// as it is a computed property it will even have access to other members of enclosing type
func secretPage() -> HTML {
HTML(...)
}
// decorated function is passed to the wrapper via closure
return LoginRequired(wrappedValue: { params in secretPage() })
}
func secretPage() -> String {
_secretPage()
}
This looks similar to how property wrappers transform code, but a little bit more involved (still doable I think) =)
If decorated function has arguments, named or not named, this is already handled by using KeyValuePairs
in dynamicallyCall so only needs some additional code to be generated.
For example if we want to decorate function like this with a URL route that accepts a parameter:
@Route("/user/:username")
@LoginRequired
func profile(username: String) -> User? {
users.first { $0.name == username }
}
will be translated to:
private var _profile: LoginRequired {
func profile(username: String) -> User? {
users.first { $0.name == username }
}
return LoginRequired(wrappedValue: { params in profile(username: params[0] as! String) })
}
func profile(username: String) -> User? {
_profile(username: username)
}
Similar to property wrappers it should be possible to pass parameters to function wrappers, but unlike property wrappers function wrappers always have wrapped value, the wrapped function itself, so the convention would be to allow any initialiser parameter and require wrapped value to be the last:
struct Route {
let path: String
let wrappedFunc: (KeyValuePairs<String, Any>) -> HTML
init(_ path: String, wrappedFunc: @escaping (KeyValuePairs<String, Any>) -> HTML) {
self.wrappedFunc = wrappedFunc
}
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Any>) -> HTML {
guard Current.request.url.path == path else { return Current.request.next() }
return wrappedFunc(args)
}
}
Then the code with such wrapper
@Route("/user/:username")
func profile(username: String) -> User? {
users.first { $0.name == username }
}
would be transformed to following:
private var _profile: Route {
func profile(username: String) -> User? {
users.first { $0.name == username }
}
// using trailing closure would make code transformation even simpler
return Route("/user/:username") { params in profile(username: params[0] as! String) }
}
func profile(username: String) -> User? {
_profile(username: username)
}
Some concerns/questions:
-
return type of wrapped functions is fixed in a wrapped value signature
A: wrapper can be generic and can provide dynamicallyCall implementations in constrained extensions (this is already supported by dynamicCallable) -
type casts and subscripts look nasty
A: this code is generated by compiler so should be safe... -
does such decorator need to have projected value? underscore value to access wrapper itself? access to original function?
A: not sure about value of projected value here but to be symmetric with property wrappers (maybe even to build on top of it as well) it might make sense to provide the same functionality. Access to original function (not something that is available for property wrappers out of the box, but doable on demand) might be also useful in some edge cases -
static callable proposal mentions unifying types as the end goal, can that affect this feature and design and then maybe it's better to wait until this work will be done?
A: don't know really, wrappers don't have to be structs here so if functions will be able to hold members in future then wrapper would stay a function, then there will be no need for dynamicCallable as a sugar, but still ther will be need for some convention I suppose, and for some code generation as well -
does it have to be dynamicCallable?
A: This way feature will be based on top of existing mechanics. dynamicCallable is a syntactic sugar itself, so it can be desugared in generated code automatically, but wrapper would still need to implement dynamicallyCall methods per convention. But if it's better to come up with a different mechanics it's acceptable. Or maybe I'm not seeing something simpler that does not need any "magic"? -
Why not just to wrap function inside it's body?
A: Surely one can wrap the body of the function in described decorators using higher order function, but this can result in increased nesting, destructs from the actual purpose of the function and complicate editing. In the end, property wrappers were also possible to implement almost in the same way as they work now even before they were introduced as a syntactic sugar =)
That's basically it. I'm not sure about this idea and such design so I'm open to any feedback. In my opinion it worths - Python decorators was one of the nicest things I learned about Python and I heard similar opinions from others - though to be faire I didn't work a lot with Python and when I did I only used decorators in the described context (would love to do that same with Swift on Server!)
Also implementation details seem to align with what was done in property wrappers proposal. So based on existing precedent of property wrappers why not to extend it to functions as well?
UPD:
- Wrappers composition
A: This was eventually solved for property wrappers, though I didn't follow the details. I imagine that compiler can desugar wrappers one by one nesting generated code, eventually transforming code with two wrappers from above examples to something like this:
private var _profile: Route {
private var _profile: LoginRequired {
func profile(username: String) -> User? {
users.first { $0.name == username }
}
return LoginRequired(wrappedValue: { params in profile(username: params[0] as! String) })
}
func profile(username: String) -> User? {
_profile(username: username)
}
// using trailing closure would make code transformation even simpler
return Route("/user/:username") { params in profile(username: params[0] as! String) }
}
func profile(username: String) -> User? {
_profile(username: username)
}