Skip to content

Refactor OpBuilder to improve usability#74

Merged
ctrueden merged 33 commits into
mainfrom
scijava/scijava-ops-api/op-builder-refactor
Apr 14, 2023
Merged

Refactor OpBuilder to improve usability#74
ctrueden merged 33 commits into
mainfrom
scijava/scijava-ops-api/op-builder-refactor

Conversation

@hinerm

@hinerm hinerm commented Mar 14, 2023

Copy link
Copy Markdown
Member

The main API changes include:

  • A new arityN() step in the builder, after specifying Op name but before input value/type(s). This avoids the arity * inputMethods method proliferation.
  • The matchInplace static utility methods now include two digits, indicating both the arity and the mutated input. This avoids having to try and count the desired number of total inputs when selecting between overloaded mutateInplace1 methods.

All public API should now have javadoc. It should be fairly consistent across similar methods. Some particular areas of attention:

  • The intro and examples
  • How hints overloads are javadoc'd
  • How inplace and mutate are documented: non-inplace/mutate methods just @see to the first arity, while each individual inplace/mutate method @see's to the complementary arity mutate/inplace. Alternatively we could link to ALL the other arities in one or both of these location(s). Or we could make a single dump of all methods in the class javadoc... but given the nature of the builders I think the method javadoc is much more visible.

Other thoughts:
I was considering expanding the [I/O][T/V/U] acronyms in some abbreviated way. I initially found them confusing and didn't like the "single-letter differentiation". After looking at the builder for a while now I'm pretty used to them, and hopefully the javadoc is clear what decision you're making at each point. So I decided to just define the acronyms in the class javadoc.

Currently a draft because of the API changes. @gselzer @ctrueden if you're ok with these changes I will update the builder uses throughout the incubator. No rush; just let me know!

@hinerm hinerm requested review from ctrueden and gselzer March 14, 2023 18:34
@ctrueden ctrueden force-pushed the scijava/scijava-ops-api/op-builder-refactor branch from 53ae8a6 to f77076b Compare March 15, 2023 02:54

@ctrueden ctrueden left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hinerm Thanks a lot for working on this! I wrote my thoughts as I went through the commits. Aside from tweaks to javadoc in various places, my big question is whether we should collapse the op(String) and arityX() calls into one layer. A secondary implied question is whether to use the terms nullary, unary, and binary instead of arity0, arity1, and arity2 for those methods.

P.S. I force-pushed the branch to have one more leading commit, which I've already pushed to main: an update to pom-scijava 34.1.0. This eliminates 12500 lines of build log from the repeatedly echoed active profiles.

Comment thread scijava/scijava-ops-api/src/main/java/org/scijava/ops/api/OpBuilder.java Outdated
Comment thread scijava/scijava-ops-api/src/main/java/org/scijava/ops/api/OpBuilder.java Outdated
Comment thread scijava/scijava-ops-api/src/main/java/org/scijava/ops/api/OpBuilder.java Outdated
Comment thread scijava/scijava-ops-api/src/main/java/org/scijava/ops/api/OpBuilder.java Outdated
Comment thread scijava/scijava-ops-api/src/main/java/org/scijava/ops/api/OpBuilder.java Outdated
Comment thread scijava/scijava-ops-api/templates/main/java/org/scijava/ops/api/OpBuilder.vm Outdated
Comment thread scijava/scijava-ops-api/templates/main/java/org/scijava/ops/api/OpBuilder.vm Outdated
Comment thread scijava/scijava-ops-api/templates/main/java/org/scijava/ops/api/OpBuilder.vm Outdated
Comment thread scijava/scijava-ops-api/templates/main/java/org/scijava/ops/api/OpBuilder.vm Outdated
@gselzer

gselzer commented Mar 21, 2023

Copy link
Copy Markdown
Member

@hinerm thank you for making these changes.

I really appreciate the work you did on the javadoc. That was needed.

I still do not see the value in adding these arityX methods to the OpBuilder syntax. While the implementation seems fine, I still believe that designing to improve auto-complete is misguided. is If I'm outnumbered, then so be it, but I think that syntax like this is a step backward.

I feel like for a long time I've been saying that I don't see the value in autocomplete in Ops, hence not bothering to fix it, so maybe I'll try to spill out my thinking below:

My intuition says that if you know a function that does what you want to do, then you should just call that function directly. I see Ops as a way to help people who don't know what function to call. By using Ops to call a specific function, you'll take a performance hit due to matching, parameter conversion, etc.

So, if you're using Ops to find some unknown function matching your needs, I only see autocomplete as being useful if it can tell you:

  1. What Ops are in the environment.
  2. How to call any given Op in the environment (i.e number of parameters and types of each).

The OpBuilder syntax can't do either of these things.

With that said, what information do I gain from autocomplete? The only thing that I can think of is that you can see that for the inputs method you need to pass Objects, while for the inTypes method you need to pass all Classes or all Nils. I admit this is cleaner with your syntax, but I'd argue that it should also be clear from the method names. If it's not, we need to change the method names or people will have trouble with Ops when they don't have autocomplete.

Again using my intuition above, I really envision Ops being most useful when you don't have these smart IDE features telling you about the functions in your environment, like with PyImageJ or in Fiji's Script Editor. The OpBuilder is a solution to this problem, promising you that it will try its best to find a function you can use to fulfill your task. Writing op(...).input(...)... every time is really elegant, and aligns well with (at least my) natural thought processes. Adding arityX makes me consider how many inputs I'll be providing before I enumerate them in the Op call, which increases my mental burden.

At the end of the day I'm probably bikeshedding, hence my approval if you guys really like this change, but I still think that we are focusing on the wrong place here.

@hinerm

hinerm commented Mar 21, 2023

Copy link
Copy Markdown
Member Author

@gselzer

I see Ops as a way to help people who don't know what function to call

Ah interesting. Amusingly this is not the same as how I see Ops. 😆 So not surprising that there are different opinions here.

I would expect Ops to be used when you know the algorithm you want, but don't know what the inputs will be (and/or want to future-proof for unknown future inputs). The cost of time and knowledge complexity is paid for by the fact that core code (calling the algorithms) doesn't have to change for future use-cases to evolve.

Based on past discussions, I don't know that the OpBuilder will actually have long-term use. I think users will want some GUI built on top of some improved Help functionality. Developers will use python so they have namespaces.

I really envision Ops being most useful when you don't have these smart IDE features telling you about the functions in your environment, like with PyImageJ or in Fiji's Script Editor

I think these use cases will be great and will use the enhanced pythonic dynamic entry points - but those things are in some obfuscated future. I don't think they will use the OpBuilder. But right now I see the OpBuilder as "the way to use Ops", and at the very least as the entry point for new developers to show what Ops can do and build buy-in.

So with that in mind, the OpBuilder is essentially a series of decisions. More decisions/choices = more cognitive load. Combining arity and input type into one step gives you a number of choices equal to the product of each individually, whereas splitting them into two steps turns the total choices into a sum.

I don't want to portray this as "just for auto-complete," although I know that's how I first brought it up in this issue. Auto-complete just illustrated how many things the user has to choose between. The reality was, when I first was using the OpBuilder, it was in an IDE and I couldn't actually tell there were 3 different input choices (class/Nil/value) and I don't want other developers to bounce off things like this.

Adding arityX makes me consider how many inputs I'll be providing before I enumerate them in the Op call, which increases my mental burden.

This is totally valid. I feel the same way. I don't like having a step to select arity. I don't want to think about arity at all. I want an Ops.run(blah) to just give it my stuff and have it do the right thing. This is exactly why I have reservations about ops.unary("math.pow"), ops.binary("math.add"), etc... but it seems like the cost of type-safety.

Different developers will probably find one option fits them better than the other. Having 2 input steps feels more comfortable to me, but ultimately if the OpBuilder sticks around I think we should show both options to a test group and just use whatever is the least offensive.

@ctrueden

Copy link
Copy Markdown
Member

@gselzer wrote:

My intuition says that if you know a function that does what you want to do, then you should just call that function directly.

The downside to calling a function directly is that you lose the extensibility provided by Ops. You will no longer gain potential performance benefits for more specialized algorithm implementations. I agree with @hinerm's point here:

I would expect Ops to be used when you know the algorithm you want, but don't know what the inputs will be (and/or want to future-proof for unknown future inputs).

With emphasis on the "want to future-proof for unknown future inputs" and especially keeping the door open for more performance-optimized implementations, as highlighted in Ops design goal 3: "Very fast".

@hinerm wrote:

Based on past discussions, I don't know that the OpBuilder will actually have long-term use. I think users will want some GUI built on top of some improved Help functionality. Developers will use Python so they have namespaces.

Here I disagree; I think the niche of the op builder is for developers programming in Java, who want type safety. There will continue to be some people programming in Java.

Combining arity and input type into one step gives you a number of choices equal to the product of each individually, whereas splitting them into two steps turns the total choices into a sum.

Indeed. I think this is a nice way to summarize the benefit of splitting out the arity layer.

The reality was, when I first was using the OpBuilder, it was in an IDE and I couldn't actually tell there were 3 different input choices (class/Nil/value) and I don't want other developers to bounce off things like this.

☝️ To me, this is the most crucial point. @gselzer and I cannot use the builder from a new developer's perspective, whereas @hinerm had that window of opportunity, and observed a point of confusion, and identified a way to relieve it. Therefore, I accept that the change is valuable.

@hinerm hinerm force-pushed the scijava/scijava-ops-api/op-builder-refactor branch from 2eb63d7 to f1bea64 Compare March 31, 2023 18:56
@hinerm hinerm force-pushed the scijava/scijava-ops-api/op-builder-refactor branch 3 times, most recently from a635409 to d88f779 Compare April 13, 2023 14:29
hinerm added 19 commits April 13, 2023 09:30
Instead of having a single choice for input/inType for all 16 arities (a
total of 49 methods), we make the first choice arity 0-16 and then
narrow to the 3 input/inType choices.

Although this makes more method calls in the builder chain, the hope is
to avoid overwhelming developers with large numbers of similar method
calls.
This only affects static utility methods and makes it so that each is
named uniquely, instead of having 16 "matchInplace1" methods.
If the full line is conditional, the #end should appear on the following
line (so the conditional line includes a newline character).

If only the final part of the line is conditional, so that the #end
appears at the end of the line, a standalone line-break must appear
after the #end. This looks unintuitive in the template, but is the only
way I could find to avoid Velocity eating the newline otherwise.

Inline conditionals that do not appear at the end of the line are
unaffected, as the newline character is not enclosed in the if..else.
This is clearer that a match is happening when this method is called.
Specifically the generic parameter of the given Nil
These steps should all now:
* have @return statement javadoc
* state that this function is performing a match
The changed functions are intermediate builder steps and do not actually
perform a match at this point.
All the "@see" statements should now have explanations as to why one
would use these other methods in place of the current method.
These locations that used to have a terminal #end no longer do, with the
addition of "@see" explanations.
hinerm added 14 commits April 13, 2023 09:30
These Functions will return Objects since the output is unknown
Adding descriptions to @see tags, by default, makes that the link text -
which looks awful and confusing.

This changes @see's to explicitly use <a> tags with our descriptions as
hover text.
Specifying the Hints when initially creating an OpBuilder provides the
potential for simplification when reusing that builder, while also
cutting the number of terminal steps in half (since they do not need to
be overloaded with ...Hints signatures)
Since this is the entry point for actually using the builder it makes
more sense to document there than on the class.
Specifying the "arity1, 2, 3, etc..." part of the builder is a pain if
you are more familiar with ops. By adding convenience methods with
common "arity" names (e.g. "unary", "binary", etc..) bypasses the need
to make this extra call within the builder process.

Note that this is not perfect, as changing the number of arguments will
still necesitate a change to the "arity" portion of the builder call.
But this is a compromise.

Note that the moving of "Hints" to OpEnvironment coupled with this
change now means there are 32 new signatures.
@hinerm hinerm marked this pull request as ready for review April 14, 2023 17:25
@hinerm

hinerm commented Apr 14, 2023

Copy link
Copy Markdown
Member Author

@ctrueden @gselzer alright I think I addressed all your feedback. Let me know if there's anything else you'd like to see! Thanks!

@ctrueden

Copy link
Copy Markdown
Member

Thanks again for all your work on this, @hinerm!

Earlier, we had discussed potentially compacting the op(...) and arityX() calls into one step. Did we ultimately decide against that? Because I still think it would be slick to do that:

ops.arity3("logic.ternary").input(condition, trueValue, falseValue).apply()
ops.binary("math.add", hints).input(img1, img2).output(result).compute()

etc.

What is the benefit of keeping the op(...) step separate? One reason was that it provides a central place to pass the Hints if you have some... anything else? We could also pass hints like:

ops.binary("math.add").hints(hints).input(img1, img2).output(result).compute()

so that the OpEnvironment's unary/binary/arity3/etc. methods don't get doubled signature-wise.

@hinerm If you like the idea of collapsing the op and arityX methods into one level, but are sick of doing work on this branch, then I volunteer to do it, since it would make me happier.

Alternately: we could loop in @tpietzsch and @tinevez next week to see which form they find the best.

What do you think?

@hinerm

hinerm commented Apr 14, 2023

Copy link
Copy Markdown
Member Author

Earlier, we had discussed potentially compacting the op(...) and arityX() calls into one step. Did we ultimately decide against that?

I put them in the OpEnvironment

@ctrueden ctrueden left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

@ctrueden ctrueden merged commit bbc6b20 into main Apr 14, 2023
@ctrueden ctrueden deleted the scijava/scijava-ops-api/op-builder-refactor branch April 14, 2023 18:29
@ctrueden

Copy link
Copy Markdown
Member

I put them in the OpEnvironment

Awesome, sorry I missed that! I saw that the builder usages across the codebase were still using the long-form op(...).arityX() style, so didn't realize it was included.

@hinerm

hinerm commented Apr 14, 2023

Copy link
Copy Markdown
Member Author

One reason was that it provides a central place to pass the Hints if you have some... anything else? We could also pass hints like:

I think we might want to do another separate pass on Hints.. I'm not confident in how they're used right now. Maybe we can/should discuss in another issue...

@hinerm

hinerm commented Apr 14, 2023

Copy link
Copy Markdown
Member Author

Awesome, sorry I missed that! I saw that the builder usages across the codebase were still using the long-form op(...).arityX() style, so didn't realize it was included.

yeah I did the changes in a dumb order 🙄

@gselzer gselzer mentioned this pull request Jan 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants