Alexito's World

A world of coding ๐Ÿ’ป, by Alejandro Martinez

On the new Point-Free swift-dependencies library

The folks at Point-Free have given us yet again a new tool to improve our Swift development: swift-dependencies, see their announcement. Let me show the journey I've been for the past couple of hours with excitement and discoveries with this new library.

I really love the work Point-Free does and won't tire of recommending them. If you want to invest in your knowledge and support me at the same time use this referral link and subscribe to Pointfree, we both get a month free :)

Some context...

First, a little bit of context.

I've always been doing dependency injection via init or property with no framework. I find it the best way, no magic, everything is clear and works very well even in a 500k LOC project. Yes, you need to be smart on your architecture to avoid having to carry around dependencies, but it's not rocket science. That's how I've been doing DI for many years.

The first time I switched strategies was when the guys at Point-Free introduced the global dependency struct technique. I used it sparingly, just to access some system dependencies (calendar, locale...) and it worked really well I have to admit, more than it had the right to for being a global variable ๐Ÿ˜‚

But then the TCA happened and with it a huge improvement on application architecture. Their most recent update includes a fully fledged and integrated dependency system, akin to SwiftUI's environment. It works very well and does wonders to clean up and modularise your codebase. It's really amazing.

Where is the hype?

So with that said, I was well aware of how the system worked, and I knew they were intending to break it into a separate package. So when they announced the new library, I was not as hyped as with others. I was using the library (in the TCA) already!

But then I remembered a question that had been hunting me since I saw the dependency system... I know how it works, and it felt to me that it worked very well for the TCA specifically... but there was something that made it unworkable for a general architecture.

So the non-hype of the announcement became a curiosity to know what magic, if any, they pulled off. Or to confirm that I didn't understand things as well as I thought, something that I'm always trying to do, makes me stay true.

How does this work?

To understand my doubts about this system working in a general sense you have to understand how it works. Internally it relies on Swift's Task Locals, a new feature of Swift that came with Structured Concurrency.

Task Locals allow you to set variables in a "context" that is automatically available to your code. But the context modifications are only visible on the scope where they changed. This plays nicely with structured concurrency and normal structured programming. As long as you keep calling functions, all the stack has access to the same context.

func someFunction() {
  Lib.$taskLocalValue  // 1 - Outisde the scope
  Lib.$taskLocalValue.withValue(42) {
    Lib.$taskLocalValue  // 42 - Inside the overwriten scope
    calledFromInside()
  }
  Lib.$taskLocalValue  // 1 - Outisde the scope
  calledFromOutside()
}

func calledFromInside() {
  Lib.$taskLocalValue  // 42 - Inside the overwriten scope
}

func calledFromOutside() {
  Lib.$taskLocalValue  // 1 - Outisde the scope
}

func someOtherUnrelatedFunction() {
  Lib.$taskLocalValue  // 1 - Outisde the scope
}

You can see how this would work very well since the TCA runs everything in reducers that are chained, so they are all in the same scope. That allows the Store to set the dependencies once and run the entire chain.

Action -> Store
Store sets dependencies and calls reduce
Reduce sees the dependencies, next reducer is run in the same scope...

Easy. But when I thought how this could be generalised, I immediately saw problems since in a non-TCA application the scopes of executions are all over the place, there is not a central object that controls the scope.

UI Button calls ViewModel.performAction
Another button calls ViewModel.doSomethingElse

There is nowhere you can overwrite the Task Local context for all the scopes. You need to do it on an ad hoc basis.

The trick

So then I started reading the new library's documentation. And they quickly make the point that you need to instantiate the "next controller" inside the library's closure.

self.onboardingTodos = withDependencies(from: self) {
  $0.apiClient = ...
} operation: {
  TodosModel()
}

And an important note that explains "It takes an extra argument, from, which is the object from which we propagate the dependencies before overriding some. This allows you to propagate dependencies from object to object."

Aha! So it all works as long as you instantiate the object inside the scope. This already tells us a bit about the trick that is being employed.

The above is all well explained in the Dependency lifetimes section. They even explain how Task Locals work ;)

Here is the part that opened my eyes:

Accomplishing this can be difficult because models are created in one scope and then dependencies are used in another scope. However, as mentioned above, the library does extra work to make it so that later referencing dependencies of a model uses the dependencies captured at the moment of creating the model.

There you go, the confirmation. As long as you instantiate the model in the scope that overwrites the task local, your model will see that overwrite even outside of the scope. You can see the code for this, seems like the trick is to keep your own storage so you can overwrite and keep your own "hierarchy" instead of relying simply in the one provided by Task Locals.

They basically made Task Locals "escape", impressive!

Single entry point systems

With the understanding of how it worked, I was satisfied. But, in a typical Point-Free fashion, they went the extra mile and actually wrote an article about the specific doubts I had at the beginning of this post. They even gave me the propar vocabulary needed to form the correct mental model.

Single entry point systems explains why systems like TCA or SwiftUI are ideal for managing a hierarchy of dependencies since it all funnels through a single entry point; and they compare it Non-single entry point systems like ViewModels or UIKit where is more complicated.

Ultimately, I think they have accomplished the right levels of ergonomics for a feature like this. If you use a non-single entry point architecture, you get the same benefits on your dependencies overwrites as long as you remember to instantiate the model inside the proper scope. It still leaves the TCA ahead of the game, but is very nice for people that don't want to jump into the future just yet ๐Ÿ˜œ

And some gifts!

As always, Point-Free provides not only a useful library, but a very well done one. Their documentation is gorgeous, full of details about the library itself but also advice on how to design dependencies and architect your app. They even released some extra goodies with the library!

Some notable details I found interesting even after being using it for a few weeks:

  • Cascading rules gives a nice summary of which "value" will be used in each context if not all of them are provided. Useful to reference until you get used to it.
  • I was not expecting to read about Concurrency support! They have split a few very useful types that were in the TCA so we can now use them on any project, nice!
    • ActorIsolated is a very useful generic wrapper to isolate a value in an actor. Great for tests but I've found myself using it in some specific cases where yo don't want an ad hoc actor just to isolate a value.
    • I haven't used LockIsolated, and it seems a bit dangerous but useful if you need to do blocking async code.
    • UncheckedSendable is one that helps a lot during the transition period for code that hasn't adopted Sendable conformances yet. Another dangerous to use but useful nonetheless. (I wish I had this one a few months back ๐Ÿ˜‚ )
    • There are even helpers for AsyncStreams! It feels a bit weird to have these in this library, but in a way it makes sense because you will need them to design your dependencies. I think this will make swift-dependencies a must include on every project of mine.
      • "This allows you to treat the stream type as a kind of โ€œtype erasedโ€ AsyncSequence." this one feels a bit weird. I'm not going to lie. One would hope that with improvements in the generic system, this wouldn't be necessary but oh well ยฏ_(ใƒ„)_/ยฏ
      • Now, the API for simultaneously constructing a stream and its backing continuation is very useful and something that should come in the standard library.
      • There are a few other inclusions so make sure to check it out!

Consider me learned

I love these quick derails my brain takes me into. I was not planning to get deeper into Task Locals at all this week, after all, I thought I already knew pretty much all about them during my Concurrency obsessive days; but then curiosity sparked, and feeding it was one of the best joys in life.

I can't thank enough Point-Free for not only pushing the limits of the Swift community - you know they even got a proposal accepted recently to fix a missing API in Clocks? - but also for making learning so fun!

Thanks for accompanying in this post and journey.

If you liked this article please consider supporting me