-
Notifications
You must be signed in to change notification settings - Fork 23
Relaxed field order preservation requirement for ObjectInput #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| } | ||
| } | ||
|
|
||
| def read(): T = if (input.hasNext) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: can we have if and else at the same level of indentation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At a price of one more level of indentation, but whatever.
| } | ||
| def readCase(caseNameField: FieldInput): T = { | ||
| val caseName = readCaseName(caseNameField) | ||
| caseIndexByName(caseName) match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2 unrelated questions about caseIndexByName:
- Why can't we just use
.indexOf(element, index)? - Does it make sense to use a more lookup-friendly data structure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
.indexOf(element, index)would put the underlyingArraythrough implicit conversion toWrappedArray. I wanted to be as raw as possible. -
I'm deliberately using
Arrayand linear search here (and also inFieldValues) because case classes tend to have small number of fields and sealed hierarchies tend to have small number of case classes. Using a map would probably introduce more overhead. Also, note that if we wanted to improve on time complexity then the most natural thing would be a binary search, not using a map.
| bound to any format. It only depends on the fact that this format is capable of serializing following types and structures: | ||
| * integer numbers up to at least 64-bit precision (i.e. `Long`) | ||
| * decimal numbers up to at least 64-bit precision (i.e. `Double`) | ||
| * `Char`s, `String`s, `Boolean`s and `null`s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we have both .nextField and .peekField, we need an example more complex than a single field class and less automatically handled than a case class. I've seen a lot of people struggle with creating 2-field class codec and it's just getting harder.
More generally, should I peek or should I next? It appears to me that GenCodec won't exactly be completely Input/Output indepedent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to write a custom codec that creates an object with two-three fields then the way to go is to implement apply/unapply in companion object or use fromApplyUnapplyProvider. This is now very clearly recommended in the docs.
You should almost never need to implement an object codec manually. If you really need to do it then the backend-independent way of doing it would be:
class Klass(val int: Int, val str: String)
object Klass {
private def getField(input: ObjectInput, name: String):
input.peekField(name).orElse(input.nextField().opt).filter(_.fieldName == name)
.getOrElse(throw new ReadFailure(s"expected field $name")
implicit val codec: GenCodec[Klass] = GenCodec.createNullableObject(
input => {
val int = getField(input, "int").readInt()
val str = getField(input, "str").readString()
new Klass(int, str)
},
(klass, output) => {
output.writeField("int").writeInt(klass.int)
output.writeField("str").writeString(klass.str)
}
)
}but really, this should be an absolute last resort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Input and Output interfaces are generally optimized for ease of implementation and performance rather than ease of use.
| } | ||
|
|
||
| final def readObject(input: ObjectInput): T = { | ||
| val oooFields = new FieldValues(oooFieldNames, oooDeps, typeRepr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: are there aaa fields as well? 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nooo
| def readCase(caseNameField: FieldInput): T = { | ||
| val caseName = readCaseName(caseNameField) | ||
| caseIndexByName(caseName) match { | ||
| case -1 => unknownCase(caseName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These barbaric checks could be avoided if caseIndexByName would just return Opt[GenCodec[_: T]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but I can't similarly change caseIndexByValue because in writeObject I need to access both caseNames and caseDeps arrays. So I have to work with indices anyway.
See updated
GenCodecdocs andObjectInputscaladocs for more details.TLDR: instead of preserving field order,
ObjectInputmay alternatively overridepeekFieldand provide random field access by field name. This will allow formats using hashtable-based object representation (e.g. native JS representation) to be fully compatible withGenCodec- this currently only affects flat sealed hierarchy encoding and possible custom-written codecs.