rokucommunity / brighterscript

A superset of Roku's BrightScript language
MIT License
152 stars 47 forks source link

Add closure support #530

Open TwitchBronBron opened 2 years ago

TwitchBronBron commented 2 years ago

BrighterScript should support closures.

Here's a quick example:

sub onButtonClick(event)
    response = "" 'gets set by closure below

    checkEmail = (domain) => {
        inputValue = m.emailAddressTextEditBox.text
        if inputValue.endsWith(domain) then
            response ="valid"
        else
            response = "invalid"
        end if
    }

    checkEmail("@roku.com")

    return responseMessage
end sub

would transpile to:

sub onButtonClick(event)
    local = {
        event: event
    }
    local.response = "" 'gets set by closure below

    local.checkEmail = {
        type: "arrow-function",
        scopes: [local],
        m: m,
        func: function(__closure, domain)
            inputValue = __closure.m.emailAddressTextEditBox.text
            if inputValue.endsWith(domain) then
                __closure.scopes[0].response ="valid email"
            else
                __closure.scopes[0].response = "invalid email"
            end if
        end function
    }

    local.checkEmail.func(local.checkEmail, "@roku.com")

    return responseMessage
end sub

Considerations:

  1. the moment a closure is created, we force all local variables onto an AA, and there are no longer any true local variables. This allows the "local" scope to get "captured" and flow through any child closures
  2. since a closure needs type tracking, this can only be implemented in the alpha branch.
  3. Add a ArrowFunction interface type which can be used as function parameters and other types.
    sub Validate(validationFunction as ArrowFunction)
        validationFunction() 'transpiles to validationFunction.func(validationFunction)
    end sub
  4. In the initial release, we will restrict closures from being passed into any parameter/assignment not explicitly marked as ArrowFunction
    sub main()
        arrowFunc = () => { return true }
        name = "bob"
        name = arrowFunc' ERROR: arrow function cannot be assigned to non-closure existing variable
        logInfo(arrowFunc) 'ERROR: arrow function cannot be passed as parameter not explicitly marked as `ArrowFunction`
        callArrowFunc(arrowFunc) ' OK 
        arrowFunc = ()=> {return false} ' OK since previous var was also an arrow function
    end sub
    sub logInfo(anything as dynamic)
        print anything
    end sub
    sub callArrowFunc(func as ArrowFunction)
        func()
    end sub
  5. Nested closures should be supported. For example:
    sub onButtonClick(event)
        outerName = "outer"
        outerFunc = () =>{
            innerName = "inner"
            print outerName
            innerFunc = () => {
                print outerName; innerName
            }
            innerFunc()
        }
        outerFunc()
    end sub

    transpiles to:

    sub onButtonClick(event)
        local = {
            event: event
        }
        local.outerName = "outer"
        local.outerFunc = {
            scopes: [local],
            m: m,
            func: function(__closure)
                local = {}
                local.innerName = "inner"
                print __closure.scopes[0].outerName
                local.innerFunc = {
                    scopes: [__closure.scopes[0], local]
                    m: __closure.scopes[0], 
                    func: function(__closure)
                        print __closure.scopes[0].outerName; __closure.scopes[0].innerName
                    end function
                end function
                local.innerFunc.func(local.innerFunc)
            end func
        }
        local.outerFunc.func(local.outerFunc)
    end sub
georgejecook commented 2 years ago

would you offer return short hand "return value by default" that other languages have?

e.g. mc.collections.filter(items, (i) => i.rating > 3.5)

TwitchBronBron commented 2 years ago

Yeah, I think so!

philippe-elsass-deltatre commented 2 years ago

It really hurts to see the JS syntax though.

General approach makes sense though we shouldn't have all the locals go in the closure object if possible.

Naming of things could be better :D

TwitchBronBron commented 2 years ago

@philippe-elsass-deltatre I assume you're talking about the curly braces for the arrow function? Since there's no opening keyword, I didn't really see any way around it. What would you suggest?

Also, what would you name the things? All this is in the air, so we should work out those things now.

TwitchBronBron commented 2 years ago

@philippe-elsass-deltatre

we shouldn't have all the locals go in the closure object if possible.

This could definitely be a compiler optimization, but it felt easier to just rewrite everything for now. But I agree, properties not used in closures don't need to be on the local collections.

philippe-elsass-deltatre commented 2 years ago

Actually the naming part is fine, but there is some ambiguity around "Closure" (a local function has access to local vars in parent scopes) VS "ArrowFunction" (a closure where this is the parent scope's)

And about the as ArrowFunction - I'm envisioning cases where we want to accept both normal functions and arrow ones; granted it would be a case where you'd have to poke at the object to check whether it's a function or an arrow function.

philippe-elsass-deltatre commented 2 years ago

Is that a big problem to consider introducing a keyword? Maybe we could use the term lambda to describe these functions - it's quite common in other languages.

TwitchBronBron commented 2 years ago

Definitely not a problem to consider a keyword. I like how the arrow functions are concise, but it does feel like a deviation from the rest of the language. Lambda doesn't feel quite right. What about 'closure' instead?

checkEmail = closure(domain)
    inputValue = m.emailAddressTextEditBox.text
    if inputValue.endsWith(domain) then
        response ="valid"
    else
        response = "invalid"
    end if
end closure
georgejecook commented 2 years ago

I actually think we should lean into lambdas, as we can't really do closures - it will never be a proper implementation; but hte limitatinos we've described here, are actually the design of lambdas, as pointed out by @philippe-elsass-deltatre : https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/procedures/lambda-expressions

TwitchBronBron commented 2 years ago

Just to be clear, the vb lambda design DOES have access to its containing scope, as outlined here: https://docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/language-features/procedures/lambda-expressions#context