- A getOperation.. this can return an option of v
- A writeOperation(key, value, modeOfWriting) … this can return a
Result
. The Result should encapsulate the actual return value and the mode. TheMode
can beOverwrite
(which can beReplaced
orNew
orFailed
), Insert (which can beNew
orFailed
) orReplace
(which can be Wrote or Failed). Feel free to represent this in any way which is type safe. - A deleteOperation returning a unit.
- The
Result
also hasFaiureFallBackOperations
orIfSuccess
encoded in as well. This may or may not be used by the clients usingDbOperation
. - A readAll returning a
Page
, representing alist of value
and thenext key
.
Let us expose the above operation as a Table
because User doesn’t need to handle any of the DbOperations directly.
Table makes use of the above DbOperation, but with a few extra logic.
- Get - uses the get operation, however depending on the value returned it should specify how to handle the result. A Handle can be asking the user to PrintLn, Concatenate with a fixed String, WriteResultsToDb (I don't know ..something).
- The user can then do (in a different space, time) to actually do the
handle
operation. However Table won't do this, it will tell the user of the table to do what next. - WriteOperation. Depending on the
Result
of operation (which meansmode
) you need to send a notification to admin which can be sending an email, sending a message or only logging it. The option to choose depends on the few runtime results with in the Put operation. Upto you to define this logic. [Constraint: Table operation should never do any notifications by any chance although, Table knows about Notification] - Replace Operation (the mode of writing is always Replace). If Mode == Fail, send a notification. If Mode is
Replace
success, thencreate an entry
insuccess control table
. Elsecreate an entry
infailure control table
. [Constraint: Table operation should never do these inserts. Please defere these callback/feedback operations as much as possible.] - Delete operation (simply makes use of the above opertaion).
- readAll returns page and an
AnotherHandler
. Example, based on the contents of the page, or size of page, or based on some internal db operation, return the callback operationWrite to a File
orWrite to InMemory mutable map
. Feel free to think we have dozens of different Handlers or CallBacks, which is returned by different functions.
- updateAndDelete
- updateAndThenRead
- deleteAndThenPut .
Feel free to add a variety of operations like this.
Please do remember that, in test cases we need to do all combinations. Example: We need to test what happens if we update after a delete.
Add one test case val r = table.readAll if (r.page.size > 100) else . [constraint: You shouldn't need to match output of a mock file-write or inmemory-write in this process]
Feel free to add InMemory which doesn’t have anything to do with IO, and everything is without effects. Basically the effect that it will handle is Invalid \/ ?
. (Gels well with Finally tagless and Free, but worth trying)
Be it finally tagless or Free Monad, we can easily anticipate an object that consist of actual Azure related operations which can be under the effect EitherT[Kleisli[F, AzureClient, ?], Invalid, ?]
. Feel free to represent Invalid
in any way. Invalid
can be a universal error of the app. There can be an engineering done in this space, but leaving it at this stage.
Object ActualAzureOperation {
type AzureAction = EitherT[Kleisli[F, AzureClient, ?], Invalid, ?]
def createTable = AzureAction[A]
def write: AzureAction[Result[V, Mode]]
def delete: AzureAction[Result[V, Mode]
... and so on!
}
-
All
Handlers
mentioned in the problem can be a Free algebra? May be yes, may be no. Let's see. -
The
DbOperation
is another Free algebra, andTable
functions hence return a Free Monad. -
A natural transformation from
f1: DbOperation ~> AzureAction
will exist for sure. -
A transformation which converts
f2: DbOperation ~> C
tof3: DbAction ~> C
will exist for sure, wheretype DbAction = Free[DbOperation, A]
. Please noteDbAction
becomes secondary here. We cared onlyDbOperation
and we getDbAction ~> C
for free. -
table.put(data)
seem to returnDbAction
. So how do we test out the actual interpreter which is someAzureAction
? .Well, usef1
and compose it with a function that goes fromAzureAction ~> Invalid \/ A
resulting inDbOperation ~> Invalid \/ A
. Again, it makes intuitive sense that we are going from justDbOperation ~> Invalid \/ A
. We don’t care other effects in test cases. -
For in memory all that I need to think of is
not F
, but aDbOperation ~> Invalid \/ A
. As mentioned before, given aDbOperation ~> Invalid \/ A
, we can inductively getf: DbAction ~> Invalid \/ A
. Hencef(table.delete(data))
returnsInvalid \/ A
. -
Also think when the return type of an
operation
is another operation. Example: An operations returns a dependent callback handler. In Free thinking, this operation is another simple algebra that are devoid of effects. In Free thinking, this meansDbAction
will contain, for example,Notification[Unit]
. We can useDbAction ~> Invalid \/ A
andNotificationAction ~> Invalid \/ B
(obtained fromNotificationOp ~> Invalid \/ A
) and then merge to getInvalid \/ (A, B)
. Oh well! We actually don't need to doNotification
yet. Defer it further. Example:DbAction ~> Invalid \/ Notification[Unit]
first and then and pass the result to next operation.NotificationAction ~> Invalid \/ Unit
. At this point, I see an advantage in my test cases. (Again, I haven't thought how would it look like in finally tagless. Free algebrae allowed me to reach the solution more intuitively. I need to "think" to solve this in inheritence mode.). -
Write test cases interpretrer that goes from
DbAction ~> Invalid \/ A
given aDbOperation ~> AwsAction
.
Natural transformation are super flexible + you can do pattern matching on operations directly , and of course being able to pass the individual components of the algebra (program) with least number of type parameters.
Actual code answer is here
Here goes another question and answer that proves Free is better. The problem is inspired from a blog, however it took some time to make it compile and that's mostly because of my ignorance.
The (question and) solution below actually inspects a program, test if it is doing the right thing, including the order of executions.
https://gist.github.com/afsalthaj/110e01322cc05ef6c7a1e6fa45e95dea
I have also tried to do the same thing with Finally Tagless and I lost the intuitive thinking of the problem - the problem of inspecting a program.
https://gist.github.com/afsalthaj/9a59ed8ae4b7b1c238759a85af5ccf45
I have a general feeling now that a program can have both patterns and is completely justified. Free can sadly come up with a big bunch of boilerplates due to the entry of Coproducts
concept, but it isn't a good justification to remove it in places in the program where it can nicely work. In summary, both patterns can solve your problem on a high level, however, both patterns are not completely isomorphic to each other, especially when considering the power of testability/correctness through inspection of a program and the actual deferring of execution (not just "delaying") that Free brings in (intuitively).
I am calling Free better for this program, because a program is returning lot many programs here, essentially giving you control over when to execute the deffered execution. A slight change in the usecase will make finally tagless win the game if you think about it.