How does everybody handle enriching data hierarchi...
# arrow
d
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
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
> 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
fun myEnrichmentStep2(val in: Step2): Step3
makes sure that you only work on the appropriate types for each stage
d
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
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
Isn't that the same thing in the end? Anyways data has to be passed in each state to all those functions...
u
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
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
@dave08 @Ulrich Schuster do you have some minimal sample code or examples that illustrate these patterns?
u
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
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...)