General question (given functional programming is ...
# arrow
d
General question (given functional programming is so much about having specific types for things...), how good of an idea is this, any downsides or better ideas?
Copy code
@JvmInline
value class Incomplete<T>(val value: T)

class FooRepo {
  suspend fun getFoo(...) : Incomplete<Foo> ... // Since the db doesn't have all the info to fill in all the Foo fields
}

class FooDecorator(val fooRepo...) {
  suspend fun getFoo(...) : Foo // get icomplete foo from fooRepo and fill in what's missing from other sources...
}
p
one downside is your
T
would need to account for being both fully and partially populated (either with default values on some properties, or making them nullable/Option to allow for missing values) you may be better off having an explicit variants "DbT" and "T" allowing T to be type-safe on complete data, and DbT to not worry about being partial.
d
You mean separate data classes + mappers...?
Right now that field IS nullable... but you're right, in some cases it's not so great to have to fill in a bunch of dummy values for them to be replaced after... but then again, what's the difference between having:
Copy code
fun DbFoo(/* only the db fields */) = Foo(
... // fill in bogus field values
)

fun Incomplete<Foo>.complete(
 // all the bogus fields need to be filled in
) = Foo(...)
and those mappers? Anyways the incomplete Foo is marked with Incomplete @phldavies
And I'd avoid a whole set of new classes...
(And naming them, managing them ... 😜)
p
unless you mark
val value: T
as internal/private it's possible someone could unwrap the
Foo
and treat is as complete
d
Good point! I could actually do that, but I couldn't access it from the extension function then...?
I guess I could create those
complete
functions on the domain layer gradle module and declare them as internal then...
Otherwise, not too bad of an idea, no?
But you're right in one thing, that in some cases this forces me to use field types that I wouldn't use if I had separate classes for dbT and T...
p
there would still be avenues where things could go wrong unless you also hide the copy constructor. Consumers would also need to handle potential nulls on the populated fields (despite them never being null once "complete") or risk possible NPEs if things change. IMO, it's safer to rely on the typesystem rather than wrappers.
d
All very good points (although this might help with copy https://kotlinlang.org/docs/whatsnew2020.html#data-class-copy-function-to-have-the-same-visibility-as-constructor), and something to consider in future refactorings... But, the current code of one of our apis is littered with tons of these decorators, and honestly I'm trying to find a quick way to make a bit of sanity out of all this without rewriting tons of code. I'll really have to reconsider the pros and cons of this discussion to see what the best approach is here, thanks!
p
One last comment, it seems like pursuing this approach correctly to avoid creating a second set of data classes that correctly model your database layer compared to your service layer is likely to entail more work and more surprises for those approaching the code than simply modelling your database and service layers correctly.
👍🏼 1
d
Yeah, well making such classes takes TONS of time, the current code is such a mess -- and each occurrence needs to be thought about separately on how the db class should be and how it should be progressively decorated to get it to a valid state... (there's adding info from different sources, making a subtitle based on other fields and translations, signing s3 urls...), I'm really wondering if my proposition would be worth going for as an initial refactor, and then eventually migrate to your proposition... 🤷🏼‍♂️. Or maybe there's a better idea out there...?
In the end, to avoid the problem of exposing the value (then the user would just unwrap and use copy on the unwrapped version) -- at least as glue code for a messy codebase until I can make proper intermediary classes, I came up with:
Copy code
@JvmInline
value class Incomplete<T>(private val value: T) {
    // Intermediate transformer that operates on an Incomplete and returns another Incomplete
    fun interface IntermediateTransformer<T> {
        suspend fun invoke(input: T): T
    }

    // Final transformer that operates on an Incomplete and returns a complete value
    fun interface FinalTransformer<T, R> {
        suspend fun invoke(input: T): R
    }

    // Intermediate transformation that returns a new Incomplete
    suspend fun map(transformer: IntermediateTransformer<T>): Incomplete<T> {
        val transformedValue = transformer.invoke(value)
        return Incomplete(transformedValue)
    }

    // Terminal transformation to produce a final value of type T
    suspend fun complete(transformer: FinalTransformer<T, T>): T {
        return transformer.invoke(value)
    }

    // Terminal transformation to produce a final value of type R
    suspend fun <R> completeTo(transformer: FinalTransformer<T, R>): R {
        return transformer.invoke(value)
    }
}

fun <T> T.asIncomplete() = Incomplete(this)

// then we could have:
fun fooTransformer(
    ...
): Foo = Incomplete.FinalTransformer { foo -> ... }

...

// in controller -- enforcing specific transformations to get to different state:
getFoo(...).complete(fooTransformer(...))
This is a Monoid isn't it? There's still something wrong with this though... since the scope is being used only to make a T, but a user could pass
it.copy(...)
down just as easily... I wonder if there's a better way... at least, the usage is marked and the user is making a conscious decision to mess things up...