How to combine `@dynamicMemberLookup` and `ExpressibleByStringInterpolation` to achieve method chaining?

huangapple go评论54阅读模式
英文:

How to combine `@dynamicMemberLookup` and `ExpressibleByStringInterpolation` to achieve method chaining?

问题

I have the following enum:

@dynamicMemberLookup
enum JSCall: ExpressibleByStringInterpolation {
    case getElement(String)
    case getInnerHTML(id: String)
    case removeAttribute(id: String, attributeName: String)
    case customScript(String)
    case document
    indirect case chainedCall(JSCall, String)

    var value: String {
        switch self {
        case .document:
            return "document"
        case .getElement(let id):
            return JSCall.chainedCall(.document, "getElementById('\(id)')").value
        case .getInnerHTML(let id):
            return JSCall.getElement(id).innerHTML.value // Using dynamic member lookup here
        case .removeAttribute(let id, let attribute):
            return JSCall.chainedCall(.getElement(id), "removeAttribute('\(attribute)')").value
        case .chainedCall(let prefixJs, let suffix):
            return "\(prefixJs.scriptString).\(suffix)"
        case .customScript(let script):
            return script
        }
    }

    init(stringLiteral value: String) {
        self = .customScript(value)
    }
    
    subscript(dynamicMember suffix: String) -> JSCall {
        return .chainedCall(.customScript(self.value), suffix)
    }
}

Notice that in computed property value, for getInnerHTML case, I could use a natural syntax for chaining .innerHTML via dot syntax, thanks to @dynamicMemberLookup. However, if I try chaining interpolated string like:

let call = JSCall.document.getElementById('\(id)').value

I am getting compiler error like Cannot call value of non-function type 'JSCall'.

I thought conforming to ExpressibleByStringInterpolation would solve the problem, but it didn't.

Is there any way to achieve natural syntax of chaining method calls in this enum ?

英文:

I have the following enum:

@dynamicMemberLookup
enum JSCall: ExpressibleByStringInterpolation {
    case getElement(String)
    case getInnerHTML(id: String)
    case removeAttribute(id: String, attributeName: String)
    case customScript(String)
    case document
    indirect case chainedCall(JSCall, String)

    var value: String {
        switch self {
        case .document:
            return "document"
        case .getElement(let id):
            return JSCall.chainedCall(.document, "getElementById('\(id)')").value
        case .getInnerHTML(let id):
            return JSCall.getElement(id).innerHTML.value // Using dynamic member lookup here
        case .removeAttribute(let id, let attribute):
            return JSCall.chainedCall(.getElement(id), "removeAttribute('\(attribute)')").value
        case .chainedCall(let prefixJs, let suffix):
            return "\(prefixJs.scriptString)\(STRING_DOT)\(suffix)"
        case .customScript(let script):
            return script
        }
    }

    init(stringLiteral value: String) {
        self = .customScript(value)
    }
    
    subscript(dynamicMember suffix: String) -> JSCall {
        return .chainedCall(.customScript(self.value), suffix)
    }
}

Notice that in computed property value, for getInnerHTML case, I could use a natural syntax for chaining .innerHTML via dot syntax, thanks to @dynamicMemberLookup. However, if I try chaining interpolated string like:

let call = JSCall.document.getElementById('\(id)').value

I am getting compiler error like Cannot call value of non-function type 'JSCall'.

I thought conforming to ExpressibleByStringInterpolation would solve the problem, but it didn't.

Is there any way to achieve natural syntax of chaining method calls in this enum ?

答案1

得分: 0

dynamicMemberLookup 不能处理函数调用。为了能够调用查找的成员,你可以使用 @dynamicCallable。你也可以使用 callAsFunction 来实现类似的功能。

我认为更简单的建模方式是只有更多的 "fundamental" 情况,建模 JS 语法树。

@dynamicMemberLookup
@dynamicCallable
enum JSCall {
    case stringLiteral(String)
    case integerLiteral(Int)
    // 如果需要,可以添加更多的字面值情况
    case call(String, args: [JSCall])
    case property(String)
    indirect case chain(JSCall, JSCall)

    var value: String {
        switch self {
        case let .call(functionName, args):
            let argList = args.map(\.value)
                .joined(separator: ", ")
            return "\(functionName)(\(argList))"
        case .property(let name):
            return name
        case .chain(let first, let second):
            return "\(first.value).\(second.value)"
        case .stringLiteral(let s):
            return "'\(s)'" // 请注意,这不会转义 JS 字符串中的引号!
        case .integerLiteral(let i):
            return "\(i)"
        }
    }

    // ...
}

当你动态查找成员时,创建一个 chain,第二个元素是一个 property。这对应于像 foo.bar 这样的情况:

subscript(dynamicMember propertyName: String) -> JSCall {
    return .chain(self, .property(propertyName))
}

然后,在像 foo.bar("bar") 这样的情况下,你会隐式调用 (foo.bar).dynamicallyCall(withArguments: [.stringLiteral("baz")])。这应该返回一个 .chain(foo, .call("bar", [.stringLiteral("baz")]))

func dynamicallyCall(withArguments args: JSCall...) -> JSCall {
    switch self {
    case .property(let name):
        return .call(name, args: args)
    case .chain(let first, let second):
        // 注意这里的递归调用:
        return .chain(first, second.dynamicallyCall(withArguments: args))
    default:
        fatalError("无法调用此函数!")
    }
}

你可以为字面值情况遵守 ExpressibleByXXXLiteral

extension JSCall: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral {
    init(stringLiteral value: String) {
        self = .stringLiteral(value)
    }
    
    init(integerLiteral value: Int) {
        self = .integerLiteral(value)
    }
}

这就是所有的基础架构。现在,你可以添加一些方便的静态工厂和属性:

static let document = JSCall.property("document")

static func getElement(_ id: String) -> JSCall {
    document.getElementById(.stringLiteral(id))
}

static func getInnerHTML(_ id: String) -> JSCall {
    getElement(id).innerHTML
}

static func removeAttribute(id: String, attributeName: String) -> JSCall {
    getElement(id).removeAttribute(.stringLiteral(attributeName))
}

用法示例:

let one = JSCall.document.getElementById("foo").value
let two = JSCall.removeAttribute(id: "bar", attributeName: "attr").somethingElse.value
print(one) // document.getElementById('foo')
print(two) // document.getElementById('bar').removeAttribute('attr').somethingElse
英文:

dynamicMemberLookup doesn't handle function calls. To be able to call a looked-up member, you can use @dynamicCallable. You can do something similar with callAsFunction too.

I think a simpler way to model this is to just have more "fundamental" cases, modelling the JS syntax tree.

@dynamicMemberLookup
@dynamicCallable
enum JSCall {
    case stringLiteral(String)
    case integerLiteral(Int)
    // add more literal cases if you like
    case call(String, args: [JSCall])
    case property(String)
    indirect case chain(JSCall, JSCall)

    var value: String {
        switch self {
        case let .call(functionName, args):
            let argList = args.map(\.value)
                .joined(separator: ", ")
            return "\(functionName)(\(argList))"
        case .property(let name):
            return name
        case .chain(let first, let second):
            return "\(first.value).\(second.value)"
        case .stringLiteral(let s):
            return "'\(s)'" // note that this does not escape quotes in the JS string!
        case .integerLiteral(let i):
            return "\(i)"
        }
    }

    // ...
}

When you look up a member dynamically, create a chain with the second thing being a property. This corresponds to cases like foo.bar

subscript(dynamicMember propertyName: String) -> JSCall {
    return .chain(self, .property(propertyName))
}

Then, in cases like foo.bar("bar"), you would implicitly invoke (foo.bar).dynamicallyCall(withArguments: [.stringLiteral("baz")]). This should return a .chain(foo, .call("bar", [.stringLiteral("baz")])).

func dynamicallyCall(withArguments args: JSCall...) -> JSCall {
    switch self {
    case .property(let name):
        return .call(name, args: args)
    case .chain(let first, let second):
        // note the recursive call here:
        return .chain(first, second.dynamicallyCall(withArguments: args))
    default:
        fatalError("Cannot call this!")
    }
}

You can conform to ExpressibleByXXXLiteral for the literal cases:

extension JSCall: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral {
    init(stringLiteral value: String) {
        self = .stringLiteral(value)
    }
    
    init(integerLiteral value: Int) {
        self = .integerLiteral(value)
    }
}

That's all the infrastructure. Now you can add some convenient static factories and properties:

static let document = JSCall.property("document")

static func getElement(_ id: String) -> JSCall {
    document.getElementById(.stringLiteral(id))
}

static func getInnerHTML(_ id: String) -> JSCall {
    getElement(id).innerHTML
}

static func removeAttribute(id: String, attributeName: String) -> JSCall {
    getElement(id).removeAttribute(.stringLiteral(attributeName))
}

Usage:

let one = JSCall.document.getElementById("foo").value
let two = JSCall.removeAttribute(id: "bar", attributeName: "attr").somethingElse.value
print(one) // document.getElementById('foo')
print(two) // document.getElementById('bar').removeAttribute('attr').somethingElse

huangapple
  • 本文由 发表于 2023年7月13日 14:55:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/76676664.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定