Skip to content

Instantly share code, notes, and snippets.

@ole
Last active May 17, 2023 19:23
Show Gist options
  • Save ole/0473d8e063762ebfb1a403d069fdddaf to your computer and use it in GitHub Desktop.
Save ole/0473d8e063762ebfb1a403d069fdddaf to your computer and use it in GitHub Desktop.
Code for my article "A heterogeneous dictionary with strong types in Swift" https://oleb.net/2022/heterogeneous-dictionary/
// A heterogeneous dictionary with strong types in Swift, https://oleb.net/2022/heterogeneous-dictionary/
// Ole Begemann, April 2022
/// A key in a `HeterogeneousDictionary`.
public protocol HeterogeneousDictionaryKey {
/// The "namespace" the key belongs to. Every `HeterogeneousDictionary` has its associated domain,
/// and only keys belonging to that domain can be stored in the dictionary.
associatedtype Domain
/// The type of the values that can be stored under this key in the dictionary.
associatedtype Value
}
/// A dictionary that can store values of varying types while preserving strong types
/// (i.e. without resorting to `Any`).
///
/// Similar in concept to the environment in SwiftUI.
///
/// The dictionary’s keys are types that conform to `HeterogeneousDictionaryKey` and have the same
/// "namespace" as this dictionary, i.e. where `Key.Domain == Self.Domain`.
///
/// This type can’t easily conform to `Collection` because `Collection`
/// assumes a single `Element` type.
public struct HeterogeneousDictionary<Domain> {
private var storage: [ObjectIdentifier: Any]
public init() {
self.storage = [:]
}
public var count: Int { self.storage.count }
public subscript<Key>(key: Key.Type) -> Key.Value?
where Key: HeterogeneousDictionaryKey, Key.Domain == Domain
{
get { self.storage[ObjectIdentifier(key)] as! Key.Value? }
set { self.storage[ObjectIdentifier(key)] = newValue }
}
/// A convenience subscript for using key paths as subscript arguments
/// (similar to how the environment is accessed in SwiftUI).
///
/// Usage example:
///
/// ```swift
/// enum PersonKeys {}
///
/// enum NameKey: HeterogeneousDictionaryKey {
/// typealias Domain = PersonKeys
/// typealias Value = String
/// }
///
/// extension HeterogeneousDictionaryValues where Domain == PersonKeys {
/// // You need to add a property of this form for every key
/// var name: NameKey.Type { NameKey.self }
/// }
///
/// var dict = HeterogeneousDictionary<PersonKeys>()
/// dict[\.name] = "Alice" // instead of dict[Name.self]
/// ```
///
/// The `HeterogeneousDictionaryValues` type serves as the "namespace" for the key properties.
/// It would be nicer to use the `Domain` type for this purpose, but then the key properties
/// would have to be static (because a "domain value" is never instantiated), and key paths
/// to static members are not supported as of Swift 5.6.
public subscript<Key>(key: KeyPath<HeterogeneousDictionaryValues<Domain>, Key.Type>) -> Key.Value?
where Key: HeterogeneousDictionaryKey, Key.Domain == Domain
{
get { self[HeterogeneousDictionaryValues()[keyPath: key]] }
set { self[HeterogeneousDictionaryValues()[keyPath: key]] = newValue }
}
}
/// A "namespace" for key properties for use with `HeterogeneousDictionary`'s key-path-based
/// convenience subscript.
public struct HeterogeneousDictionaryValues<Domain> {
fileprivate init() {}
}
// MARK: - Usage
import AppKit
enum TextAttributes {}
struct FontSize: HeterogeneousDictionaryKey {
typealias Domain = TextAttributes
typealias Value = Double
}
struct Font: HeterogeneousDictionaryKey {
typealias Domain = TextAttributes
typealias Value = NSFont
}
struct ForegroundColor: HeterogeneousDictionaryKey {
typealias Domain = TextAttributes
typealias Value = NSColor
}
var dict = HeterogeneousDictionary<TextAttributes>()
dict[ForegroundColor.self] // → nil
dict[ForegroundColor.self] = NSColor.systemRed
dict[ForegroundColor.self] // → NSColor.systemRed (type: Optional<NSColor>)
dict[FontSize.self] // → nil
dict[FontSize.self] = 24
dict[FontSize.self] // → 24 (type: Optional<Double>)
// MARK: - Convenience subscript API based on key paths
extension HeterogeneousDictionaryValues where Domain == TextAttributes {
var fontSize: FontSize.Type { FontSize.self }
var font: Font.Type { Font.self }
var foregroundColor: ForegroundColor.Type { ForegroundColor.self }
}
dict[\.foregroundColor] // → NSColor.systemRed
dict[\.fontSize] // → 24
dict[\.font] // → nil
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment