Hi! Thanks to you, I think I finally understand ho...
# arrow
c
Hi! Thanks to you, I think I finally understand how typeclasses solve the problem of adding new behavior to domain classes (eg. Convert a domain object to JSON, Serialize a domain object, Compare two objects, Display an object, etc). However, I do not understand how typeclasses can be used to add data to an object. I'm trying to model different ‘providers', which can request information from the net to create a specific domain object, let's call it ‘event'. Most properties of events will be identical, but some providers might have additional properties: for instance, maybe two providers can also create sub-events, but a third one cannot. I'd like to be able to conditionally know if a specific property is available on a specific event or not, for example to display the event (only available properties should be displayed). For the use case of ‘one provider lets you edit the event, the others don't', I see how I can leverage typeclasses to specifically add that behavior. However, I don't know how to add data. I thought of three solutions, that I don't find ‘clean', so I'd like to have your thoughts: 1. Use nullable fields for data that is not present on all events That way, it's easy to test if some data is present or not. The code can be very straightforward, but it's not very beautiful that there are many nullable fields on every object. A slightly better solution could be to have every field be an union of the expected data and an
Unimplemented
Singleton, but that still doesn't look so good. 2. Use inheritance Create one interface per optional field, and have each provider have its own event implementation that extends the correct interfaces. This could lead to a bit of repetition, though I guess combined with delegation it could look okay. 3. Use WeakHashMap in a typeclass to store the additional information That doesn't seem very nice as well... What do you think?
j
However, I do not understand how typeclasses can be used to add data to an object
They don't typeclasses are behavior, not data (and data is ideally dumb). These two are strictly separated.
c
@Jannis Hm. How would you implement something like that, then?
j
Good question, been thinking about this for a bit and I have had this problem previously as well^^
All I know for sure is that typeclasses cannot add data, they can add accessors to data which will help you with dynamic data but they can't how data is represented itself
In haskell I'd solve this with typefamilies where I would have
data Event = MkEvent { ...; evData: EventData a }
and
EventData a
resolves my data through a type level function. But this is not possible in kotlin... Normally this screams sum-types and thus
sealed classes
but this becomes unwieldy with too many fields and too many repeated fields
s
one approach is:
Copy code
sealed class EventData
object Nothing : EventData()
data class Name(val name: String) : EventData()
data class Value(val value: Int): EventData()

data class Event<A : EventData>(val id: Long, val data: A)

fun main() {
    val eventWithoutData = Event(0L, Nothing)
    val eventWithName = Event(1L, Name("My event!"))
    val eventWithValue = Event(2L, Value(15))
}
you could drop the sealed class (and use
String
and
Int
directly) if you don't care about exhaustive when
j
And if you make
EventData
recursive using
EventData<A>
and for example
Name<A: EventData>(val name: String, val data: A)
you can build sort of linked lists of properties
Accessing these is going to be a pain though
unless you do optics and abstract away the accessor 😄
Then define typealiases for the specific types of events
NameAndValueEvent = Event<Value<Name<Nothing>>>
c
Maybe I should rephrase as ‘how can providers inject data into the domain object they provide'? Because I expect that each provider will also want an internal ID to link the domain object with the external representation, etc.
I think I'm going to go with the ‘each field that might not be available is a union with a Unimplemented Singleton, and each provider implements the interface with its own private data, such as an ID & similar'
j
So you are dealing with fully dynamic data that you have no static information about?
c
@Jannis At implementation time, I know which provider provides what information, but I'd rather have a polymorphic way to handle events no matter which provider they come from, for code beauty Also to reduce code duplication, because most providers will give the same information in a different way (for example who participates), that I'd rather be able to use transparently no matter what the underlying representation is
j
That sounds like a classy lens problem
So type classes for data access and manipulation but individual datatypes and/or sumtypes.
If you are unfamiliar with optics/lenses: They are combined getters/setters which can be composed and provide read/write access through any nesting or complex immutable data. This can be used to provide virtual fields for any datatype where the actual representation does not matter. Classy lenses extend this by using typeclasses to provide these lenses: (can also provide traversals, prisms etc here)
Copy code
interface HasName<A> { fun name(): Lens<A, Name> }
This provides a nice api that does not care about the underlying representation (you could even keep it in binary format, but that'd be overdoing it 😅). Lenses are amazing for this but require some learning effort... You can also do this without lenses by defining the interface with a getter and setter method or probably some other method of accessing. This is also very close to the inheritance method you described and defining an interface which each event implements. In haskell these instances are trivially derived through use of generics, with arrow-meta we could provide similar means of derivation, but right now this leads to boilerplate on each implementation of an event. Not sure if I have a better idea tbh, you can always do the same with an unsafe representation e.g. define a type safe api based on lenses and lens typeclasses on top of an unsafe impl using hashmaps or similar
c
Yeah, that's interesting. I'm trying not to jump too much into lenses for now since I'm not completely familiar with type classes and all provided data types, I think it's better if I get to those a bit later. I agree the principle sounds really nice but the syntax sounds a bit much for now 🤔 Thanks a lot for your input, it's pretty nice learning from people who love what they do