It would be better to work hard on good abstractio...
# compiler
m
It would be better to work hard on good abstractions and throw all the codegen away.
m
But that's pretty annoying for serialization 🤔
m
It is annoying only with old way of thinking, when you want your objects to be traversed by reflection/codegen, breaking encapsulation etc.
m
Quite a misuse of Kotlin imo. This makes things way more complicated than necessary and doesn't look nor feel like idiomatic Kotlin anymore. And the moment I read reactive or data binding I'm out anyway. Both solve inconvenience problems by adding piles of complexity and upfront learning. Oh and excessive use of wrapper classes and generics put quite some pressure on memory usage, CPU and garbage collection.
m
What is not idiomatic here? Which code is just code, and which one abuses idioms? 🙂 Of course, it adds complexity, but, unlike reflection/codegen, it is absolutely transparent, with no magic. If you want to measure overhead of something, you must compare it to something other. There's no way to compare observable wrappers with something which does not provide observability. Immutable structs produce no observable wrappers, and will become even more cheaper when inline classes will grow mature. Generics have no impact on memory/CPU/GC because they are absent at runtime.
m
• cannot use data classes anymore and thus no destructuring for example • you lose the ability to use
is
and
as
as well as all smart-case benefits for instances and instance properties • it's quite confusing that if you set the type of something to
: Player
or
: User
you won't get an actual instance but merely a schema • plenty of operations are remapped/reinvited as - I don't even know how to call them - for example
CharSequencez.…
,
Objectz.…
- that's basically yelling at you "something is horribly wrong here" • high amount of boilerplate even for simple things • you lose some compile-time guarantees (what if I build an instance but a property is missing? likely fails at runtime) •
Copy code
override fun saveOrRestore(d: PropertyIo) {
        d x emailProp
        d x nameProp
        d x surnameProp
    }
or
Copy code
val buttonClickedProp = propertyOf(false).also {
        // reset flag and perform action — patch 'user' with values from memory
        it.clearEachAnd {
            user.transaction { t ->
                t.setFrom<User>(editableUser, User.Email + User.Name + User.Surname)
            }
        }
    }
How are these (and similar weird constructs) easier to understand and less magical than clean generated code which I can inspect, read and debug as if I've written it by myself, with standard Kotlin? I see that it's a clever solution, but it's not easy, not transparent, not without magic but complex instead. Regarding performance: • using a HashMap for every single object instance adds a lot of overhead already (you're basically creating a semi-dynamic type system here) • every single property is wrapped in a wrapper object, hidden behind an interface - +1 object allocation per property per object, +n virtual function calls for every single property access while probably killing all possibilities JVM-internal property access caching • because generics are used, every single primitive value is boxed - ~+1 object allocation per primitive property value • because generics are used, every single inline class instance is boxed - +1 object allocation per inline class property value That's just a quick first shot without going deeper. But by far enough reasons for me not to.
m
Thanks for the big review!
cannot use data classes
yep, hashCode/equals/toString are implemented, destructuring and copy are not, you're right
you lose the ability to use
is
and
as
These operators are evil should never exist. But you're still can check struct.schema == anotherSchema
it's quite confusing
Well, a habit comes with time. Also, it's quite confusing when you have no control over your class, as it happens with reflection/codegen.
that's basically yelling at you "something is horribly wrong here"
They're basically yelling "Kotlin cannot deduplicate method references, Android will suck at loading many classes"
high amount of boilerplate
comparing to...
you lose some compile-time guarantees
You also don't have them when your DTO schema cannot be mapped to actual JSON/SQLite/Prefs.
d x emailProp
I still think it is better then writing both
parcel.writeString
and
parcel.readString
for a single property
using a HashMap for every single object
Of course I don't.
every single property is wrapped in a wrapper object
Only mutable ones, which could be bound directly to views, e. g.
textView.bindTextTo(user prop Name)
. Any better ways to do observability? I know only worse ones.
every single primitive value is boxed
Yep, it is known problem with no acceptable solution, but tens of boxes are negligible if compared to a single
TextView#onMeasure
.
m
I guess we're indeed in quite different worlds here 🙂
> cannot use data classes
yep, hashCode/equals/toString are implemented, destructuring and copy are not, you're right
Can I override them?
> you lose the ability to use
is
and
as
These operators are evil should never exist. But you're still can check struct.schema == anotherSchema
They are perfectly fine and more safe than casting between generics which I would have to do after I've compare schemas.
> it's quite confusing
Well, a habit comes with time. Also, it's quite confusing when you have no control over your class, as it happens with reflection/codegen.
If I and all developers who take over my code have such a steep learning curve then it's too complicated. Sure, I can learn everything with time, so that's not a helpful argument here. What do you mean by no control?
> that's basically yelling at you "something is horribly wrong here"
They're basically yelling "Kotlin cannot deduplicate method references, Android will suck at loading many classes"
Method overload resolution works quite well, even for references 🤔 The issue here is reinventing the wheel at stdlib-level. If that's needed than building an own programming language is probably more suitable than a library imo.
> high amount of boilerplate
comparing to...
schema definition vs. simple (data) class, schema instance creation vs. simple POJO, having to use
Something<Foo>
everywhere where just POJO
Foo
would be fine, wrapping every single value in
propertyOf
> you lose some compile-time guarantees
You also don't have them when your DTO schema cannot be mapped to actual JSON/SQLite/Prefs.
They're only lost for DTOs which is normal and unavoidable, but in the library they're lost also when manually writing Kotlin code which is the important case.
>
d x emailProp
I still think it is better then writing both
parcel.writeString
and
parcel.readString
for a single property
And I prefer clarity over brevity. Too much magic hidden behind non-self-explaining infix operator and variable name.
> using a HashMap for every single object
Of course I don't.
True, Array here mostly. Nice solution! Still causes boxing though.
> every single property is wrapped in a wrapper object
Only mutable ones, which could be bound directly to views, e. g.
textView.bindTextTo(user prop Name)
.
True, only for mutating, observing, global variables and whenever
propertyOf
makes sense.
Any better ways to do observability? I know only worse ones.
Yes, no observability! It completely circumvents proper abstractions by giving every single piece of an application, even the tiniest one, direct access to the data model where they respond to and cause changes independently without knowing any context of the change. It's like KVO on iOS which causes more issues than it actually solves due to unexpected side-effects which are very difficult to anticipate in advance and there's no control over the order in which changes are applied. Rather than random elements all over the place updating their own state on model changes - or even worse directly updating some global/high-level model - all updates should be properly communicated top-down (Activity knows much more context of the change than a single TextField for example and thus decides how to propagate downwards) and all modification should be communicated bottom-up (TextField triggers the change, reports upward to the Activity which can put the change into proper context and cause the appropriate action).
> every single primitive value is boxed
Yep, it is known problem with no acceptable solution, but tens of boxes are negligible if compared to a single
TextView#onMeasure
.
I don't see how boxing helps preventing
TextView#onMeasure
. Usually you simply compare new state to old state and if there's a change then a measurement is needed. Works perfectly without any property observation or other magic, although a bit more boilerplate, but I take that for clarity and predictability.
m
Can I override them?
You can define extensions:
operator fun Struct<Something> component1() = this[Something.Field]
. But I don't really know why you want destructuring for non-tuples.
safer than casting between generics
That's true. But typecasting as such just yells "something went wrong!"
What do you mean by no control?
For example, with Gson you can declare
@JsonAdapter([De]Serializer::class)
, but it will be instantiated with parameterless constructor. This means that YourDto::class and [De]Serializer::class are singletons, i. e. cannot have any parameters/dependencies. With my approach, Schemas are just objects, so they can have parameters — you don't need any special annotations to choose converters or names for properties.
Method overload resolution works quite well, even for references
I'm talking about a different problem: for n
SomeClass::method
expressions compiler will generate n classes. While this issue exists, I will workaround it in wild ways. https://youtrack.jetbrains.com/issue/KT-15690
schema definition vs. simple (data) class, schema instance creation vs. simple POJO
Copy code
class User(
    val name: String
    val surname: String
)

User(
    name = "John",
    surname = "Galt"
)
Copy code
object User : Schema<User> {
    val Name = "name" let string
    val Surname = "surname" let string
}

User.build {
    it[Name] = "Hank"
    it[Surname] = "Rearden"
}
The noticeable difference is that
Name
symbol is duplicated by
"name"
string literal, etc. That's the price I ready to pay for keeping in-program identifier and exposed name apart.
having to use
Something<Foo>
everywhere where just POJO
Foo
would be fine
there are different forms of `Something`: write-only
StructBuilder
, read-only
StructSnapshot
, mutable & observable
ObservablePropertyStruct
,
Record
for RDBMS,
SharedPreferencesStruct
for Android prefs. With no additional boilerplate, for free!
Too much magic hidden behind non-self-explaining infix operator and variable name.
Totally agree.
d
was renamed to
io
, and I want to rename
x
.
Still causes boxing though.
It is also avoidable for non-observable structs, but it's not the main priority.
Yes, no observability!
It's optional.
:presistence
module does not depend on reactive
:properties
.
no control over the order in which changes are applied
I'm currently designing a solution which will allow applying changes transactionally, without transient states.
I don't see how boxing helps preventing
TextView#onMeasure
.
Of course not, it's just a comparison. I think that these boxings are insignificant.