A Swift library for implicit parameter passing through call stacks, similar to implicit parameters in Scala or context receivers in Kotlin.
- Swift 6.2+
- macOS 13+
Add Implicits to your Package.swift:
dependencies: [
.package(url: "https://github.com/yandex/implicits", from: "1.0.0"),
]Then add the library to your target:
.target(
name: "MyApp",
dependencies: ["Implicits"],
plugins: ["ImplicitsAnalysisPlugin"] // Compile-time validation
)The ImplicitsAnalysisPlugin performs static analysis at build time to verify that all implicit parameters are properly provided through the call chain.
Consider a simple shopping simulation where we need to pass payment details through multiple function layers:
func goShopping(money: Money, discountCard: DiscountCard) {
goToGroceryStore(money: money, discountCard: discountCard)
goToClothesStore(money: money, discountCard: discountCard)
}
func goToGroceryStore(money: Money, discountCard: DiscountCard) {
pay(money: money, discountCard: discountCard)
}
func goToClothesStore(money: Money, discountCard: DiscountCard) {
pay(money: money, discountCard: discountCard)
}
func pay(money: Money, discountCard: DiscountCard) {
money.amount -= 100 * (1 - discountCard.discount)
}
// Usage
goShopping(
money: Money(amount: 1000),
discountCard: DiscountCard(discount: 0.05)
)This pattern, known as parameter drilling, requires passing the same arguments through every layer of the call stack, even when intermediate functions don't use them. This creates unnecessary coupling and makes refactoring more difficult.
With Implicits, you declare values once and access them anywhere in the call stack without explicit parameter passing:
func goShopping(_ scope: ImplicitScope) {
goToGroceryStore(scope)
goToClothesStore(scope)
}
func goToGroceryStore(_ scope: ImplicitScope) { pay(scope) }
func goToClothesStore(_ scope: ImplicitScope) { pay(scope) }
func pay(_: ImplicitScope) {
// Access money and discountCard implicitly — no parameters needed!
@Implicit var money: Money
@Implicit var discountCard: DiscountCard
money.amount -= 100 * (1 - discountCard.discount)
}
// Usage
let scope = ImplicitScope()
defer { scope.end() }
@Implicit var money = Money(amount: 1000)
@Implicit var discountCard = DiscountCard(discount: 0.05)
goShopping(scope)Note: Due to Swift's current limitations, a lightweight
ImplicitScopeobject must be passed through the call stack. However, the actual data (money,discountCard) doesn't need to be passed — it's accessed implicitly via@Implicit.
Implicit arguments behave like local variables that are accessible throughout the call stack. They follow standard Swift scoping rules and lifetime management.
Just like regular Swift variables have their lifetime controlled by lexical scope:
do {
let a = 1
do {
let a = "foo" // shadows outer 'a'
let b = 2
}
// 'a' is back to being an integer
// 'b' is out of scope
}Implicit variables follow the same pattern, but their scope is managed by ImplicitScope objects. Always use defer to guarantee proper cleanup:
func appDidFinishLaunching() {
let scope = ImplicitScope()
defer { scope.end() }
// Declare dependencies as implicit
@Implicit
var network = NetworkService()
@Implicit
var database = DatabaseService()
// Components can now access these dependencies
@Implicit
let omnibox = OmniboxComponent(scope)
@Implicit
let webContents = WebContentsComponent(scope)
@Implicit
let tabs = TabsComponent(scope)
let browser = Browser(scope)
browser.start()
}In this example, we establish a dependency injection container where services are available to all components without explicit passing.
Sometimes you need to add local implicit arguments without polluting the parent scope:
class OmniboxComponent {
// Access implicit from parent scope
@Implicit()
var databaseService: DatabaseService
init(_ scope: ImplicitScope) {
// Create a nested scope for local implicits
let scope = scope.nested()
defer { scope.end() }
// This implicit is only available in this scope
@Implicit
var imageService = ImageService(scope)
self.thumbnailsService = ThumbnailsService(scope)
}
}Key points:
- Use
nested()when adding new implicit arguments - Parent scope implicits remain accessible
- Nested implicits don't leak to parent scope
Closures require special handling to capture implicit context:
class WebContentsComponent {
init(_ scope: ImplicitScope) {
// Using the #implicits macro (recommended)
self.webContentFactory = {
[implicits = #implicits] in
let scope = ImplicitScope(with: implicits)
defer { scope.end() }
return WebContent(scope)
}
}
}The #implicits macro captures the necessary implicit arguments. The analyzer detects which implicits are needed and generates the appropriate capture list.
When creating factory methods that need access to implicit dependencies:
class TabsComponent {
// Store implicit context at instance level
let implicits = #implicits
@Implicit()
var networkService: NetworkService
@Implicit()
var omniboxComponent: OmniboxComponent
init(_ scope: ImplicitScope) {}
func makeTab() -> Tab {
// Create new scope with stored context
let scope = ImplicitScope(with: implicits)
defer { scope.end() }
return Tab(scope)
}
}This pattern allows factory methods to access dependencies available during initialization.
By default, Implicits uses the type itself as the key. But what if you need multiple values of the same type?
extension ImplicitsKeys {
// Define a unique key for a specific Bool variable
static let incognitoModeEnabled =
Key<ObservableVariable<Bool>>()
}
class TabsComponent {
let implicits = #implicits
init(_ scope: ImplicitScope) {}
func makeTabsUI() -> TabsUI {
let scope = ImplicitScope(with: implicits)
defer { scope.end() }
// Type-based key (default)
@Implicit()
var db: DatabaseService
// Named key for specific semantic meaning
@Implicit(\.incognitoModeEnabled)
var incognitoModeEnabled = db.incognitoMode.enabled
return TabsUI(scope)
}
}Choose your key strategy based on semantics:
// Type key: Only one instance makes sense
@Implicit()
var networkService: NetworkService
// Type key: Singleton service
@Implicit()
var tabManager: TabManager
// Named key provides clarity when type would be ambiguous
@Implicit(\.incognitoModeEnabled)
var incognitoModeEnabled: ObservableVariable<Bool>
@Implicit(\.darkModeEnabled)
var darkModeEnabled: ObservableVariable<Bool>Need to derive one implicit from another? Use the map function:
class Browser {
@Implicit()
var databaseService: DatabaseService
init(_ scope: ImplicitScope) {
let scope = scope.nested()
defer { scope.end() }
// Transform DatabaseService → IncognitoStorage
Implicit.map(DatabaseService.self, to: \.incognitoStorage) {
IncognitoStorage($0)
}
// Now IncognitoStorage is available as an implicit
self.incognitoBrowser = IncognitoBrowser(scope)
}
}This is equivalent to manually creating the derived implicit.
The analyzer tracks implicit dependencies at compile time, generating interface files that propagate through your module dependency graph. This provides type safety and IDE integration.
Since the analyzer works at the syntax level, there are some constraints to be aware of:
1. No Dynamic Dispatch
- Protocols, closures, and overridable methods can't propagate implicits
- Use concrete types and final classes where possible
2. Unique Function Names Required
- Can't have multiple functions with the same name using implicits
- The analyzer can't resolve overloads
3. Explicit Type Annotations
- Type inference is limited for type-based keys
- Named keys include type information
// Type can't be inferred
@Implicit
var networkService = services.network
// Explicit type annotation
@Implicit
var networkService: NetworkService = services.network
// Type inference works with initializers
@Implicit
var networkService = NetworkService()
// Named keys don't need type annotation
@Implicit(\.networkService)
var networkService = services.networkIn DEBUG builds, Implicits provides powerful debugging tools to inspect your implicit context at runtime.
At any breakpoint, add this expression to Xcode's variables view:
ImplicitScope.dumpCurrent()💡 Tip: Enable "Show in all stack frames" for complete visibility
List all available keys:
p ImplicitScope.dumpCurrent().keysExample output:
([String]) 4 values {
[0] = "(extension in MyApp):Implicits.ImplicitsKeys._DarkModeEnabledTag"
[1] = "(extension in MyApp):Implicits.ImplicitsKeys._AnalyticsEnabledTag"
[2] = "MyApp.NetworkService"
[3] = "MyApp.DatabaseService"
}
Search for specific implicits (case-insensitive):
p ImplicitScope.dumpCurrent()[like: "network"]Example output:
([Implicits.ImplicitScope.DebugCollection.Element]) 1 value {
[0] = {
key = "MyApp.NetworkService"
value = <NetworkService instance>
}
}
See CONTRIBUTING.md for contribution guidelines.
Apache 2.0. See LICENSE for details.