The first thing that leaps out at me is the <String : Any>: it declares a type parameter called âStringâ, as some subtype of âAnyâ. (Thatâs not related to the normal âStringâ type.) It then defines the formatter() override in terms of that type parameter.
Your formatter function doesnât really make sense to me. It is generic and that means the caller provides the T. How could implementation of FormatterLookup provide exactly the formatter needed by the caller, and especially if the lookup object doesnât even know what is T?
Did you maybe mean to make the FormatterLookup generic, instead of its function? This way implementations could be related to specific T.
I would love to do it, but enum canât implement generic interfaces because enums canât be generic, right? Or am I missing something?
In my idea, each enum member should have either property or function which returns its a specific object implementing DataFormatter<T> interface of a specific type T. So, a enum member knows a specific T and can implement formatter() for this specific type.
But it seems this trick doesnât work. Any other ideas how to overcome the limitation of non-generic enums?
Yes, you are correct, enums are limited. Is there any specific reason why you need enums and you canât use a singleton object or sealed class?
Also, frankly speaking Iâm a little confused with your design. How do you plan to access these formatters in the code? If your case is that at some point we need a formatter specifically for type String, so the caller knows the data type, then we can use the TextFormatter directly, we donât need this DataTypes.TEXT. If your case is that the caller doesnât know T, e.g. we have a KClass and we need a formatter for it, then we canât use this DataTypes anyway. So what benefits do we get from having the DataTypes? I donât say we donât get any, Iâm just curious about use cases.
Thank you for your interest! Yes, you are right, it makes sense to clarify the overall idea.
I want to read from a YAML/JSON file some metadata about other files in the same folder (letâs name it metadata file). One of the atributes is a String value that is planned to be de-serialized to an enum value, which holds information about particular formatter to read data from this file (or write this file).
Moreover, this is a library, and the enum was supposed to be provided by a user of the library. So, I was planning to de-serialize metadata file to an object of a generic class with a specific user-provided enum implementing the DataFormatter<T> interface.
The ultimate goal is to have a library that will allow its users:
to define a list of supported data formats along with the corresponding formatters
to store information about formats of specific files in a library-defined metadata file
to read/write files of specified formats to Kotlin objects in a typesafe way
But it seems I wanted too much or missed something important from Kotlin, or most probably both, lol.
Hmm, so this is actually my point. If T is always dynamic in your case, then you donât really benefit from having this strongly-typed DataTypes class. And it adds boilerplate and complicates the logic. You can have a single, fully dynamic class:
fun main() {
val reg = FormattersRegistry()
reg.register(TextFormatter)
reg.register(DateFormatter)
reg.read(String::class, ...)
}
class FormattersRegistry {
fun register(formatter: DataFormatter<*>) { ... }
fun <T : Any> read(type: KClass<T>, path: Path): T { ... }
fun write(data: Any, path: Path) { ... }
}
Of course we can create a function which initializes the registry and adds all formatters. Is there anything you like to do, but canât with this approach?
Iâm not sure if I completelly understand the suggested (for example, what register() function actually does and why read() needs type: KClass<T> as an argument if we already registered a relevant formatter), but I think I got your general idea.
a simple enum that doesnât implement any interface
a factory (as a lambda, for example) of type (DataTypes)-> DataFormatter<*>
The library will use the provided enum and factory to read data from files, but the user still needs to know the type of data in a file and use it for values which will be serialized/de-serialized to/from this file. I missed the last piece in my initial idea, thank you for pointing it out!
My main point is: do we even need to require this enum? User of your lib has to first create formatters (e.g. TextFormatter, DateFormatter), then they additionally need to create enum fields (e.g. DataTypes.TEXT, DataTypes.DATE), then they probably initialize your lib with something like: initialize(DataTypes::class). And as we can see, we have some problems due to using enums that have some restrictions.
Why not simply: initialize(TextFormatter, DateFormatter) and skip the enum part entirely? Or if we need to specify formatter names/ids: initialize("text" to TextFormatter, "date" to DateFormatter).
I need enum to serialize data types to the metadata file (maintened by the library, not user). I no longer need to implement any functions in those enums, but they can have some simple properties, like file extension, for example.
initialize("text" to TextFormatter, "date" to DateFormatter)
I would prefer using enums instead of strings like text and date in such maps because enums are more convinient in when statements.
But wouldnât it be enough if DataFormatter had val id: String (or name)? By having a list of such formatters, we can create a metadata file and we can find a formatter for a specific id/name.
So Iâm back at the question if you ever need such a compile-time formatter resolution? Because all you said feels like the list of formatters is dynamic in your case. Formatters are provided by the user code, library doesnât know them, so such when code is impossible at least in the library code anyway. So to simplify my question: do you ever need to write a code which âknowsâ very specific formatters and provides different behavior depending on the chosen formatter? Does this happen in the library or in the user code?
Conceptually it doesnât matter if you define mapping between label and class in enumerator or by registry. The actual problem you facing is the type erasure: JVM doesnât know about generics and wonât be able to cast classes correctly. This is also the reason you need to provide the Class parameter to the signature of the method.
I would suggest to request from the application the map of label to Class type (as enum, registry, or simply a Map<String, Class>) and generic method, which takes that Class as a first parameter and returns the instance of T.
Inside this method they can use switch to call individual converting methods base on the Class parameter value.
Another variation would be to make this method inline. In that case it will have access to the type from the calling site without getting it explicitly as a parameter.
The enum value may define the abstract method, returning Any, or each element will implement its own method, returning its own type. In both cases the application code should âknowâ the type in the file. This is what you try to avoid.