question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Binding extension functions are confusing

See original GitHub issue

Let me preface this by saying that this is a discussion whether this issue exists, and if it confirmed, I can submit a pull request for it. I’m just creating this issue to discuss and confirm the issue.

Utility functions that help create bindings, such as:

  • stringBinding
  • objectBinding
  • booleanBinding
  • doubleBinding
  • floatBinding

could be simplifed.

For every type, there is a pair of functions:

  • The extension function, e.g. ObservableValue<T>.stringBinding(vararg dependencies: Observable, op: (T?) -> String?)
  • The non-extension function, e.g. stringBinding(receiver: T, vararg dependencies: Observable, op: T.() -> String?)

What’s confusing is the parameter of receiver on the non-extension function.

As seen from it’s body:

Bindings.createStringBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))

the receiver ends up in the same observable array as dependencies, and the only thing that differentiates it from the dependencies is that is used in the Callable. Seems reasonable to split them up, right? Well, this ends up really ugly:

val prop1 = SimpleDoubleProperty()
val prop2 = SimpleDoubleProperty()
val prop3 = SimpleDoubleProperty()

val product = doubleBinding(prop1, prop2, prop3) {
    val value1 = this.value
    val value2 = prop2.value
    val value3 = prop3.value
    value1 * value2 * value3
}

Why does receiver receive the “special” treatment? And by the “special treatment”, I mean “no treatment at all”, because it’s just passing the whole observable into the lambda, and you have to extract the value by using this.value, same as using prop1.value. In my opinion, this split between receiver and dependencies is redundant, for these reasons:

  1. It creates confusion when using the function. When seeing the sigunature stringBinding(receiver: ..., dependencies: ..., op: ...), you can be confused about what is the importance of the receiver, how is it different from the dependencies, and why does op use this receiver, but not the dependencies. Then you have to read the source code to find out the difference, just to realize that there is essentially no difference between them.
  2. It is redundant when the extension function exists. In the case of extension function, you explicitly differentiate the receiver by making it a receiver of an extension function, and in that way, you’re implicitly stating that the receiver is more important than other dependencies. Also, the Callable contains op(this.value) instead of receiver.op(), which means that the parameter of lambda is the actual value of the observable, and not the observable itself (that makes much more sense).

My proposal

  1. Regarding the non-extension function, merge the receiver and dependencies and provide no parameters within the lambda. Users can access all the properties directly by referring to observable variables outside the lambda. That way, all dependencies are equal, and the API is simpler.
  2. Regarding the extension function, remove dependencies from the signature. Why? Because, if you called it as an extension function, you’re implicitly stating that you’re making a “mapping” between the receiver and the resulting value, and that that mapping is relying only on the receiver to be calculated. If, on the other hand, you want to have multiple dependencies in your binding, use the non-extension function instead, because all dependencies are equal and there shouldn’t be one (extension function receiver) that is different than others.

Before & After

While the resulting API is pretty similar in practice, the change it is definitely breaking the compatibility with previous versions.

Before:

fun <T> ObservableValue<T>.stringBinding(vararg dependencies: Observable, op: (T?) -> String?): StringBinding
        = Bindings.createStringBinding(Callable { op(value) }, this, *dependencies)

fun <T : Any> stringBinding(receiver: T, vararg dependencies: Observable, op: T.() -> String?): StringBinding =
        Bindings.createStringBinding(Callable { receiver.op() }, *createObservableArray(receiver, *dependencies))

After:

    fun <T> ObservableValue<T>.stringBinding(op: (T) -> String?): StringBinding
            = Bindings.createStringBinding({ op(value) }, this)

    fun stringBinding(vararg dependencies: Observable, op: () -> String?): StringBinding =
        Bindings.createStringBinding(op, *dependencies)

The change is most visible when reading the parameters of the function, but here’s a (simplified) example usage:

    val prop1 = SimpleDoubleProperty()
    val prop2 = SimpleDoubleProperty()
    val prop3 = SimpleDoubleProperty()

    val product = doubleBinding(prop1, prop2, prop3) {
        val value1 = prop1.value
        val value2 = prop2.value
        val value3 = prop3.value
        value1 * value2 * value3
    }

    val prop1Squared = prop1.doubleBinding { it.pow(2) }

P.S. and while we’re at it, I might as well add some documentation to the functions.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:6 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
SKeeneCodecommented, Dec 15, 2021

I think it’s a good idea to include the extra extensions because it keeps the API consistent between ObservableValue and ObservableList. Keeping the experience consistent for the programmer is probably more important than concerns of code bloat. Besides, tornadofx has always been pretty generous with helper extension functions across the codebase.

The real bloat comes from packaging things like a restful API or the JSON part with tornadofx when they really should be their own optional modules. ctadlock was working on this here. Thinking about finding a spare week at some point and doing it myself…

0reactions
aleksandar-stefanoviccommented, Jan 3, 2022

@pavlus

I understand what you’re saying. There certainly are cases where the previous API worked better. However, we must either compromise on API clarity with the previous implementation or compromise on code conciseness with the current version. I’m honestly not sure which one I like best, but I can definitely say that the previous API looked like a “hack” or a “shortcut” to achieve code conciseness (which is not a bad thing by itself, but should be considered). Maybe we could come up with something better, that is the middle-ground between these two implementations?

It crossed my mind that we could do something kind of dirty implementation-wise, but elegant usage-wise. The idea was to utilize list destructuring to expose values within the lambda, like so:

fun integerBinding(vararg dependencies: Observable, op: (values: List<Any>) -> Int): IntegerBinding
        = Bindings.createIntegerBinding(Callable { op(dependencies.map { it.value }) }, *dependencies)

(note that this code will not compile, it’s only for demonstration purposes)

In the ideal world, this implementation would allow something like this:

label(stringBinding(foo, bar, interestingMessageTemplateProperty) { (foo, bar, template) -> template.format(foo, bar)})

However, this code is impossible, because there is no way for the compiler to deduce the types of the values of the dependencies — it wouldn’t be able to deduce that the template is of String type, etc.

So, I thought that we could maybe substitute the generalized vararg with specific implementations, up to 5 elements (more than 5 elements and it uses the vararg version instead), and this would, for example, be the 3-parameter variant:

fun <T1, T2, T3> integerBinding(
    dep1: ObservableValue<T1>,
    dep2: ObservableValue<T2>,
    dep3: ObservableValue<T3>,
    op: (val1: T1, val2: T2, val3: T3) -> Int
): IntegerBinding
        = Bindings.createIntegerBinding(Callable { op(dep1.value, dep2.value, dep3.value) }, dep1, dep2, dep3)

This would allow calls like the one that was impossible above.

HOWEVER this has two glaring issues:

  • It would cause an explosion in the lines of code: for every type of binding, there would have to 7 functions (instead of current 2)
  • It would cause issues with objects that are Observable, but are not ObservableValue (the current implementation supports Observables as well, and by issues I mean that it would revert to the vararg variant, that doesn’t expose the values in the lambda, which can cause API usage confusion, that we tried to prevent in the first place.

Not to draw attention away from the original discussion, we definitely need to figure out which version is generally better, however, I see this as a moot point, unless someone with higher authority (like @edvin) makes a decision. Of course, Edvin wrote (or at least ratified) the original versions, so it would seem that the original is the way to go, but maybe Edvin didn’t think about the arguments that I’ve provided in the issue, and would agree that the implementation could be like the one proposed.

Read more comments on GitHub >

github_iconTop Results From Across the Web

View Binding extension function - android - Stack Overflow
For the custom views, it works fine, but for the adapters, I need the boolean in the invoke to be false. How could...
Read more >
CosmosDBTrigger binding extension is not registered on my ...
Overall, I'm confused as to whether the CosmosDB binding extension should be installed automatically when deploying my function from VS with ...
Read more >
Dagger Party Tricks: Extension Functions - Zac Sweers
Sometimes it's a nuisance, but extension functions are way that Dagger code actually becomes simpler in Kotlin. Consider this Java code: @Binds ......
Read more >
I m confused about when to use extension functions Take this
thisen: In this case it looks like utility methods that are just simple helpers for other methods in the class are implemented as...
Read more >
Extension Functions - Ignition User Manual 8.1
Changing the docstring could be misleading or confusing as you'd lose the documentation for how your implementation of the function should work.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found