Skip to content

Instantly share code, notes, and snippets.

@vincent-pradeilles
Created December 18, 2019 14:05
Show Gist options
  • Save vincent-pradeilles/55f5339138dfc9fe07d67b438c1e68a0 to your computer and use it in GitHub Desktop.
Save vincent-pradeilles/55f5339138dfc9fe07d67b438c1e68a0 to your computer and use it in GitHub Desktop.

Turning Property Wrappers into Function Wrappers 🤯

If you're keeping up with the new features of Swift, you've probably heard of Property Wrappers. They were introduced a few months ago as part of Swift 5.1 and we've covered their basics in a previous article.

Property Wrappers & Functions

Today, we're going to take things a bit further by exploring an unexpected use case of Property Wrappers!

Now, the name Property Wrapper can be slightly deceiving, because when we read Property, we intuitively think of data.

And this makes sense, because when we write code in Swift our primary use case for properties is to store data. However, we shouldn't forget that, in Swift, functions types are first-class citizen. Meaning that the signature of a function is a type with the same features than any other Swift type. Consequently, it is perfectly legal to store a function inside a property, as follows:

undefined

This fact bears a very interiesting consequence: since functions can be stored inside properties, it means that Property Wrappers could also be made to work with functions 🎉

So let us take a look at the kind of construct this approach could enable us to build.

As an example, we'll be implementing something that is rather straightforward to understand: a caching mechanism.

To begin, we are going to define a struct called Cached:

undefined

For the moment, this struct does nothing more than storing a function. To turn it into a Property Wrapper, we'll decorate it with the attribute @propertyWrapper:

undefined

The compiler is now going to ask us to implement a wrappedValue:

undefined

Its getter is straightforward to implement: we just return our stored function. The actual caching logic is going to be implemented inside the setter:

undefined

So what's happening inside this setter? First we declare the cache that will store the results of our computations. To keep things simple, we'll use a Dictionary, with a Key and Value that respectively match the Input and Output of the function we want to cache.

Then, we'll assign a value to our cachedFunction. The logic here is simple: when the cachedFunction is called, we take its argument and look it up in our cache. If we find a match, we return the value we had previously stored. Otherwise, we perform the computation, store the result in the cache, and return it.

(You might be wondering why the variable cache is defined inside the setter and not as a private var of the struct. The explanation is simple: the cache is being mutated inside an escaping closure, which is not allowed for properties of a struct. By defining it inside the setter, we avoid this issue.)

Now that our wrapper has been implemented, we can try it out:

undefined

And if we run this code, we'll indeed see that our caching mechanism is working as expected 🎉

undefined

We are now able to define custom wrappers that will decorate pieces of our code and bring them additional behavior in a very seamless way! To some extent, we could say that we've built our first Function Wrapper 🚀

To Conclude

In this article, we took the example of implementing a caching mechanism, but we could cover many more use cases: for instance, we could implement wrappers like @Delayed(delay: 0.3) and @Debounced(delay: 0.3), to deal with the timing of code execution. Or we could provide thread safety via a wrapper @ThreadSafe, that would wrap the execution of a piece of code around a Lock.

The possibilities are basically endless, and now it's up to you to think of and implement the Function Wrappers that will make sense inside your own codebase 💪

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