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
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users

Reply via email to