I have a use-case where I need to model objects th...
# arrow
c
I have a use-case where I need to model objects that come from many different data sources, and they are not always the same. As a comparison, I could take calendar events. Some of my data sources let you subscribe to an event, some don't, but they all let you have a start/end date, etc. I think the ‘traditional' way would be something like:
Copy code
interface Event {
  val start: ...
  val end: ...
  val canSubscribe: Boolean
  suspend fun subscribe()
}
But that's a lot of boilerplate per feature, and since I want to learn Arrow, I'm thinking there must be a better way. What do you think of:
Copy code
interface Event {
  val start: ...
  val end: ...
  fun subsribe(): Either<UnsupportedOperationException, IO<Unit>>
}
p
I’m not sure I quite understand the question
you want a pub-sub system and this is the design of each message?
use a Sealed class if at all possible
if you want events in a pub/sub system to be sequenced (await event A then await event B, then do action with both), I suggest looking into our Queue implementation
otherwise, think in terms of functions, rather than modules
could
Copy code
suspend fun subscribe(start, end): Either<UnsupportedOperationException, Unit>
subsume all functionality you need? maybe it needs one or two additional parameters
c
I think my issue is that I have an interface that has many attributes/methods, but depending on the implementation some of them may be missing (maybe with one data source you can't edit an event, maybe with another you can't invite people). In ‘classic' Java, unsupported methods would throw UnsupportedOperationException, but that feels very wrong in Kotlin, so I was wondering if it would be good practice to have each method return either that error or an IO representing what the method would otherwise do, so it's possible to test at runtime if the method is supported or not
p
that’s a sealed class
if some elements implement something and others don’t, that’s a sealed class and not an interface
you refine on the element you receive using when/fold, and you call the relevant functions accordingly
if you have lots of elements in the sealed class, yes, that’s what ends up happening with this pattern. What I’ve seen in flux/redux codebases are unions of 1000+ cases
c
I see. I don't know if I want to have that many classes, that cannot be easy to maintain 🤔
That also means a ton of duplicated code, right?
p
it may not be
you can think in functions and extract common behavior, and specialize afterwards
same as you'd do with inheritance, almost
for example
Copy code
class Parent {
  open fun bla() {
    doThing()
  }
}

class Child1 {
  override fun bla() {
    super.bla()
    someThing()
  }
}

class Child2 {
  override fun bla() {
    super.bla()
    otherThing()
  }
}
becomes
Copy code
fun bla(f: () -> Unit) {
  doThing()
  f()
}

...

when (val action = getAction()) {
  is Action1 -> bla { action.someThing() }
  is Action2 -> bla { action.otherThing() }
}
can be before, after, with context, whichever way
bla
can be defined
Copy code
suspend fun <A: Serializable, B> bla(before: () -> A, after: (A) -> B): B {
  val thing = before()
  serialize(thing)
  return after(thing)
}
Copy code
when (val action = getAction()) {
  is Action1 -> bla(before = { processAction(action) }, after = { action.someThing(it) })
  is Action2 -> bla(before = { action.someField }, after = { action.otherThing(it) })
}
then you can start thinking aboiut either returns, fx, helper functions such as
handleError
on top of this way of writing code
because you're not thinking about things nominally (Parent, Child1, Child2, Manager, AbstractProxyFactory) but structurally (Either, lambda, suspend), the number of helpers available skyrockets
c
I guess that means I would need an interface per behavior (subscribe, edit, invite...) and use `when`s on that?
p
sealed class
and multiple inheritors
I am definitively missing some of the context you have
hahaha
this isn’t about FP necessarily, or arrow
you have lots of events
some have some similar functionality
so you want to group them together with interfaces
and you have a combinatory explosion of interfaces, where some may even implement several of them even=
?
that will not scale well either
not that it needs to, I mean that you’re on the same problem as the sealed class
except the sealed class is flat
sealed class Events • Event1 • Event2 • Event3 • … • EventN
and interfaces will be a tree
interface Event interface Subscribable • Event1 • Event2 interface Editable • Event3 • Event4 interface Invitable • Event5 • Event 6
the interface thing works until you have something that’s editable and subscribable, then you have an ambiguity problem
the sealed class approach will have edit and subscribe and envite method per class that will not be an override
you’re thinking about “is-a” relationship rather than “has-a”
“because it’s Invitable you can invite”
whereas sealed class is “this branch has this invite-like method” which is more flexible for you
I wish sealed classes were not classes but union types so these concepts weren’t mixed together
c
I see your point, but I don't know if it would work with me 🤔 I'm going to try to make it more explicit what the situation is. Events can be synced with multiple providers, eg. Google Calendar, NextCloud and Apple Calendar. Google doesn't support setting a priority, but NextCloud does. Google allows you to create a Google Meet conference, but both others don't. Google and NextCloud let's you set notifications, but Apple doesn't [I'm sure Apple does as well, that's just for the example]. From what I understand of your proposal, I would write:
Copy code
interface Event
interface Priority
interface GoogleMeetConference
interface Notifications
class GoogleEvent : Event, GoogleMeetConference, Notifications
class NextCloudEvent : Event, Priority, Notifications
class AppleEvent : Event
I think it's good because it allows to write a
when
to know which functionalities are supported by a particular object, to eg. hide the other options when it's displayed; but it sounds to me like a lot of code, what do you think?
p
This is starting to look like typeclasses
if instead of is-a you have has-a relationships
Events can be synced with multiple providers, eg. Google Calendar, NextCloud and Apple Calendar. Google doesn’t support setting a priority, but NextCloud does. Google allows you to create a Google Meet conference, but both others don’t. Google and NextCloud let’s you set notifications, but Apple doesn’t [I’m sure Apple does as well, that’s just for the example].
that’s fine, each can do its own thing. What’s telling them what to set? can you make a single mega data class with all this info, then delegate to each implementation to set according to its own capabilities
why does the thing that sets the event have to know the specifics of each platform?
data class Event(val p: Priority, val c: ConferenceRoom, val n: Notification)
Copy code
interface EventVisitor<T> {
  suspend fun visit(e: Event): T
}
Copy code
object GoogleEventSave: EventVisitor<Unit> {
  suspend fun visit(e: Event): Unit {
    GoogleAPI.newEvent().setConference(e.c).submit()
  }
}
Copy code
object NextCloudSave: EventVisitor<Unit> {
  suspend fun visit(e: Event): Unit {
    NextCloud().makeEvent().setConference(e.c).setPriority(e.p).save()
  }
}
that’s the OOP version
Copy code
suspend fun Event.save(v: EventVisitor<Unit>): Unit = v.visit(this)
myEvent.save(GoogleEventSave)
typeclasses makes it so you move resolving the problem to compile time
Copy code
@extension interface SaveEvent<F, T>: EventVisitor<Unit> {
  suspend fun visit(e: Event): Unit

  suspend fun Event.save(): Unit = visit(this)
}

object GoogleEventSave: SaveEvent<ForGoogle> {
  suspend fun visit(e: Event): Unit {
    GoogleAPI.newEvent().setConference(e.c).submit()
  }
}

object NextCloudSave: SaveEvent<ForNextCloud> {
  suspend fun visit(e: Event): Unit {
    NextCloud().makeEvent().setConference(e.c).setPriority(e.p).save()
  }
}
Copy code
myEvent.save() // SaveEvent is resolved statically on the method where this line is. Otherwise the method needs to be parametrized for F
c
Objects can be edited with the UI, and the UI shouldn't let the user set a priority if the back-end doesn't support it; so there needs to be a way at runtime to test how the event should be displayed
Thanks a lot for your links 😊 It makes a lot more sense now. Honestly, it's quite fitting that the solution is type classes: this particular project is already waiting for Arrow Meta to be released (because of Unions), I guess that's an occasion to use it even more ^^
Essentially, for each behavior I would have a typeclass that represents it, which would be have an implementation for each provider. It also adds the ability for users of the project to create their own providers. Would you say this is correct?
Now that I think of it, how would you handle adding new information to types? My understanding is that if I want to store a priority for events, but all events don't support it, I either have to make it private field of the Event class (but that doesn't make sense), or somehow store it in the typeclass. Maybe using map delegation could be a solution?
Copy code
object GooglePriorityProvider: Priority<ForGoogle> {
  private val priorities: MutableMap<Event, Int>
  var Event.priority by priorities[this]
}
But then, the GoogleEventSave object should know about it to be able to save that information as well? Is there such a thing as implementing a typeclass as a
class
and not an
object
, so it can have state linked to the original object? Sorry if this doesn't make sense, typeclasses are not fully clear to my mind, at this point.