Skip to content

Instantly share code, notes, and snippets.

@teriiehina
Created June 6, 2016 01:26
Show Gist options
  • Save teriiehina/53def0d4af8e81f3af6629cc3ddde371 to your computer and use it in GitHub Desktop.
Save teriiehina/53def0d4af8e81f3af6629cc3ddde371 to your computer and use it in GitHub Desktop.
This is my first Swift Evolution proposition draft

Static Function Decorator

Introduction

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

Motivation

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.

Proposed solution

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 befores or afters. For exemple mulitple befores in different place cannot share variables.

before/after/decorate

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 prioritys. 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").

beforeAll/afterAll/decorateAll

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)")
}

General Examples

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

Detailed design

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

Before/After

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...")
	}
}

Decorate

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

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)")
	}
}

Grammar

ToDo

I'm not sure it should work or not with theses attributes

@notdecorable

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...")
	}
}

@decorable

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...")
	}
}

Impact on existing code

As a new API, this will have no impact on existing code.

Limitation

  • 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*.

Alternatives considered

Using precedence name instead of priority

Priority is better understand by non-english people.

Using #selector name instead of #decorator

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.

Not having a priority

The priority help to order the injection of those code blocks.

Criticism

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.

Other Languages Does It

Aspect oriented programming is implemented in many languages https://en.wikipedia.org/wiki/Aspect-oriented_programming#Implementations.

Objective-C use of AOP

Acknowledgements

Thanks to those peoples who have provided valuable input which helped shape this proposal:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment