[swift-evolution] [Pitch] Contextual variables

rintaro ishizaki fs.output at gmail.com
Sun Jul 2 21:59:24 CDT 2017


Ughh, my bad, let me withdraw this idea:

func saveAndRestore<T, R>(_ variable: inout T, _ tmpVal: T, body: ()
-> R) -> R {
    let savedVal = variable
    variable = tmpVal
    defer { variable = savedVal }
    return body()
}
var contextVal: Int = 0saveAndRestore(&contextVal, 1) {
    print(contextVal)
}


I don't think this is guaranteed to work.




2017-07-03 11:48 GMT+09:00 rintaro ishizaki via swift-evolution <
swift-evolution at swift.org>:

>
>
> 2017-07-03 11:23 GMT+09:00 rintaro ishizaki via swift-evolution <
> swift-evolution at swift.org>:
>
>>
>>
>> 2017-06-28 21:33 GMT+09:00 Dimitri Racordon via swift-evolution <
>> swift-evolution at swift.org>:
>>
>>> Hello community!
>>>
>>> I’d like to pitch an idea for a user-friendly way for functions to pull
>>> values from an arbitrary environment. Let me introduce the concept with a
>>> motivational example before I dig into dirty syntax and semantics. Note
>>> that I intentionally removed many pieces of code from my examples, but I
>>> guess everybody will be able to understand the context.
>>>
>>> Say you are writing a visitor (with the pattern of the same name) for an
>>> AST to implement an interpreter:
>>>
>>> class Interpreter: Visitor {
>>>     func visit(_ node: BinExpr)    { /* ... */ }
>>>     func visit(_ node: Literal)    { /* ... */ }
>>>     func visit(_ node: Scope)      { /* ... */ }
>>>     func visit(_ node: Identifier) { /* ... */ }
>>> }
>>>
>>> Although this design pattern is often recommended for AST processing,
>>> managing data as we go down the tree can be cumbersome. The problem is that
>>> we need to store all intermediate results as we climb up the tree in some
>>> instance member, because we can’t use the return type of the visit(_:) method,
>>> as we would do with a recursive function:
>>>
>>
>>
>> Why you can't use the return type? associatedtype doesn't solve the
>> problem?
>>
>> protocol Visitor {
>>     associatedtype VisitResult
>>     func visit(_ node: BinExpr) throws -> VisitResult
>>     func visit(_ node: Literal) throws -> VisitResult
>>     func visit(_ node: Scope) throws -> VisitResult
>>     func visit(_ node: Identifier) throws -> VisitResult
>> }
>> extension BinExpr {
>>     func accept<V : Visitor>(_ visitor: V) throws -> V.VisitResult { return visitor.visit(self) }
>> }extension Literal {
>>     func accept<V : Visitor>(_ visitor: V) throws -> V.VisitResult { return visitor.visit(self) }
>> }extension Scope {
>>     func accept<V : Visitor>(_ visitor: V) throws -> V.VisitResult { return visitor.visit(self) }
>> }extension Identifier {
>>     func accept<V : Visitor>(_ visitor: V) throws -> V.VisitResult { return visitor.visit(self) }
>> }
>> class Interpreter : Visitor {
>>     func visit(_ node: BinExpr) -> Int {
>>         let lhsResult = node.lhs.accept(self)
>>         let rhsResult = node.rhs.accept(self)
>>         /* ... */
>>         return result
>>     }
>>
>>     /* ... */
>> }
>>
>>
>>
>>
>>
>>
>>> class Interpreter: Visitor {
>>>     func visit(_ node: BinExpr) {
>>>         node.lhs.accept(self)
>>>         let lhs = accumulator!
>>>         node.rhs.accept(self)
>>>         let rhs = accumulator!
>>> /* ... */
>>>     }
>>>
>>>     func visit(_ node: Literal)    { /* ... */ }
>>>     func visit(_ node: Scope)      { /* ... */ }
>>>     func visit(_ node: Identifier) { /* ... */ }
>>>
>>>     var accumulator: Int? = nil
>>>
>>>     /* ... */
>>> }
>>>
>>> As our interpreter will grow and need more visitors to “return” a value,
>>> we’ll be forced to add more and more stored properties to its definition.
>>> Besides, the state of those properties is difficult to debug, as it can
>>> quickly become unclear what depth of the tree they should be associated to.
>>> In fact, it is as if all these properties acted as global variables.
>>>
>>> The problem gets even bigger when we need to *pass* variables to a
>>> particular execution of a visit(_:). Not only do we need to add a
>>> stored property to represent each “argument”, but we also have to store
>>> them in stacks so that a nested calls to a particular visit can get their
>>> own “evaluation context”. Consider for instance the implementation of the
>>> visit(_ node: Identifier), assuming that the language our AST
>>> represents would support lexical scoping.
>>>
>>
>>
>> How about returning curried function from visitor?
>>
>>
>> class Interpreter : Visitor {
>>
>>     typealias VisitResult = ([String:Int]) throws -> Int
>>
>>     func visit(_ node: Identifier) throws -> VisitResult {
>>         return { symbols in
>>             guard let result = symbols[node.name] {
>>                 throws UndefinedIdentifierError(node.name)
>>             }
>>             return result
>>         }
>>     }
>>
>>     /* ... */
>> }
>>
>>
>>
>>
>>> class Interpreter: Visitor {
>>>     /* ... */
>>>
>>>     func visit(_ node: Scope) {
>>>         symbols.append([:])
>>>         for child in node.children {
>>>             child.accept(self)
>>>         }
>>>         symbols.removeLast()
>>>     }
>>>
>>>     func visit(_ node: Identifier) {
>>>         accumulator = symbols.last![node.name]!
>>>     }
>>>
>>>     var symbols = [[String: Int]]()
>>> }
>>>
>>> We could instead create another instance of our visitor to set manage
>>> those evaluation contexts. But that would require us to explicitly copy all
>>> the variables associated to those contexts, which could potentially be
>>> inefficient and error prone.
>>>
>>> In fact, this last point is also true when dealing with recursive
>>> functions. For instance, our visit(_ node: Identifier) method could be
>>> rewritten as:
>>>
>>> func interpret(_ identifier: Identifier, symbols: [String: Value]) ->
>>> Int { /* ... */ }
>>>
>>> so that its evaluation context is passed as a parameter. But this also
>>> requires all other functions to also pass this argument, even if their
>>> execution does not require the parameter.
>>>
>>> func interpret(_ binExpr: BinExpr, symbols: [String: Value]) -> Int {
>>>     let lhs = interpret(node.lhs.accept, symbols: symbols)
>>>     /* ... */
>>> }
>>>
>>> This technique consisting of passing parameters through a function just
>>> so that another function called deeper in the stack can get its variable is
>>> actually quite common. Sadly, it clouds all signatures with many
>>> parameters, which make it more difficult to reason about what a particular
>>> function actually needs from its caller. Note also that this overuses the
>>> running stack, putting many unnecessary values in all execution frames.
>>>
>>> The idea I’d like to pitch is to offer a mechanism to address this
>>> issue. Namely, I’d like a way to provide a function with an environment
>>> when using its parameter and/or return type is not an option, or when doing
>>> so would add unnecessary complexity to its signature (like illustrated
>>> above). While this mechanism would share similarities with how functions
>>> (and closures) are able to capture variables when they are declared, it
>>> would differ in the fact that these environment would depend on the
>>> execution frame prior to that of a particular function call rather than the
>>> function declaration/definition.
>>>
>>> First, one would declare a *contextual variable:*
>>>
>>> context var symbols: [String: Int]?
>>>
>>> Such contextual variables could be seen as stacks, whose values are
>>> typed with that of the variable. In that particular example, the type of
>>> the context symbols would be [String: Int]. The optional is needed to
>>> explicitly represent the fact that a context may not always be set, but
>>> this could be inferred as well. One would be able to set the value a
>>> contextual variable, effectively pushing a value on the stack it represent,
>>> before entering a new execution frame:
>>>
>>> set symbols = [:] in {
>>>     for child in node.children {
>>>         child.accept(self)
>>>     }
>>> }
>>>
>>> In the above example, the contextual variable symbols would represent
>>> an empty dictionary for all execution frames above that of the context
>>> scope (delimited by braces). Extracting a value from a context would boils
>>> down to reading an optional value:
>>>
>>> guard let val = symbols?[node.name] else {
>>>     fatalError("undefined symbol: \(node.name)")
>>> }
>>> accumulator = val
>>>
>>>
>>
>> You can do something like this:
>>
>> func saveAndRestore<T, R>(_ variable: inout T, _ tmpVal: T, body: () -> R) -> R {
>>     let savedVal = variable
>>     variable = tmpVal
>>     defer { variable = savedVal }
>>     return body()
>> }
>> class Interpreter : Visitor {
>>
>>     var symbols: [String: Int] = [:]
>>
>>     func visit(_ node: Scope) throws -> Int {
>>         return saveAndRestore(symbols, [:]) {
>>
>>
>
> Ah, this must be `saveAndRestore(&symbols, [:]) {` of course.
>
>
>
>>             for child in node.children {
>>                 child.accept(this)
>>             }
>>             return 0
>>
>>         }
>>     }
>>
>>     func visit(_ node: Identifier) throws -> Int {
>>         guard let result = symbols[node.name] {
>>             throws UndefinedIdentifierError(node.name)
>>         }
>>         return result
>>     }
>>
>>     /* ... */
>> }
>>
>>
>>
>> And as contextual variables would actually be stacks, one could push
>>> another value on the top of them to setup for another evaluation context.
>>> Hence, would we call set symbols = [:] in { /* ... */ } again, the
>>> contextual variable symbols would represent another empty dictionary as
>>> long as our new context would be alive:
>>>
>>> set symbols = ["foo": 1] in {
>>>     set symbols = ["foo": 2] in {
>>>         print(symbols!["foo”]!)
>>>         // Prints 2
>>>     }
>>>     print(symbols!["foo”]!)
>>>     // Prints 1
>>> }
>>>
>>> The advantage of that approach is threefold.
>>>
>>>    1. It lets us provide an environment to functions that can’t receive
>>>    more parameters or return custom values. This is particularly useful when
>>>    dealing with libraries that provide an entry to define custom behaviour,
>>>    but fix the API of the functions they expect (e.g. a visitor protocol). In
>>>    those instances, capture by closure is not always possible/desirable.
>>>    2. In large function hierarchy, it lets us provide deep functions
>>>    with variables, without the need to pass them in every single function call
>>>    just in the off chance one function may need it deeper in the call graph.
>>>    3. It better defines the notion of stacked environment, so that one
>>>    can “override” an execution context, which is often desirable when
>>>    processing recursive structures such as trees or graphs. In particular, it
>>>    is very useful when not all functions require all data that are passed down
>>>    the tree.
>>>
>>>
>>> Using our contextual variables, one could rewrite our motivational
>>> example as follows:
>>>
>>> class Interpreter: Visitor {
>>>     func visit(_ node: BinExpr) {
>>>         let lhs, rhs : Int
>>> set accumulator = nil in {
>>>             node.lhs.accept(self)
>>>             lhs = accumulator!
>>>         }
>>> set accumulator = nil in {
>>>             node.lhs.accept(self)
>>>             rhs = accumulator!
>>>         }
>>>
>>>         switch node.op {
>>>         case "+":
>>>             accumulator = lhs + rhs
>>>         case "-":
>>>             accumulator = lhs - rhs
>>>         default:
>>>             fatalError("unexpected operator \(node.op)")
>>>         }
>>>     }
>>>
>>>     func visit(_ node: Literal) {
>>>         accumulator = node.val
>>>     }
>>>
>>>     func visit(_ node: Scope) {
>>> set symbols = [:] in {
>>>             for child in node.children {
>>>                 child.accept(self)
>>>             }
>>>         }
>>>     }
>>>
>>>     func visit(_ node: Identifier) {
>>>         guard let val = symbols?[node.name] else {
>>>             fatalError("undefined symbol: \(node.name)")
>>>         }
>>>         accumulator = val
>>>     }
>>>
>>>     context var accumulator: Int?
>>>     context var symbols: [String: Int]?
>>> }
>>>
>>> It is no longer unclear what depth of the tree the accumulator variable
>>> should be associated with. The mechanism is handled automatically,
>>> preventing the programmer from incorrectly reading a value that was
>>> previously set for another descent. It is no longer needed to manually
>>> handle the stack management of the symbols variable, which was error
>>> prone in our previous implementation.
>>>
>>> The scope of contextual variables should not be limited to type
>>> declarations. One may want to declare them in the global scope of a module,
>>> so that they would be part of the API of a library. Imagine for instance a
>>> web framework library, using contextual variables to provide the context of
>>> a request handler:
>>>
>>> // In the framework ...
>>> public context var authInfo: AuthInfo
>>>
>>> // In the user code ...
>>> framework.addHandler(for: URL("/index")) {
>>>     guard let user = authInfo?.user else {
>>>         return Redirect(to: URL("/login"))
>>>     }
>>>
>>>     return Response("Welcome back \(user.name)!")
>>> }
>>>
>>> In that example, one could imagine that the framework would set the
>>> contextual authInfo variable with the authentication information it
>>> would parse from the request before calling the registered handlers.
>>>
>>> This idea is not exactly new. In fact, people familiar with Python may
>>> recognise some similarities with how "with statements" work. Hence, it
>>> is not surprising that things one is able to do with Python’s contexts
>>> would be possible to do with contextual variables as presented above.
>>> Consider for instance the following class:
>>>
>>> class Connexion {
>>>     init(to: URL) { /* ... */ }
>>>
>>>     deinit {
>>>         self.disconnect()
>>>     }
>>>
>>>     func disconnect() { /* ... */ }
>>> }
>>>
>>> Thanks to Swift’s memory lifecycle, instantiating an instance of
>>> Connexion as a contextual variable would automatically call its
>>> destructor when the context would get popped out.
>>>
>>> context var conn: Connexion
>>> set conn = Connexion(to: URL("http://some.url.com")) in {
>>>     /* ... */
>>> } // the first connection is disconnected here
>>>
>>> I see many other applications for such contextual variables, but I think
>>> this email is long enough.
>>> I’m looking forward to your thought and feedbacks.
>>>
>>
>>> Best regards,
>>>
>>>
>>> Dimitri Racordon
>>> CUI, Université de Genève
>>> 7, route de Drize, CH-1227 Carouge - Switzerland
>>> Phone: +41 22 379 01 24 <+41%2022%20379%2001%2024>
>>>
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>>
>>>
>>>
>>>
>>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170703/4ccc4235/attachment.html>


More information about the swift-evolution mailing list