- Proposal: SE-NNNN
- Author(s): Jeffrey Macko
- Status: Awaiting review
- Review manager: TBD
This is heavely inspired by Aspect Oriented Programming although I don't think this is Aspect Oriented Programming per se but it carries a lot of similarities.
The goal is to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior(code before/after/around a function) to existing code without modifying the code itself, instead separately specifying which code is modified via an injection. This allows behaviors that are not central to the business logic to be added to a program without cluttering the core code and still adding functionality.
Analytics exemplifies a cross-cutting concerns because an analytics strategy necessarily affects every part of the system. Analytics thereby crosscuts all analyse classes and methods.
This implementations have some cross-cutting expressions that encapsulate each concern in one place. One of the main advandage of this implementation is that simple, powerful, and safe because it work at compile time. This feature is build to allow everything to be check by the compiler and be statically dispatched.
This will ease maintenance of these cross-cutting concerns.
Other contributors already express the need for this kind of feature on the mailing list Function Decorator sadly this thread don't get enough traction. I propose a different approach to fix the same issue. More recently Swift request list ask for Aspect Oriented Programming too.
Separation of concerns is a big common problem in programming that Aspect Oriented Programming help to reduce.
Swift-evolution thread: Discussion thread topic for that proposal
In every project that I work on it always was complicated to refactor code that is not business code but cross-cutting concerns through out the app code. With this approch we can simply isolate cross-cutting concerns code from business logic code. And so increase maintanability of both.
Cross-cutting concerns are most of the time third party SDK that we implement for a specific need.
Having a kind-of macro called #decorator
which allow to inject code before/after or around a function. The code in before/after/decorate
should not be coupled between before
s or after
s. For exemple mulitple before
s in different place cannot share variables.
functionPattern:
May be a valid class or struct associated with a function
If the functionPattern
does not match/is not found a compiler error should be emmited
If the functionPattern
does math to a protocol a fix-it should propose to use the All version.
This feature does not support partial pattern like UIViewControler.viewdid*
.
priority:
It's a UInt8 between 0-255
The weakest priority is 0 and the highest is 255.
The compiler can/should be able to raise error when there is a conflict with the priority
s.
If a priority level is already used the IDE should show where it has been used.
closure:
It's the place where the code that wiil be injected will be.
The closuse inside has access to the self
of where it will be injected.
self
is always access in unowned
.
The closure has access to the parameter that the function matched have.
#decorator.before(functionPattern: <function-expression>, priority: Int, @autoclosure closure: ([unowned self], <function-expression-parameters>) -> ())
#decorator.after(functionPattern: <function-expression>, priority: Int, @autoclosure closure: ([unowned self], <function-expression-parameters>) -> ())
#decorator.decorate(functionPattern: <function-expression>, priority: Int, @autoclosure closure: (<function-expression-parameters>) -> (<function-expression-return-type>))
Example:
#decorator.before(TestUIApplicationDelegateClass.application(:didFinishLaunchingWithOptions:), priority: 0) { ([unowned self], application: UIApplication, launchOptions: [NSObject : AnyObject]?) in
print("Before: \(self.dynamicType)")
}
#decorator.after(TestUIApplicationDelegateClass.application(:didFinishLaunchingWithOptions:), priority: 0) { ([unowned self], application: UIApplication, launchOptions: [NSObject : AnyObject]?, returnValue: Bool) in
print("After: \(self.dynamicType)")
}
#decorator.decorate(TestUIApplicationDelegateClass.application(:didFinishLaunchingWithOptions:), priority: 0) { (application: UIApplication, launchOptions: [NSObject : AnyObject]?) in
print("Before")
let result = decoratedFonction(application, launchOptions)
print("Result After: \(result)")
}
before and after might be used in conjunction with decorate, because they are not inject the same way(go to "Detailed design" -> "Decorate").
functionPattern:
May be a valid protocol(with or with constraint) associated with a function
If the functionPattern
does not match/is not found a compiler error should be emmited
If the functionPattern
does math to a class/struct a fix-it should propose to use the not All version.
This feature does not support partial pattern like UIViewControler.viewdid*
.
closure: It's the place where the code that wiil be injected will be. The closure has access to the parameter that the function matched have.
The priority is not represented here because it does not make sens. So we have to arbitrarily decide of it positioning. I think these should always have the lowest priority.
#decorator.beforeAll(functionPattern: <function-expression>, @autoclosure closure: (<function-expression-parameters>) -> ())
#decorator.afterAll(functionPattern: <function-expression>, @autoclosure closure: (<function-expression-parameters>) -> ())
#decorator.decorateAll(functionPattern: <function-expression>, @autoclosure closure: (<function-expression-parameters>) -> (<function-expression-return-type>))
Example:
#decorator.beforeAll(UIApplicationDelegate.application(:didFinishLaunchingWithOptions:)) { (application: UIApplication, launchOptions: [NSObject : AnyObject]?) in
print("Before")
}
#decorator.afterAll(UIApplicationDelegate.application(:didFinishLaunchingWithOptions:)) { (application: UIApplication, launchOptions: [NSObject : AnyObject]?) in
print("After")
}
#decorator.decorateAll(UIApplicationDelegate.application(:didFinishLaunchingWithOptions:)) { (application: UIApplication, launchOptions: [NSObject : AnyObject]?) in
print("Before")
let result = decoratedProtocol(application, launchOptions)
print("Result After: \(result)")
}
Foo.swift
class foo {
let someInnerVariable = "I’m no Hero. Never was, Never Will Be."
static func importantWork() {
print("foo is doing important work...")
}
}
Stat.swift
#decorator.after(foo.importantWork(), priority: 0) { [unowned self] in
print("[stat] after foo \"\(self.someInnerVariable)\"")
}
Logger.swift
#decorator.before(foo.importantWork(), priority: 1) { [unowned self] in
print("[logger] before foo")
}
Performance.swift
#decorator.before(foo.importantWork(), priority: 0) { [unowned self] in
print("[performance] before foo")
}
#decorator.after(foo.importantWork(), priority: 255) { [unowned self] in
print("[performance] after foo")
}
Executing foo.importantWork()
prints
[performance] before foo
[logger] before foo
foo is doing important work...
[stat] after foo "I’m no Hero. Never was, Never Will Be."
[performance] after foo
I don't have the knowledge to argue of what is the best way to implement this feature.
In order for this to work we will need some kind of selector-expression
to work with swift in order to help identify a function in a class / struct / protocol. This should return information about where is this function in order to know where to inject the new behaviour. All of this phase should be done in indexing / pre-processing phase.
The compiler will do most of the job so we will never have to see the generated version of the code unless we go into the pre-process representation in Xcode.
My 2 cents naive approach would be to do something along those lines.
Order of evaluation:
- Before/After are the first to be evaluated
- BeforeAll/AfterAll
- Decorate
- DecorateAll
Foo.swift
class foo {
let someInnerVariable = "I’m no Hero. Never was, Never Will Be."
static func importantWork() {
print("foo is doing important work...")
}
}
Stat.swift
#decorator.after(foo.importantWork(), priority: 0) { [unowned self] in
print("[stat] after foo \"\(self.someInnerVariable)\"")
}
Logger.swift
#decorator.before(foo.importantWork(), priority: 1) { [unowned self] in
print("[logger] before foo")
}
Performance.swift
#decorator.before(foo.importantWork(), priority: 0) { [unowned self] in
print("[performance] before foo")
}
#decorator.after(foo.importantWork(), priority: 255) { [unowned self] in
print("[performance] after foo")
}
Code for foo
after #decorator injection
class foo {
let someInnerVariable = "I’m no Hero. Never was, Never Will Be."
static func importantWork() {
let v1 = { print("[performance] before foo") }; v1();
let v2 = { print("[logger] before foo") }; v2();
defer { print("[performance] after foo") }
defer { print("[stat] after foo \"\(self.someInnerVariable)\"") }
print("foo is doing important work...")
}
}
Foo.swift
class foo {
let someInnerVariable = "I’m no Hero. Never was, Never Will Be."
static func importantWork() {
print("foo is doing important work...")
}
}
Toppings.swift
#decorator.decorate(foo.importantWork()) {
print("let's do some stuff")
decoratedFonction()
print("let's do some other stuff")
}
Code for foo
after #decorator injection
class foo {
func importantWork_decorated() {
print("foo is doing important work...")
}
func importantWork() {
print("let's do some stuff")
importantWork_decorated()
print("let's do some other stuff")
}
}
The decorated function importantWork()
became importantWork_decorated()
and is now called within the new importantWork()
.
BeforeAll/AfterAll/DecorateAll works the same way as the non All versions but for protocol that match.
It will not work for getter et setter on protocol.
Protocole.swift
protocol SomeProtocol {
func usefulSlgorithmBar(state: Bool)
}
Foo.swift
class foo, SomeProtocol {
func usefulSlgorithmBar(state: Bool) {
print("Mr Babadook: \(state)")
}
}
Stat.swift
#decorator.before(SomeProtocol.usefulSlgorithmBar(state:)) { (state: Bool) in
sendStat(["SomeProtocol.usefulSlgorithmBar(state:)" : state])
}
#decorator.after(SomeProtocol.usefulSlgorithmBar(state:)) { (state: Bool) in
print("End of SomeProtocol.usefulSlgorithmBar(state:)")
}
Code for foo
after #decorator injection
class foo {
func usefulSlgorithmBar(state: Bool) {
let v1 = { sendStat(["SomeProtocol.usefulSlgorithmBar(state:)" : state]) }; v1();
defer { print("End of SomeProtocol.usefulSlgorithmBar(state:)") }
print("Mr Babadook: \(state)")
}
}
ToDo
This keyword can be used like @objc but this keyword prevent other to decorate your code.
If someone try to decorate importantWork()
he will receive a compiler error.
Bar.swift
class bar {
@notdecorable func importantWork() {
print("bar is working...")
}
}
This keyword can be used like @objc but this keyword is necessary to allow other to decorate your code.
Decoration only work on function with this decorator
Bar.swift
class bar {
@decorable func importantWork() {
print("bar is working...")
}
}
As a new API, this will have no impact on existing code.
- With the current design this only work on code that we have the source of so it does not work on library and frameworks.
- It's not dynamic.
- Does not support partial pattern
UIViewControler.viewdid*
.
Priority is better understand by non-english people.
Using #selector
was my first idea and it was a bad one because this does not do the same as the selector
and can be confusing on what it try to achieve.
The priority help to order the injection of those code blocks.
It's biggest advantege is it biggest weakness because with this it's harder to understand the flow of execution. Since all the injection is happening at compile time. Since it's a compile time feature (a bit like macro) it does not need to be declared within a function or a class it could increase a bit the compile time.
Aspect oriented programming is implemented in many languages https://en.wikipedia.org/wiki/Aspect-oriented_programming#Implementations.
- http://artsy.github.io/blog/2014/08/04/aspect-oriented-programming-and-aranalytics/
- http://petersteinberger.com/blog/2014/hacking-with-aspects/
Thanks to those peoples who have provided valuable input which helped shape this proposal: