deusaquilus/pprint-kotlin

Find a way to not depend on runtime reflection

deusaquilus opened this issue · 10 comments

This library uses kotlin-reflect in the Walker in order to recursively get properties from each class as well as the class names. This functionality can be replaced with kotlinx-serialization functionality fairly easily. I have a branch that does exactly this. Can develop this further if there is an ask.
https://github.com/deusaquilus/pprint-kotlin/tree/using-kotlinx-serialization

Screenshot from 2023-12-31 03-13-49

I would really like to have a multiplatform solution without using kotlin-reflect. So does this approach require the classes to be annotated with @Serializable ? Can we use kotlin-metdata-jvm that will be lighter than kotlin-reflect ? Not sure if we can use this for multiplatform support though

Yeah, this solution would require @Serializeable. Is that a problem?

Most of the time you are not in charge of the data class component to annotate... what if data classes contains other classes from third party deps?

I see logging as a hassle-free way to print debug information. Being forced to annotate my data classes with @Serializable before I'm able to print them would be very unfortunate, especially for classes that I do not own as @sureshg already mentioned.

Turns out kotlinx-serialization has a way to do that. If it's a simple Kotlin class with a properties-only constructor (i.e. all data classes are this way as far as I know) you can just do:

// NOT @Serializable
class ClassThatIDontControl(val foo: String, val bar: Int)
                           
@Serializer(forClass = ClassIDontControl::class)
object ClassIDontControlSerializer

// Then the pprint API would look something like this 
val instance = ClassIDontControl("hello", 123)
println(pprint(ClassIDontControlSerializer, instance))

Have a look here for info.

Now if the ClassThatIDontControl is more complex e.g. a Java class you'll have to write the ClassThatIDontControlSerializer in a more manual way. Have a look at Delegating Serializers for more info on that. I imagine it would be something like:

// Imagine this is coming from some 3rd-party dependency
public class ClassIDontControl {
  private String foo;
  private Int bar;
  public String getFoo { return foo; }
  public String getBar { return bar; }
  // blah blah blah constructors, initializers, etc...
}
// Then in your application you'd do...
class ClassIDoControl(foo: String, bar: Int)

class ClassIDontControlSerializer : KSerializer<ClassIDontControl> {
    private val delegateSerializer = ClassIDoControl.serializer()
    override val descriptor = SerialDescriptor("ClassIDontControl", delegateSerializer.descriptor)

    override fun serialize(encoder: Encoder, value: ClassIDontControl) {
        val data = ClassIDoControl(value.getFoo(), value.getBar())
        encoder.encodeSerializableValue(delegateSerializer, data)
    }

    override fun deserialize(decoder: Decoder): ClassIDontControl {
        val data: ClassIDoControl = decoder.decodeSerializableValue(delegateSerializer)
        return ClassIDontControl.constructWhateverWayYouNeed(data.foo, data.bar)
    }
}
                           
@Serializer(forClass = ClassThatIDontControl::class)
object ClassIDontControlSerializer

// Then the pprint API would look something like this 
val instance: ClassIDontControl = getItFromWhereverYouNeed(...)
println(pprint(ClassIDontControlSerializer, instance))

You can learn more about delegating serializers here.

Bottom line, it's doable. The mighty kotlinx-serialization people have thought about these use-cases.

Bottom line, it's doable. The mighty kotlinx-serialization people have thought about these use-cases.

I'm sorry I should have clarified this -- we're aware that it is doable, but the effort needed to make them serializable can be non-trivial which will discourage us from using pprint.

I'm a mobile developer so my experience may be different from other platforms, but I've never seen anyone check-in println statements to production code. It is always used for temporary debugging sessions as an inexpensive way to aid/avoid the IDE debugger. I'd love to use pprint for printing nicer, readable logs but I'd not use it if it required my classes to be serializable.

Plus what happens to projects that use libraries other than kotlinx.serializable such as moshi?

I’m confused. Do you want to use kotlin-reflect, moshi, or something else?

I'm suggesting to continue using reflection.

I won't remove any of the current reflection-based functionality. I'll add a new module (probably pprint-multiplatform) that does the non-reflection based stuff.

Sounds superb!