@Douglas_Gregor, @John_McCall
Although swift is rely heavily on composition and interfaces, there was a lack and need of thing called existential container, which means a variable that can hold a value of a type, specified by interface constraint. If you have been is swift long enough, you probably faced this problem.
protocol MyProtocol {} //empty protocol, aka empty inteface
var someVar: MyProtocol // this actually works, but hold on.
It works indeed, but thing get more complicated,when protocol contains an assocciatedtype
, which means, that types must be derived from context.
protocol OpaqueProtocol { assocciatedtype Value ; var someVar: Value }
struct SomeStruct: OpaqueProtocol { var someVar = "Hello" }
var instance = SomeStruct() // type of Value is inferred to be String
var opaqueVar: OpaqueProtocol = SomeStruct() //error: protocol cannot be used here
I was very disappointed by fighting that issue, but I tried to make a workaround and made a sort of satisfieble solution.
struct CustomStorage<Value> {
private(set) var value: Value?
private(set) var setter: Functor<Value, Void>
init(constraints: [(Value)->Bool] = []) {
self.setter = Functor.init(preconditions: constraints){$0}
}
init(constraints: [(Value)->Bool] = [], value: Value) {
self.setter = Functor.init(preconditions: constraints){$0}
do {
try self.setter.invoke(with: value)
} catch let predicate {
fatalError("\(value) does not satisfy \(predicate)")
}
self.value = value
}
/*mutating func turnIntoUnboundType() {
self.value = value as! Any
}
mutating func reboundTo<P>(type: P.Type){
self.value = value as! P
}
mutating func rebound() {
self.value = value as! value.Type
}*/
mutating func addConstraints(_ list: (Value)->Bool...){
self.setter.preconditions += list
}
mutating func addConstraintsAndCheck(_ list: (Value)->Bool...){
self.setter.preconditions += list
if self.value != nil {
if (try? self.setter.invoke(with: self.value!)) == nil {
fatalError("Current value in \(self) does not satisfy new constraints")
}
}
}
func get<T>(_ transformer: (Value) -> T) -> T? {
if self.value != nil {
return transformer(self.value!)
}
else { return nil }
}
mutating func set(to newValue: Value) throws {
do {
try setter.invoke(with: newValue)
} catch let error {
throw error
}
self.value = newValue
}
}
struct Functor<Input, Output> {
//assuming only fuctions with one argument
let function: (Input) -> Output
let inputType: Input.Type
let outputType: Output.Type
var preconditions: [(Input)->Bool]
init(preconditions: [(Input)->Bool] = [], function: @escaping (Input) -> Output) {
self.function = function
self.preconditions = preconditions
self.inputType = Input.self
self.outputType = Output.self
}
enum PredicateError: Error {
case preconditionError((Input)->Bool)
case postconditionError((Output)->Bool)
}
func invoke(with a: Input) throws -> Output {
for f in preconditions {
if f(a) == false {
throw PredicateError.preconditionError(f); break
}
}
return self.function(a)
}
}
It works in quite useful way, like this:
var nonZeroNumber = CustomStorage.init(constraints: [{$0 > 0}, {Float($0) != nil}], value: 5)
// can hold any value greater than zero, that can be converted to Float
The reason I am showing you this is to demonstrate a concept of a customizible variable, that can not only hold some trivial constraint like var a: Numeric = 0
, but more flexible and even modifiable at run-time! Like this, potentially:
var onlyNumbers = CustomStorage
.init(constraints: [{$0 is Numeric}])
onlyNumbers.addConstraints({$0 < 1024}, {($0 * $0) > $0 + $0})
onlyNumbers =? 0 //assignments fails, cuz constraints are violated
onlyNumbers =! 3 //assignment succeeds
//I also made some syntactic sugar in form of this operators,
//so it is possible to
if a =? 10 { print(a.get{$0}!) }
A variable onlyNumbers
can contain any type that conform to numeric, or any other wild constraints, which you can create. Turing-completeness in help!
The most noticeable downfall here is that it doesn't provide wanted flexibility. And strict typing and messy type system are those to be blamed.
First of all, the potential way to do this is to explicitly initialize CustomStorage
with type Any
, but because swift cannot guarantee, that traits of a given type satisfy closures in preconditions, is halts compilation with error, whenever these closures contains anything type-specific. For example:
var anyStringRepresentable = CustomStorage<Any>
.init(constraints: [{$ != ""}, {$0 is StringRepresentable}]) //doesnt not compile
One thing to pay attention to is that ,even though anyStringRepresentable
is not declared with concrete type(e.g. String
), at any given moment after initialization and assignment that Any
type in fact represent some valid concrete type(compiler checks all conformances at compile-time). So at any that time it is some what allowed to do the following, if we omit the fact that Any
can be non-nominal type:
value = value as! value.Type //impossible to do now. Compiler doesn't know what i want
//or
var a: Any = 5
a = a as! a.Type
type(of: a) //Int
//consider following code as well
var c = CustomStorage<Any>.init(constraints: [{$0 is Int}]) //Only Ints
c =! 4
var d = CustomStorage<Any>.init(constraints: [{$0 is Int}]) //Only Ints
d =! 5
type(of: c.get{$0}!) //Int. Concrete type
type(of: d.get{$0}!) //Int. Concrete type
//If we need to abstract away concrete types, we can define it so all lookups happen at runtime from what i know
func + <T, U>(lhs: CustomStorage<T>, rhs: CustomStorage<U>) throws
-> Int where T: BinaryInteger, U: BinaryInteger { ... }
let someVar = c + d //Nope! Compilation error
But it fails at compilation, beacuse binary operator '+' cannot be applied to two 'CustomStorage<Any>' operands
. So swift actually doesn't perform lookup, i believe, because it cannot know that these types are actually nominal, hence it faults.
Potential solution to this is to split Any
into subtypes to have some guaranties, so type cheker can enforce correctness:
AnyType - can store anything that exist in swift. No safe way to lookup at runtime.
AnyNominal - can only store types, that conform to protocols(structs, classes, enums), can be look-uped for metadata in runtime
AnyMetatype - can store type, that describe other types
If we assume the idea above, than doing conformance check is always 100% safe.
extension AnyNominal { func isConforming(to protocol: Protocol.Type) -> Bool {...} }
"String".isConforming(to: Equatable.self) //true
//or
var a = CustomStorage<AnyNominal>
.init(constraints: [{$0.isConforming(to: Numeric.self)}])
// means it is safe to compare against
//or from previous example above
var c = CustomStorage<AnyNominal>.init(constraints: [{$0 is Int}]) //Only Ints
c =! 4
var d = CustomStorage<AnyNominal>.init(constraints: [{$0 is Int}]) //Only Ints
d =! 5
func + <T, U>(lhs: CustomStorage<T>, rhs: CustomStorage<U>) throws
-> Int where T: BinaryInteger, U: BinaryInteger { ... }
let p = c + d // look up at runtime, crush or propagate error, if constraints aren't met
I assume as of now conformance check can be done by some introspection capabilities with involvement of spawning mirrors and comparing structures to know if an object is nominal, meta-type or anything else, but it would be much less clunky and faster to embed this functionality in lower level.
I have been on swift forum for a while, and noticed folks around were asking for simillar functionality, like in these threads: [Generalized Type Erasure (Existential Types)], [Pitch: Introduce custom attributes], [Extensions for existential types. Custom existential containers], [Enhanced existential types proposal discussion], [Status of subclass existentials]
So what was this all about?
Well,
- What is the best way to declare variable with multiple constraints on storable value ?
- I want to know what's the state of existential storage, and why it is so delayed
- It is already possible to make something similar with current language facilities as I showed, but to have same value as the concept we keep dream of, language should extend type casting system at least. Is it actually the case or am I missing something?
- Given all those points , I wonder, whether it would be better to implement opaque storage at language level.
- Will swift acquire any rich functionality, if it posses
Any
split as shown?
Here is some complete code to play around, if you wish. [Opaque Storage.swift - Google Drive]