DEV Community

Discussion on: Grokking Applicative Validation

Collapse
 
deyanp profile image
Deyan Petrov

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

Thread Thread
 
choc13 profile image
Matt Thornton • Edited

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.

Thread Thread
 
deyanp profile image
Deyan Petrov

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

let worker1a (someParam:bool) : Result<string, string> = 
    if someParam then Ok "all good1" else Error "sth wrong1a"

let worker1b (someParam:bool) : Result<string, string> = 
    if someParam then Ok "all good2" else Error "sth wrong1b"

type OrchestrFunctionErrorDU =
    | HighLevelError1 of string
    | HighLevelError2 of string

let orchestrator1 () : Result<string, OrchestrFunctionErrorDU> = 
    result {
       let! x = worker1a true |> Result.mapError OrchestrFunctionErrorDU.HighLevelError1
       let! y = worker1b false |> Result.mapError OrchestrFunctionErrorDU.HighLevelError2
       return "all good"
    }
Enter fullscreen mode Exit fullscreen mode

OPTION 2

type OrchestrFunctionErrorDU =
    | HighLevelError1 of string
    | HighLevelError2 of string

let worker2a (someParam:bool) : Result<string, OrchestrFunctionErrorDU> = 
    if someParam then Ok "all good1" else "sth wrong2a" |> OrchestrFunctionErrorDU.HighLevelError1 |>Error

let worker2b (someParam:bool) : Result<string, OrchestrFunctionErrorDU> = 
    if someParam then Ok "all good2" else "sth wrong2b" |> OrchestrFunctionErrorDU.HighLevelError2 |>Error

let orchestrator2 () : Result<string, OrchestrFunctionErrorDU> = 
    result {
       let! x = worker2a true
       let! y = worker2b false
       return "all good"
    }
Enter fullscreen mode Exit fullscreen mode

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>]
module ResultComputationExpression =

    type ResultBuilder() =
        member __.Return(x) = Ok x
        member __.Bind(x, f) = Result.bind f x

        member __.ReturnFrom(x) = x
        member this.Zero() = this.Return ()

        member __.Delay(f) = f
        member __.Run(f) = f()

        member this.While(guard, body) =
            if not (guard()) 
            then this.Zero() 
            else this.Bind( body(), fun () -> 
                this.While(guard, body))  

        member this.TryWith(body, handler) =
            try this.ReturnFrom(body())
            with e -> handler e

        member this.TryFinally(body, compensation) =
            try this.ReturnFrom(body())
            finally compensation() 

        member this.Using(disposable:#System.IDisposable, body) =
            let body' = fun () -> body disposable
            this.TryFinally(body', fun () -> 
                match disposable with 
                    | null -> () 
                    | disp -> disp.Dispose())

        member this.For(sequence:seq<_>, body) =
            this.Using(sequence.GetEnumerator(),fun enum -> 
                this.While(enum.MoveNext, 
                    this.Delay(fun () -> body enum.Current)))

        member this.Combine (a,b) = 
            this.Bind(a, fun () -> b())

    let result = new ResultBuilder()
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
choc13 profile image
Matt Thornton • Edited

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.