try! Swift: Type Erasureのユースケースを考えてみた話|サイバーエージェント 公式エンジニアブログという記事を拝見した。
try! Swiftで話題になった型消去について解説されている。良く理解できていなかったのでとても参考になった。ありがとうございます。
ところで最近私はSwiftにおけるオブジェクト指向・ジェネリクスによる実装と関数型的実装の比較とそのハイブリッド方式の実装の検討 - Qiitaという投稿をした。このようなハイブリッド方式でも型消去が必要な問題を解決できないかを考えてみた。
元記事と同様に以下のDefaultConvertibleプロトコルに適合した型を保存するための仕組みを考える。
protocol DefaultConvertible {
/// UDへの保存時に使うキー文字列
static var key: String { get }
/// UDから取得した値をデータ型に変換
init?(_ object: AnyObject)
/// UDへはAnyObjectで保存するのでAnyObjectに変換
func serialize() -> AnyObject
}
struct CameraConfig: DefaultConvertible {
enum Size: Int {
case Large, Medium, Small
}
let saveToCameraRoll: Bool
let size: Size
static let key = "CameraConfig"
init?(_ object: AnyObject) {
guard let dict = object as? [String: AnyObject] else { return nil }
self.saveToCameraRoll = dict["cameraRoll"] as? Bool ?? true
if let rawSize = dict["size"] as? Int, let size = Size(rawValue: rawSize) {
self.size = size
} else {
self.size = .Medium
}
}
func serialize() -> AnyObject {
return ["cameraRoll": saveToCameraRoll, "size": size.rawValue]
}
}
機能の器を作る
まず型消去だが元記事では、PersistentStoreやInMemoryStoreはassociatedtypeに対応するtypealiasを持たないのでDefaultStoreTypeとして、つまり同じ型として保持できない。よってAnyStoreで包んで同じ型として利用できるようにするということのようである。
ハイブリッド方式ではまずプロトコルを用いず基本の型をジェネリクスを利用して作成する。
struct DefaultStore<D: DefaultConvertible>{
typealias Default = D
let set: Default -> ()
let get: () -> Default?
let remove: () -> ()
init(set: Default -> (), get: () -> Default?, remove: () -> ()) {
self.set = set
self.get = get
self.remove = remove
}
}
このDefaultStoreは機能はいっさい持っておらず、機能の器となっている。
機能を作る
器に入れる機能を作成する。DefaultStoreを作成する時に注入するクロージャ3つを含むタプルを返す。DefaultsStoringはNSUserDefaultsを利用し、InMemoryStoringはDictionaryを利用する。
struct DefaultsStoring {
static func storing<D: DefaultConvertible>() -> (set: D -> (), get: () -> D?, remove: () -> ()) {
let defaults = NSUserDefaults.standardUserDefaults()
return (
{ value in
let obj = value.serialize()
defaults.setObject(obj, forKey: D.key)
},
{
guard let obj = defaults.objectForKey(D.key) else { return nil }
return D(obj)
},
{
defaults.removeObjectForKey(D.key)
}
)
}
}
struct InMemoryStoring {
static func storing<D: DefaultConvertible>() -> (set: D -> (), get: () -> D?, remove: () -> ()) {
var dictionary: [String:AnyObject] = [:]
return (
{ value in
let obj = value.serialize()
dictionary[D.key] = obj
},
{
guard let obj = dictionary[D.key] else { return nil }
return D(obj)
},
{
dictionary[D.key] = nil
}
)
}
}
機能を器に注入する
以下のように器であるDefaultStoreに機能を注入する。§はinitにstoring()の返値を適用するSwiftzで定義されている演算子で、Option+6で入力できる。
let defaultsStore = DefaultStore<CameraConfig>.init § DefaultsStoring.storing()
let inMemoryStore = DefaultStore<CameraConfig>.init § InMemoryStoring.storing()
これでdefaultsStoreもinMemoryStoreも同じDefaultStoreという型なので、DefaultStoreを利用する場面ではどちらでも扱うことが可能になる。
考察
ハイブリッド方式は器と機能を分けて定義することで器に柔軟性を持たせることができるようである。
オブジェクト指向・プロトコル・ジェネリクスを組み合わせて型消去が必要になる場面では、オブジェクト指向+関数型のハイブリッド方式で対応することもできるようである。ただし実用性は今後実際に利用していく中で検証していく必要がある。