I could use some advice on how to architect things for Swift (as opposed to
ObjC). This seems like the most appropriate list to ask, but let me know if
there is a more appropriate place.
I am updating an old library to Swift 3 (from 2), and it seemed like a good
opportunity to experiment and modernize things. The library allows you to
write expressions, which you can simplify and calculate the end result. The
API I am going for is similar to a class cluster, where it looks like a single
type to the outside world, but behind the scenes there are different structs to
efficiently represent different types of equations. These structs all adhere
to a common protocol:
public protocol ExpressionProtocol {
associatedtype Result
func value(using provider:ValueProvider)throws -> Result
func simplified(using provider:ValueProvider) -> Expression<Result>
func variables<Identifier:Hashable>() -> Set<Identifier>
var isConstant:Bool {get}
var isLeaf:Bool {get}
}
public extension ExpressionProtocol {
func value()throws -> Result {
return try value(using: EmptyValueProvider())
}
func simplified() -> Expression<Result>{
return self.simplified(using: EmptyValueProvider())
}
}
The ‘ValueProvider’ mentioned here is an abstraction that provides values (of
Result) in return for a given Identifier. Basically, it lets you fill in values
for variables.
All of this gets wrapped behind an ‘Expression’ class which type erases the
various conforming structs and provides initializers for constant values. Then
in separate files, I define various private types of expressions as structs and
use an extension to add convenience initializers to Expression that create
them. The class has a default initializer which takes anything conforming to
ExpressionProtocol.
public extension Expression {
public convenience init(left: Expression<T>, right: Expression<T>,
operation:@escaping (T,T)throws->T) {
self.init(OperationExpression(lhs: left, rhs: right, operation:
operation))
}
}
struct OperationExpression<T>:ExpressionProtocol {
typealias Result = T
var lhs:Expression<T>
var rhs:Expression<T>
var operation:(T,T)throws->T
init(lhs:Expression<T>, rhs:Expression<T>, operation:@escaping
(T,T)throws->T) {
self.lhs = lhs
self.rhs = rhs
self.operation = operation
}
func value(using provider:ValueProvider)throws -> Result {
return try operation(lhs.value(using: provider), rhs.value(using:
provider))
}
func simplified(using provider:ValueProvider) -> Expression<Result> {
let simpleLeft = lhs.simplified(using: provider)
let simpleRight = rhs.simplified(using: provider)
if simpleLeft.isConstant, let left = try? simpleLeft.value(using:
provider) {
if simpleRight.isConstant, let right = try?
simpleRight.value(using: provider) {
if let answer = try? operation(left, right) {
return Expression(answer)
}
}else{
return Expression(OperationExpression(lhs: Expression(left),
rhs: simpleRight, operation: operation))
}
}
return Expression(OperationExpression(lhs: simpleLeft, rhs:
simpleRight, operation: operation))
}
func variables<Identifier:Hashable>() -> Set<Identifier> {
return lhs.variables().union(rhs.variables())
}
var isConstant:Bool {return lhs.isConstant && rhs.isConstant}
var isLeaf:Bool {return false}
}
This all works pretty well, but I am having two main areas of difficulty:
1) Containers such as Optionals and Arrays - I can go one way, but not
the other. If Result is [T] or T?, I can’t seem to get at the internal type T.
I can do it with a free function, but not as an init or even static function
for some reason.
2) Bools - This one surprised me. Most of my expression structs are
generic over their result, but there are a few (e.g. checking equality) where
their Result should always be a Bool. The compiler freaks out about this and
says it isn’t able to convert Result/T to Bool.
extension Expression {
convenience init<E:Equatable>(_ left:Expression<E>, isEqual
right:Expression<E>) {
self.init(EqualsExpression(lhs: left, rhs: right)) //ERROR: 'T' is not
convertable to 'EqualsExpression.Result' (aka Bool)
}
}
struct EqualsExpression<T:Equatable>:ExpressionProtocol {
typealias Result = Bool
var lhs:Expression<T>
var rhs:Expression<T>
init(lhs:Expression<T>, rhs:Expression<T>) {
self.lhs = lhs
self.rhs = rhs
}
func value(using provider:ValueProvider)throws -> Result {
return try lhs.value(using: provider) == rhs.value(using: provider)
}
func simplified(using provider:ValueProvider) -> Expression<Result> {
let simpleLeft = lhs.simplified(using: provider)
let simpleRight = rhs.simplified(using: provider)
if simpleLeft.isConstant, let left = try? simpleLeft.value(using:
provider) {
if simpleRight.isConstant, let right = try?
simpleRight.value(using: provider) {
return Expression(left == right)
}else{
return Expression(EqualsExpression(lhs: Expression(left), rhs:
simpleRight))
}
}
return Expression(EqualsExpression(lhs: simpleLeft, rhs: simpleRight))
}
func variables<Identifier:Hashable>() -> Set<Identifier> {
return lhs.variables().union(rhs.variables())
}
var isConstant:Bool {return lhs.isConstant && rhs.isConstant}
var isLeaf:Bool {return false}
}
I have tried about a dozen ways, and I can’t seem to make it fit. I can do
free functions, but not initializers.
Is there a way to make this work? Is my architecture fundamentally wrong for
Swift? Is there another design I should be considering instead?
What I like about the current approach that I would like to keep:
• Everything is a single type to the end user. They don’t have to remember
whether it was called an EqualsExpression, etc… there are just a bunch of inits
which autocomplete.
• I like the generic return type, as it lets me put things together correctly,
and reads clearly. e.g. Expression<Int> is a very clear type to explain to
people.
• There aren’t restrictions on what you can have an Expression of. I may
conditionally add some features based on the Result type, but at the very
least, you can make an Expression of anything and combine them with operations.
• It is retroactively extensible with new types of expressions while keeping
all of the above features.
Any advice is appreciated…
Thanks,
Jon
_______________________________________________
swift-users mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-users