[swift-evolution] [Proposal] Property behaviors

davesweeris at mac.com davesweeris at mac.com
Fri Jan 15 02:18:24 CST 2016


> On Jan 14, 2016, at 17:16, Dave via swift-evolution <swift-evolution at swift.org> wrote:
> 
> I might play with it a bit tonight after dinner.

Ok, so here’s what I came up with. It partially works. Also, I thought it might be easier to supply a list of behaviors instead of using function composition. Both ways are included, since I’d already written the one using function composition when I thought of the other way.

Oh, and to be clear, I’m NOT saying this is the way I think we should do this… I’m just exploring how for we can take it without adding any new language features.

Here’s the “library” part:
//: Playground - noun: a place where people can play

import Cocoa

protocol Wrapper {
    typealias T
    var value: T {get set}
}

// "assignment" operator, since we can't overload the real one
// (yes, I know "≠" means "not equals"... it's easy to type, and
// this is just for a proof of concept)
infix operator ≠ {}
func ≠ <U: Wrapper, V: Wrapper where U.T == V.T> (inout lhs: U, rhs: V) { lhs.value = rhs.value }
func ≠ <V: Wrapper> (inout lhs: V.T, rhs: V) { lhs = rhs.value }
func ≠ <V: Wrapper> (inout lhs: V, rhs: V.T) { lhs.value = rhs }

// function composition. Nothing special about "•"... it's just about as close as
// I could get to the "⊙" my math textbooks used without pulling up the symbols palette
infix operator • {}
func • <T, U, V> (f1: U -> V, f2: T -> U) -> (T -> V) { return { f1(f2($0)) } }

// function currying. Nothing special about "<|>"... is was just what someone else used
infix operator <|> {precedence 200}
func <|> <T, U, V> (f: (T,U)->V, b: U) -> (T->V) { return { f($0, b) } }

// Seemed the easiest way to handle “lazy”.
enum MaybeLazy<T> {
    case Yep(()->T)
    case Nah(T)
    
    init(_ value: ()->T) { self = .Yep(value) }
    init(_ value: T) { self = .Nah(value) }
    
    var value: T {
        get {
            switch self {
            case .Nah(let t): return t
            case .Yep(let tc): return tc()
            }
        }
        set {
            self = .Nah(newValue)
        }
    }
}

// Where the sufficiently advanced technology happens
struct Observable<T> : Wrapper, CustomStringConvertible {
    // I'm not actually sure this typealias needs to exist. Sometimes
    // the playground gets... stubborn... about what is or isn't an error,
    // and there's no way to clean/build. Plus I lose track of where the Ts go.
    typealias FType = (T,T)->(T,T)
    
    // The "do nothing" closure
    static func _passThrough(x:(oldValue: T, newValue: T)) -> (T,T) { return x }
    
    // "Function"
    var f: FType! = nil
    // "Element Function". More on this later.
    var ef: Any? = nil
    var _value:MaybeLazy<T>
    var value: T {
        get { return _value.value }
        set { _value.value = (f != nil) ? f(_value.value, newValue).1 : newValue }
    }
    
    // inits for function composition
    init(_ value: T, _ f: (oldValue: T, newValue: T)->(T, T) = Observable._passThrough) {
        self._value = .Nah(value)
        self.f = f
    }
    init(lazy: ()->T, _ f: (oldValue: T, newValue: T)->(T, T) = Observable._passThrough) {
        self._value = .Yep(lazy)
        self.f = f
    }
    
    // inits for variadic list of functions
    init(_ value: T, behaviors: ((oldValue: T, newValue: T)->(T, T))...) {
        self._value = .Nah(value)
        switch behaviors.count {
        case 0: self.f = Observable._passThrough
        case 1: self.f = behaviors[0]
        case _:
            var c = behaviors.last!
            for i in (0 ..< (behaviors.count - 1)).reverse() {
                c = c • behaviors[i]
            }
            self.f = c
        }
    }
    init(lazy: ()->T, behaviors: ((oldValue: T, newValue: T)->(T, T))...) {
        self._value = .Yep(lazy)
        switch behaviors.count {
        case 0: self.f = Observable._passThrough
        case 1: self.f = behaviors[0]
        case _:
            var c = behaviors.last!
            for i in (0 ..< (behaviors.count - 1)).reverse() {
                c = c • behaviors[i]
            }
            self.f = c
        }
    }
    
    var description: String { return "\(value)" }
}

// the behavior functions to be passed in
func didSet <T> (x: (oldValue: T, newValue: T), _ f: ((T,T))->((T,T))) -> (T,T) {
    f(x)
    return (x)
}
func didChange<T: Equatable> (x: (oldValue: T, newValue: T), _ f: ((T,T))->((T,T))) -> (T,T) {
    if x.oldValue != x.newValue {
        f(x)
    }
    return (x)
}

And here’s how you’d use it (except with “=“ instead of “≠”, of course):
var exampleOfCompositionSyntax = Observable(3,
    didSet <|> {
        print("didSet")
        return ($0, $1)
    } • didChange <|> {
        print("didChange")
        return ($0,$1)
    }
)
exampleOfCompositionSyntax ≠ 3 // the closure passed to didSet is called, but not didChange
exampleOfCompositionSyntax ≠ 4 // both are called, and it now evaluates to 4

var exampleOfVariadicSyntax = Observable(3, behaviors:
    didSet <|> {
        print("didSet")
        return ($0, $1)
    },
    didChange <|> {
        print("didChange")
        return ($0,$1)
    }
)
exampleOfVariadicSyntax ≠ 3 // the closure passed to didSet is called, but not didChange
exampleOfVariadicSyntax ≠ 4 // both are called, and it now evaluates to 4

var nowWithMoreLaziness = Observable(lazy: {return 4})
var ibar = 0 // here’s one glitchy bit… ibar has to be declared first since “=“ can’t be overloaded
ibar ≠ nowWithMoreLaziness // ibar evaluates to 4

Trying to extending the behavior to collections is where things kinda fall apart. This code doesn’t generate any errors, but it causes Xcode to repeatedly “lose communication with the playground service":
extension Observable where T: MutableCollectionType {
    // This extension is where we support per-element behavior. "ef" is really
    // of type "CType", but we couldn't declare it that way because we didn't
    // know that T was a MutableCollectionType until now.
    typealias CType = (T.Generator.Element,T.Generator.Element,T.Index)->(T.Generator.Element,T.Generator.Element,T.Index)
    
    // The "do nothing" closure
    static func _ePassThrough(x:CType) -> CType { return x }
    
    init(_ value: T, elementalbehaviors: CType...) {
        self._value = .Nah(value)
        switch elementalbehaviors.count {
        case 0: self.ef = Observable._ePassThrough
        case 1: self.ef = elementalbehaviors[0]
        case _:
            var c = elementalbehaviors.last!
            for i in (0 ..< (elementalbehaviors.count - 1)).reverse() {
                c = c • elementalbehaviors[i]
            }
            self.ef = c
        }
    }
    subscript(i: T.Index) -> T.Generator.Element {
        get { return value[i] }
        set { value[i] = (ef != nil) ? (ef! as! CType)(value[i], newValue, i).1 : newValue }
        
    }
}

So is it a bug in my code, or a bug in Playgrounds that’s causing the problem? Dunno, and I’m too tired to find out tonight. Obviously, if any of you want to play with it, go ahead… that’s why I posted the code :-)

- Dave Sweeris

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160115/c7e7d965/attachment.html>


More information about the swift-evolution mailing list