Thanks for your patience, Matt! I think it will be over after you read the below ;)
I understand that you want to extend the output set of possible values of the function in order to generalize its interface to theoretically handle more than 1 error.
The fact of the matter is that the current version of the function(s) is returning only a single error though, and a Result<_, string list> may not be really justified by the function when looking at it in isolation.
The Result<_, string list> is solely required so that this function can be used in a parent/"orchestration" function utilizing applicative validation. So the "innocent" function knows this parent/orchestration context now ...
Let me ask you a similar question - imagine you have an orchestration/workflow function returning Result<_, OrchestrFunctionErrorDU>, and invoking 2 simple/reusable in different context "worker" functions. Which variant of the 2 below would you choose?
The difference is that in Option 1 the worker functions do not know about the higher level context and its error DU, and return some primitive error type (in this case string). In Option 2 they do know about the higher-level error DU, and they use it by pretending (imho) to be able to return both error cases, but of course returning only one of them.
I think you would choose option 2 ... I would choose option 1 but still trying to understand why people would choose option 2 ...
P.S. if you need the result CE (from Scott Wlaschin) to make the above work in fsx here it is:
//==============================================// Computation Expression for Result//==============================================[<AutoOpen>]moduleResultComputationExpression=typeResultBuilder()=member__.Return(x)=Okxmember__.Bind(x,f)=Result.bindfxmember__.ReturnFrom(x)=xmemberthis.Zero()=this.Return()member__.Delay(f)=fmember__.Run(f)=f()memberthis.While(guard,body)=ifnot(guard())thenthis.Zero()elsethis.Bind(body(),fun()->this.While(guard,body))memberthis.TryWith(body,handler)=trythis.ReturnFrom(body())withe->handlerememberthis.TryFinally(body,compensation)=trythis.ReturnFrom(body())finallycompensation()memberthis.Using(disposable:#System.IDisposable,body)=letbody'=fun()->bodydisposablethis.TryFinally(body',fun()->matchdisposablewith|null->()|disp->disp.Dispose())memberthis.For(sequence:seq<_>,body)=this.Using(sequence.GetEnumerator(),funenum->this.While(enum.MoveNext,this.Delay(fun()->bodyenum.Current)))memberthis.Combine(a,b)=this.Bind(a,fun()->b())letresult=newResultBuilder()
Yes, in this case I would take option1, because OrchestrFunctionErrorDU has nothing to do with the lower level functions. However, I don't think this is the same case, although it might appear to be.
My primary motivation for choosing to use Result<'a, 'e list> is not because the parent function needs an error list in order for apply to work, but instead because it's a good representation of a validation computation in its own right. The argument being that when validating any data it's not unreasonable to expect to encounter several errors.
It just also happens that in this case it does make the parent composition easier too, but that's a secondary reason for doing it that way.
If I were actually modelling the errors then I would likely use a DU to describe the errors cases for each field and then unify those with a DU at the parent level (similar to your example). In that case I would only lift those errors into the parent DU within the parent function, because as you rightly say the child validation function should have no knowledge of that parent level DU type.
So I normally expect to do some error mapping in the parent function, but in the case of whether or not to return a list of errors I choose to use a list in the child function because above all else I believe it is a better api for that function, which allows that function to evolve more independently in the future.
I guess another way to think about this is the locality of any future changes. If I were to change the validateChild function in the future and it now started returning several errors instead of one I would be forced to also go and fix validateParent which called that. However, in the scenario where I'm unifying errors in parent ParentErrorDU type then that is something that is defined at the same abstraction level as validateParent (probably in the same F# module) and so if I change that type then I'm going to have to fix the error mappings in validateParent but I'm OK with that because that is a change at the same level of abstraction. I don't however have to go and change any of the child validation functions just because ParentErrorDU changed.
So by returning a list of errors from the child and doing any parent level DU mapping in the parent we've achieved high cohesion and low coupling even though they look like they're contradictory design choices.
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Thanks for your patience, Matt! I think it will be over after you read the below ;)
I understand that you want to extend the output set of possible values of the function in order to generalize its interface to theoretically handle more than 1 error.
The fact of the matter is that the current version of the function(s) is returning only a single error though, and a
Result<_, string list>
may not be really justified by the function when looking at it in isolation.The
Result<_, string list>
is solely required so that this function can be used in a parent/"orchestration" function utilizing applicative validation. So the "innocent" function knows this parent/orchestration context now ...Let me ask you a similar question - imagine you have an orchestration/workflow function returning
Result<_, OrchestrFunctionErrorDU>
, and invoking 2 simple/reusable in different context "worker" functions. Which variant of the 2 below would you choose?OPTION 1
OPTION 2
The difference is that in Option 1 the worker functions do not know about the higher level context and its error DU, and return some primitive error type (in this case string). In Option 2 they do know about the higher-level error DU, and they use it by pretending (imho) to be able to return both error cases, but of course returning only one of them.
I think you would choose option 2 ... I would choose option 1 but still trying to understand why people would choose option 2 ...
P.S. if you need the result CE (from Scott Wlaschin) to make the above work in fsx here it is:
Yes, in this case I would take option1, because
OrchestrFunctionErrorDU
has nothing to do with the lower level functions. However, I don't think this is the same case, although it might appear to be.My primary motivation for choosing to use
Result<'a, 'e list>
is not because the parent function needs an error list in order forapply
to work, but instead because it's a good representation of a validation computation in its own right. The argument being that when validating any data it's not unreasonable to expect to encounter several errors.It just also happens that in this case it does make the parent composition easier too, but that's a secondary reason for doing it that way.
If I were actually modelling the errors then I would likely use a DU to describe the errors cases for each field and then unify those with a DU at the parent level (similar to your example). In that case I would only lift those errors into the parent DU within the parent function, because as you rightly say the child validation function should have no knowledge of that parent level DU type.
So I normally expect to do some error mapping in the parent function, but in the case of whether or not to return a list of errors I choose to use a list in the child function because above all else I believe it is a better api for that function, which allows that function to evolve more independently in the future.
I guess another way to think about this is the locality of any future changes. If I were to change the
validateChild
function in the future and it now started returning several errors instead of one I would be forced to also go and fixvalidateParent
which called that. However, in the scenario where I'm unifying errors in parentParentErrorDU
type then that is something that is defined at the same abstraction level asvalidateParent
(probably in the same F# module) and so if I change that type then I'm going to have to fix the error mappings invalidateParent
but I'm OK with that because that is a change at the same level of abstraction. I don't however have to go and change any of the child validation functions just becauseParentErrorDU
changed.So by returning a list of errors from the child and doing any parent level DU mapping in the parent we've achieved high cohesion and low coupling even though they look like they're contradictory design choices.