Last active
May 17, 2023 19:23
-
-
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/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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