I still cannot get my head around why a list of errors would be the more natural representation ....
List can be regarded as an "Effect" as per Scott Wlaschin's "Effect World". Why would you change the signature of a function to return a List intead of single string, when it really validates a single thing?
In my functions I am trying to have the most correct and minimal/most primitive types in the signature. I do not have my functions return an Option if they don't need to. I also do not return Result if not needed. Actually, when I look at a function and see that it has a Result return type but only returns Ok, then I go and change the signature and remove the Result.
This is obviously philosophical and in no means challenging your excellent (series of) article(s), I am just trying to wrap my head around the question why you and so many other people seem to be relaxed about function signatures when it comes to applicative validation ... I would Result.mapError List.singleton in the orchestration function ...
Don't worry about the critique, I like the discussion, it's part of the reason I wanted to start posting on here.
I think there are cases when having a function return a degenerate value is fine. For example, if I was writing C# and I was implementing an interface that required me to return Async<string>, but in my particular implementation I didn't need to do any async work then I would just implement with Task.FromResult("not async"). So viewed from that angle then you could think of Result<'a, 'e list> as the most permissive interface that a validation function could have. Therefore by using this consistently it allows looser coupling between the validation functions working at different levels of the data.
You said:
List can be regarded as an "Effect" as per Scott Wlaschin's "Effect World". Why would you change the signature of a function to return a List intead of single string, when it really validates a single thing?
It's worth noting that whilst we're validating a single piece of data, it's not the Ok value that we're turning into a list here, it's the Error case, so what the signature is really saying is that there might be multiple validation errors for this single piece of data.
As for lists being effects, that is certainly one way to interpret them. If I'm not mistaken it represents the effect of running several computations. A Result is also an effect, one that represents a computation that might fail. So from that viewpoint then a Result<'a, 'e list> could be interpreted as "this function will run several validation computations on this single piece of data and if they're all fine it will just return the single piece of data otherwise it will return the outputs from all of the failed validation computations". That to me sounds like quite a general statement about how validation works and therefore makes for a type that can encapsulate many different validation computations.
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.
Hi Matt,
I still cannot get my head around why a list of errors would be the more natural representation ....
List can be regarded as an "Effect" as per Scott Wlaschin's "Effect World". Why would you change the signature of a function to return a List intead of single string, when it really validates a single thing?
In my functions I am trying to have the most correct and minimal/most primitive types in the signature. I do not have my functions return an Option if they don't need to. I also do not return Result if not needed. Actually, when I look at a function and see that it has a Result return type but only returns Ok, then I go and change the signature and remove the Result.
This is obviously philosophical and in no means challenging your excellent (series of) article(s), I am just trying to wrap my head around the question why you and so many other people seem to be relaxed about function signatures when it comes to applicative validation ... I would Result.mapError List.singleton in the orchestration function ...
Best regards,
Deyan
Don't worry about the critique, I like the discussion, it's part of the reason I wanted to start posting on here.
I think there are cases when having a function return a degenerate value is fine. For example, if I was writing C# and I was implementing an interface that required me to return
Async<string>
, but in my particular implementation I didn't need to do any async work then I would just implement withTask.FromResult("not async")
. So viewed from that angle then you could think ofResult<'a, 'e list>
as the most permissive interface that a validation function could have. Therefore by using this consistently it allows looser coupling between the validation functions working at different levels of the data.You said:
It's worth noting that whilst we're validating a single piece of data, it's not the
Ok
value that we're turning into alist
here, it's theError
case, so what the signature is really saying is that there might be multiple validation errors for this single piece of data.As for lists being effects, that is certainly one way to interpret them. If I'm not mistaken it represents the effect of running several computations. A
Result
is also an effect, one that represents a computation that might fail. So from that viewpoint then aResult<'a, 'e list>
could be interpreted as "this function will run several validation computations on this single piece of data and if they're all fine it will just return the single piece of data otherwise it will return the outputs from all of the failed validation computations". That to me sounds like quite a general statement about how validation works and therefore makes for a type that can encapsulate many different validation computations.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.