https://kotlinlang.org logo
#arrow
Title
# arrow
d

dave08

03/27/2024, 11:02 AM
How does everybody handle enriching data hierarchies in a step by step process while avoiding mutable structures? Do you have a bunch of nullable properties and empty lists/maps that you fill in with Optics, or do you use different data classes with more and more data for each step (which makes naming pretty hard...)?
u

Ulrich Schuster

03/27/2024, 12:07 PM
I would spend the effort to model each step explicitly using maybe an algebraic data type, so my function signatures match each stage of the enrichment process. For an idea of how ADTs can be used to model multi-stage business processes, have a look a Scott Wlashin's book "Domain Modeling made functional"
☝️ 3
d

dave08

03/27/2024, 12:12 PM
> so my function signatures match each stage of the enrichment process What do you mean @Ulrich Schuster?
I guess that really limits the use of Optics then... since it's only made to mutate current structures... not map them to new ones...
u

Ulrich Schuster

03/27/2024, 12:30 PM
fun myEnrichmentStep2(val in: Step2): Step3
makes sure that you only work on the appropriate types for each stage
d

dave08

03/27/2024, 12:38 PM
In my case, I need to resolve a set of actions, and those actions need to be enriched/changed in the steps that follow... the actions derived from an Action sealed interface, and if I need to compose a FooAction with a BarAction, I made a FooWithBarAction that has a property for each... but according to what you're suggesting, I should have a set that only takes FooAction at the first step, and when I get to composing them, have a separate structure to allow containing FooWithBarAction?
I guess it could work to have them be derived from 2 sealed interfaces ActionStep1 and ActionStep2... 🙈
u

Ulrich Schuster

03/27/2024, 12:42 PM
hmm, I don't quite have the right picture, I guess. Instead of pushing data through a pipeline of functions, you could also compose the enrichment functions, and only create the result object at the very last step
d

dave08

03/27/2024, 12:43 PM
Isn't that the same thing in the end? Anyways data has to be passed in each state to all those functions...
u

Ulrich Schuster

03/27/2024, 12:51 PM
I suppose all your functions have side effects (to get the enrichment from somewhere). So instead of passing an ever increasing type through a chain of enrichment functions with side effects, you could encapsulate the effectful functions to only obtain the exact arguments they need for their effect and return only the new data, execute them (e.g., concurrently), and then assemble your output. If you have these functions as function arguments, you can mock them for testing, or use a context for each effect
d

dave08

03/27/2024, 12:57 PM
Yeah, most do have side-effects, and are modelled as use-cases
fun interfaces
, so they're easy to replace for testing. I think you have a good point... each one could technically return only what they are adding... I think I'm not yet used to "functional thinking"... and my current implementation was mutable, so I didn't really notice that pattern. I guess the reason was to "over-optimize" things... but it makes things more complex to reason about and test... Thanks for your advice!
🙂 1
r

raulraja

03/27/2024, 2:45 PM
@dave08 @Ulrich Schuster do you have some minimal sample code or examples that illustrate these patterns?
u

Ulrich Schuster

03/27/2024, 2:49 PM
unfortunately not. I'm currently trying to incorporate typed errors for smart constructors on our domain types, which is a similar pattern. In general, it sounds very much like what folks in the Haskell world would call Applicative Functors. There is a nice example in the book by Alejandro Serrano Mena (Functional Programming IDeas for the Curious Kotliner), which is where I got most of the patterns from I'm learning to apply currently
👍 1
d

dave08

03/27/2024, 2:50 PM
Copy code
sealed interface Action

sealed interface ActionWithKey : Action {
  val key: Key
}

data class Action1(...) : ActionWithKey

data class ActionEnriched(val action1: Action1, val enrichment:...) : ActionWithKey

...

data class FooResponse(
  val bar: Map<Key, ActionWithKey>,
  val baz: Set<Action>
)

fun interface UseCase1 {
  operator invoke(currResponse: FooResponse): FooResponse
}
...
this is my current attempt... while trying to migrate to something better...
👍 1
So UseCase1 only gives Action1 w/o the enrichment, but UseCase2 might enrich Action1 and make it into ActionEnriched, or might not need to do that (if it doesn't need it...)