Reputation: 351
I'm seeking for best practices in modeling request-response based system using functional approach in F#.
Generalized case requirements for example:
succeed
and failed
common
data piece and specific
A/B/... data piececommon
and specific
reasonsAs for now, I end up with following design:
type CommonData = {...}
type RequestMessage =
| ARequestMessage of CommonData * ARequestData
| BRequestMessage of CommonData * BRequestData
...
type Response<'S, 'F> =
| Success of 'S
| Failure of 'F
type ResponseMessage =
| AResponseMessage of Response<AResponseSuccess, AResponseFailure>
| BResponseMessage of Response<BResponseSuccess, BResponseFailure>
...
where AResponseSuccess and BResponseSuccess contains same discriminated union cases or ResponseMessage
type is covered by GeneralResponseMessage
type like
type GeneralResponseMessage =
| CommonFailure of CommonResponseFailure
| SpecificResponse of ResponseMessage
both cases look terrible and I can't figure out more clear and elegant solution.
System processes requests in statemachine-manner over single evolving state, so it isn't possible to separate different types of request processing
Upvotes: 3
Views: 156
Reputation: 10947
This may be better suited for https://codereview.stackexchange.com/, but let me try to answer this here.
My take on this would be to group the operations vertically, not horizontally. What I mean by that is that instead of grouping all requests in one type and all responses in another, I would pair up the corresponding request and response types, and build a generic wrapper for the processing itself.
In your case, the processing fundamentally is a function of type RequestMessage -> GeneralResponseMessage
. I propose generalizing this into the following:
type ProcessRequest<'requestData, 'responseMessage, 'responseFailure> =
CommonData -> 'requestData ->
Result<'responseMessage, ErrorResponse<'responseFailure>>
and ErrorResponse<'responseFailure> =
| CommonFailure of CommonResponseFailure
| SpecificResponse of 'responseFailure
Just a note here, I use Result
from FSharp.Core
instead of your Response
type, as they model the exact same thing.
Then you can create a specific instances, for example
type AProcessRequest =
ProcessRequest<ARequestMessage, AResponseMessage, AResponseFailure>
And this can then be used to process specific messages. In your case, the processing logic would have to match on RequestMessage
case and proceed based on that, in my case you have to this match before calling the specific processor, but if you need it grouped in one type, you can always do something this to supply all the implementations as a single piece of data
type ProcessMessage =
{ A : AProcessRequest
B : BProcessRequest
C : ... }
and you can supply the implementations in composition root.
If you don't like so many specific types, you don't have to give names to the concrete instances of the generic processor, C : ProcessMessage<CRequestMessage, CResponseMessage, CResponseFailure>
will do just fine, but may be less readable if there are many instances of similar types.
Upvotes: 2