Prototype code on what might look like to remove the AttributeValue from the API#1500
Prototype code on what might look like to remove the AttributeValue from the API#1500jkwatson wants to merge 7 commits intoopen-telemetry:masterfrom
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1500 +/- ##
============================================
- Coverage 91.45% 86.21% -5.25%
Complexity 949 949
============================================
Files 116 120 +4
Lines 3405 3612 +207
Branches 281 292 +11
============================================
Hits 3114 3114
- Misses 204 411 +207
Partials 87 87
Continue to review full report at Codecov.
|
Not sure whether you want feedback or not ;) But if you do, maybe you can temporarily copy in the |
I would love feedback/thoughts/ideas/etc. I added the Main class/method. |
| public void consume(String key, AttributeValue.Type type, Object value) { | ||
| switch (type) { | ||
| case STRING: | ||
| String stringValue = attributes.getStringValue(value); |
There was a problem hiding this comment.
This could just be (String) value, there doesn't seem to be much value to delegate the cast.
There was a problem hiding this comment.
yeah, I have gone back and forth on this one. I agree with you, but I also like having the option for the non-obvious ones (like the "array" values that are actually Lists). You can definitely just have the consumer do the casts manually, for sure.
There was a problem hiding this comment.
Can you clarify "non-obvious"? I guess just that it looks a little gross (which I agree with but not enough to make the indirect API I think)
List<String> stringArrayValue = (List<String>) attributes.getStringArrayValue(value);
That being said, I think requiring casts does have the downside of requiring the suppresswarnings "unchecked" too if those are enabled. I guess if we provide a helper whose purpose is a cast, we could make it static instead of going through the attributes instance.
There was a problem hiding this comment.
getStringArrayValue implies String[] not List<String>
There was a problem hiding this comment.
How about String AttributeValue.Type.STRING.cast(Object value) and similar for other types? That will clean up the API of Attributes
There was a problem hiding this comment.
excellent. something like this has been tickling the back of my brain, but I couldn't quite put my finger on it. I will try this out and add this API to this PR to see how it feels/looks.
There was a problem hiding this comment.
hmm. I'm not sure how you get a handle to call those methods. They don't appear to be visible to be called anywhere. Java doesn't let you add static members to enum instances, either, unfortunately.
There was a problem hiding this comment.
yeah, you can't call methods like that on enum values. See here for the JEP to make it possible: http://openjdk.java.net/jeps/301
There was a problem hiding this comment.
I moved the methods to do the casts into a new AttributeType class, which just moves the issue to a different place. I haven't been able to figure out a way to make it so you can only call the right one, yet.
There was a problem hiding this comment.
I also added an 8-way typed consumer interface, just to see how that worked out. With the requirement for a consumer to do the casts with the single method, 8 methods doesn't look too bad. But, also doesn't have the advantage of being a SMI for implementation via a lambda.
| Double doubleValue = attributes.getDoubleValue(value); | ||
| System.out.println("doubleValue = " + doubleValue); | ||
| break; | ||
| case STRING_ARRAY: |
There was a problem hiding this comment.
It's too bad there's no way to do instanceof List<String>, Java 15 would look quite nice
if (value instanceof String) {
String foo = value; // Automatically cast;
} else if (value instanceof List<String>) // Unfortunately can't :(There was a problem hiding this comment.
agreed. or kotlin. :)
| /** Iterates over all the key-value pairs of attributes contained by this instance. */ | ||
| void forEach(AttributeConsumer consumer); | ||
|
|
||
| Boolean getBooleanValue(Object value); |
There was a problem hiding this comment.
Mentioned it below, but these seem to not provide much value. But boolean getBoolean(String key), now that sounds nice :)
There was a problem hiding this comment.
I think we should demand to the users the type retrieval. This way, we do not have to define what should happen if we call getBoolean(key) on an attribute that was stored as long, for example.
There was a problem hiding this comment.
I think such methods are exactly demanding users to do the type retrieval - they are explicitly picking the method to call. But it gives us the ability to provide a UX possibly better for users. If we throw an exception, it's no different than any other option. But we could also treat it as an empty value if it makes sense - I'd lean towards this given we generally don't want to crash user's apps. With it in the javadoc it's pretty straightforward.
There was a problem hiding this comment.
Yes, this is probably the use-case that seems most beneficial with having these extra helper methods. We can do some intelligent default behavior when the wrong one is called, and make that uniform and documented. And, if people don't want to use them, they can do the cast themselves and handle the bad conditions however they desire.
This is one of the main values of having the typed-keys..having a type-safe API for retrieval, because then you can't ask for a boolean value from a String-typed key.
There was a problem hiding this comment.
If we don't use primitive types and we return null value in case of the wrong type, I agree. It would make a better UX for users.
There was a problem hiding this comment.
One option I did consider was to have the API not give the consumer the Object value, but require them to make a get call based on the type and the value, which could enforce that the right type is passed in for the right method. This would necessitate, probably, doing some optimization of the get code path, which currently just traverses the entire list to find the key. I wasn't sure how exactly to do that. One option would be to provide an Iterator that kept the index to the currently location in the list, rather than doing it via the forEach and an consumer.
There was a problem hiding this comment.
I agree with Anuraag that boolean getBoolean(String key) etc would make a nice API for single key retrieval.
There was a problem hiding this comment.
easy to add, but wouldn't help with the iteration use-case, which I think will be the most common one, unfortunately.
anuraaga
left a comment
There was a problem hiding this comment.
Make the AttributeConsumer interface have 8 separate methods on it to get the various typed values out, hence forcing readers to implement all 8 methods in order to read anything.
Realized when looking at the switch again, this option may not be so bad.
| public void consume(String key, AttributeValue.Type type, Object value) { | ||
| switch (type) { | ||
| case STRING: | ||
| String stringValue = attributes.getStringValue(value); |
There was a problem hiding this comment.
Can you clarify "non-obvious"? I guess just that it looks a little gross (which I agree with but not enough to make the indirect API I think)
List<String> stringArrayValue = (List<String>) attributes.getStringArrayValue(value);
That being said, I think requiring casts does have the downside of requiring the suppresswarnings "unchecked" too if those are enabled. I guess if we provide a helper whose purpose is a cast, we could make it static instead of going through the attributes instance.
|
|
||
| private static void processTyped(CleanAttributes attributes) { | ||
| attributes.forEach( | ||
| new TypedAttributeConsumer() { |
There was a problem hiding this comment.
I like this a bit better than the switch. No noise (asString, etc) just pure consumption
There was a problem hiding this comment.
me, too, although implementing a 8-method interface is not super ergonomic, either. Especially in the cases where you don't care about the types, and just want to gather raw data [if this is a use-case?]
There was a problem hiding this comment.
Easy :) Copy all attributes to another Attributes before you add your own attributes to it :)
My problem is the opposite: I cannot imagine when do you to iterate over all attributes in a type-safe manner.
There was a problem hiding this comment.
any exporter needs to do this. See all the exporters in the repo that have to do this work.
There was a problem hiding this comment.
I think that's great to have, no matter what, so yes, definitely.
| public void consume(String key, AttributeType type, Object value) { | ||
| switch (type) { | ||
| case STRING: | ||
| String stringValue = type.asString(value); |
There was a problem hiding this comment.
Not strong but I might find this a bit more readable
enum AttributeType {
STRING, BOOLEAN, FOO, CAT;
@SuppressWarnings("unchecked")
public <T> T cast(Object dog) {
return (T) dog;
}
case STRING:
String stringValue = STRING.cast(value); // Having the same STRING seems easy to readThere was a problem hiding this comment.
I still like my original proposal more :) This code and Anuraag's suggestion are equal to me.
There was a problem hiding this comment.
I don't know how to implement your proposal. Can you add a PR to this one to show me?
There was a problem hiding this comment.
@anuraaga it's more readable, for sure, but 100% not type-safe. The user can do the wrong thing trivially, which seems to defeat the purpose, to me. I have this locally, though, and will commit it. :)
There was a problem hiding this comment.
Is it easier to do the wrong thing with that vs type.asString? They still need to call the correct method in either case I think, but using the same STRING twice is more obvious I think for a reader of the code since there's symmetry there.
There was a problem hiding this comment.
Well, using the type capture, you could put any old value on the LHS and it would still compile (BigDecimal, SpanContext, you name it). So, with the type.as... methods, at least they only have a limited number of options?
Unfortunately, at this point we just discussing the least bad option, I'll admit. Java's type system just doesn't have the facilities to make a really good API, without going back to AttributeValue, as far as I can see.
I could probably get behind any of these options, TBH. I think the least-bad option is the 8-way interface, because at least in that case they're forced to consider all the possible types. We can also have the single one and require the manual casting along with that, so at least both options are available, depending on the use-case.
There was a problem hiding this comment.
+1 vote to 8-way interface (and optionally also single casting variant, but with toBuilder the latter becomes less important perhaps)
fce23c7 to
000b710
Compare
|
I've pruned this down to 2 options: 1) The 8-way typed consumer interface, and 2) the completely open consumer interface. I'd really like to make a decision on weather we want to go down this route. If we do, I'll prep a PR that converts the repo over to this. @bogdandrutu @carlosalberto Your opinions would be very useful here. |
anuraaga
left a comment
There was a problem hiding this comment.
My vote is to add both APIs, they LGTM
…ollection of strongly typed attributes.
… essentials. Also added a toBuilder to the CleanAttributes since it's been added to the Attributes class.
000b710 to
bfeee75
Compare
|
closing this, as I've now got an actual PR submitted that I think most of us like. |
The goal of this experiment is to figure out how to efficiently write & read strongly-typed attributes into and out of an immutable collection-like structure. This shouldn't be taken as a proposal, but just the result of my noodlings on how to you could build an API like this.
Notes:
of(...)constructs, since you would have no type-safety on the values, and the implementation would have to try to figure out how to divine the types and reject non-conforming values if we wanted to just haveof(String, Object)as the style. So, I just kept the builder, instead.Other approaches I considered (in no order...none of them are great):
Here is some sample code that utilizes the hacky API I built here: