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 have 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, function types are the first-class citizens. Meaning that the signature of a function is a type with the same features as any other Swift type. Consequently, it is perfectly legal to store a function inside a property, as follows:
This fact bears a very interesting 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
:
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
:
The compiler is now going to ask us to implement a wrappedValue
:
Its getter is straightforward to implement: we just return our stored function. The actual caching logic is going to be implemented inside the setter:
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, the actual caching logic is pretty simple to follow: 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 not defined as aprivate var
of thestruct
. The explanation is simple: thecache
is being mutated inside an escaping closure, which is not allowed for properties of astruct
. By defining and scoping it inside a function, we avoid this issue.)
Finally, you can notice that we’ve also defined an init
. It performs the same job than the setter, and allows us to add our caching logic right when the wrapper is first instantiated.
Now that our wrapper has been implemented, we can try it out:
And if we run this code, we’ll indeed see that our caching mechanism is working as expected 🎉
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 💪
You liked this article and you want to see more content like it? Feel free to follow me on Twitter: https://twitter.com/v_pradeilles