FSharpPlus icon indicating copy to clipboard operation
FSharpPlus copied to clipboard

WIP: Validation CE Builder

Open natalie-o-perret opened this issue 5 years ago • 11 comments

Description

Not directly related to https://github.com/fsprojects/FSharpPlus/issues/388 But more about the newly released .NET + F# 5.0. The idea would be to provide a dedicated CE builder for validation types supporting the and! keyword.

Sources:

  • https://devblogs.microsoft.com/dotnet/announcing-f-5/#applicative-computation-expressions
  • https://gist.githubusercontent.com/cartermp/8540e3fb1edf647f3c7ee8e4352c5941/raw/0c6893a68499499eca517118d3db80c379fc8799/fs5-applicatives.fsx
// First, define a 'zip' function
module Result =
    let zip x1 x2 = 
        match x1,x2 with
        | Ok x1res, Ok x2res -> Ok (x1res, x2res)
        | Error e, _ -> Error e
        | _, Error e -> Error e

// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() = 
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x

let result = ResultBuilder()

let run r1 r2 r3 =        
    // And here is our applicative!
    let res1: Result<int, string> =
        result { 
            let! a = r1 
            and! b = r2
            and! c = r3
            return a + b - c 
        }

    match res1 with
    | Ok x -> printfn "%s is: %d" (nameof res1) x
    | Error e -> printfn "%s is: %s" (nameof res1) e

let printApplicatives () =
    let r1 = Ok 2
    let r2 = Ok 3 // Error "fail!"
    let r3 = Ok 4

    run r1 r2 r3
    run r1 (Error "failure!") r3

Repro steps

Please provide the steps required to reproduce the problem

let runValidations v1 v2 v3 =
    let val1 =
        validation {
            let! a = v1
            and! b = v2
            and! c = v3
            return a + b - c
        }

    match val1 with
    | Success s -> printfn "%A is: %A" (nameof val1) s
    | Failure f -> printfn "%A is: %A" (nameof val1) f


let printValidations () =
    let r1 = Validation<string list, int32>.Success 2
    let r2 = Validation<string list, int32>.Success 3
    let r3 = Validation<string list, int32>.Success 4

    runValidations r1 r2 r3
    runValidations
        r1
        (Validation<string list, int32>.Failure [ "Meow 1" ])
        (Validation<string list, int32>.Failure [ "Meow 2" ])

[<EntryPoint>]
let main _ =
    printValidations()
    0
  1. Step B

Expected behavior

validation CE builder should be defined and the output of the program should be:

"val1" is: 1
"val1" is: ["Meow 1"; "Meow 2"]

Actual behavior

The value or constructor 'validation' is not defined.

Known workarounds

open FSharpPlus.Data
module Result =
    let zip r1 r2 =
        match r1, r2 with
        | Ok o1, Ok o2 -> Ok (o1, o2)
        | Error e1, _ -> Error e1
        | _, Error e2 -> Error e2
​
// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() =
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x
​
module Validation =
    let zip x1 x2 =
        match (x1, x2) with
        | Success s1, Success s2 -> Success (s1, s2)
        | Failure f1, Success _  -> Failure f1
        | Success _,  Failure f2 -> Failure f2
        | Failure f1, Failure f2 -> Failure (f1 @ f2)
​
type ValidationBuilder() =
    member _.MergeSources(t1, t2) = Validation.zip t1 t2
    member _.BindReturn(x, f) = Validation.map f x
    member _.Bind(x, f) = Validation.bind f x
​
​
let result = ResultBuilder()
let validation = ValidationBuilder()
​
let runResults r1 r2 r3 =
    // And here is our applicative!
    let res1: Result<int, string> =
        result {
            let! a = r1
            and! b = r2
            and! c = r3
            return a + b - c
        }
​
    match res1 with
    | Ok o -> printfn "%A is: %A" (nameof res1) o
    | Error e -> printfn "%A is: %A" (nameof res1) e
​
let runValidations v1 v2 v3 =
    let val1 =
        validation {
            let! a = v1
            and! b = v2
            and! c = v3
            return a + b - c
        }
​
    match val1 with
    | Success s -> printfn "%A is: %A" (nameof val1) s
    | Failure f -> printfn "%A is: %A" (nameof val1) f
​
let printResults () =
    let r1 = Ok 2
    let r2 = Ok 3
    let r3 = Ok 4
​
    runResults r1 r2 r3
    runResults r1 (Error "failure!") r3
​
let printValidations () =
    let r1 = Validation<string list, int32>.Success 2
    let r2 = Validation<string list, int32>.Success 3
    let r3 = Validation<string list, int32>.Success 4
​
    runValidations r1 r2 r3
    runValidations
        r1
        (Validation<string list, int32>.Failure [ "Meow 1" ])
        (Validation<string list, int32>.Failure [ "Meow 2" ])
​
[<EntryPoint>]
let main _ =
    printResults()
    printValidations()
    0

Output:

"res1" is: 1
"res1" is: "failure!"
"val1" is: 1
"val1" is: ["Meow 1"; "Meow 2"]

Related information

  • Operating system: Windows 10 professional
  • Branch: master
  • .NET Runtime, CoreCLR or Mono Version: .NET 5.0 + F# 5.0 (lang preview enabled)
  • Performance information, links to performance testing scripts

natalie-o-perret avatar Nov 16 '20 13:11 natalie-o-perret

Actual behavior The value or constructor 'validation' is not defined.

Have you tried using monad instead of validation ?

gusty avatar Nov 16 '20 21:11 gusty

Actual behavior The value or constructor 'validation' is not defined.

Have you tried using monad instead of validation ?

Just did, so turns out this one does relate to: https://github.com/fsprojects/FSharpPlus/issues/388

Closing that one and creating the lists of the dedicated CEs in the other issue.

natalie-o-perret avatar Nov 17 '20 01:11 natalie-o-perret

I think if you use monad, intellisense will be happy, but you'll get the unfortunate unknown(1,1): error FS0073: internal error: Undefined or unsolved type variable: ^_?1831670 error.

Using monad' (since Validation is strict) seems to solve the issue.

gusty avatar Nov 17 '20 05:11 gusty

Well I got a 164685 when using the non-strict monad CE builder:

internal error: Undefined or unsolved type variable:  ^_?164685

but yea I guess the number doesn't really matter here, innit?

natalie-o-perret avatar Nov 17 '20 09:11 natalie-o-perret

No, it doesn't. You are getting that error because the runValidations is not inline, and it has to have the inline modifier because it has a static requirement for the + operator, which means it's generic over semigroups.

BTW: As discussed by slack, now with #425 you can do this:

#r "nuget: FSharpPlus,1.2.0-CI01958"

open FSharpPlus
open FSharpPlus.Data

let resultCE<'t,'e> = MonadFxStrictBuilder<Result<'t,'e>> ()

type AppicativeValidationBuilder<'e,'t> () =
    inherit MonadFxStrictBuilder<Validation<'e,'t>> ()
    member _.BindReturn (x, f: 'T -> 't) = Validation.map f x : Validation<'e,'t>

let validation<'e,'t> = AppicativeValidationBuilder<'e,'t> ()

in your sample code, it should work fine. Don't use result for the builder because it will pick the result function instead.

Let me know if you feel we can close this issue.

gusty avatar Feb 25 '21 11:02 gusty

No, it doesn't. You are getting that error because the runValidations is not inline, and it has to have the inline modifier because it has a static requirement for the + operator, which means it's generic over semigroups.

BTW: As discussed by slack, now with #425 you can do this:

#r "nuget: FSharpPlus,1.2.0-CI01958"

open FSharpPlus
open FSharpPlus.Data

let resultCE<'t,'e> = MonadFxStrictBuilder<Result<'t,'e>> ()

type AppicativeValidationBuilder<'e,'t> () =
    inherit MonadFxStrictBuilder<Validation<'e,'t>> ()
    member _.BindReturn (x, f: 'T -> 't) = Validation.map f x : Validation<'e,'t>

let validation<'e,'t> = AppicativeValidationBuilder<'e,'t> ()

in your sample code, it should work fine. Don't use result for the builder because it will pick the result function instead.

Let me know if you feel we can close this issue.

Like I said ydy, I have been running some tests tdy and so far, it looks promising! Will finish testing this coming weekend.

natalie-o-perret avatar Feb 25 '21 11:02 natalie-o-perret

No, it doesn't. You are getting that error because the runValidations is not inline, and it has to have the inline modifier because it has a static requirement for the + operator, which means it's generic over semigroups.

BTW: As discussed by slack, now with #425 you can do this:

#r "nuget: FSharpPlus,1.2.0-CI01958"

open FSharpPlus
open FSharpPlus.Data

let resultCE<'t,'e> = MonadFxStrictBuilder<Result<'t,'e>> ()

type AppicativeValidationBuilder<'e,'t> () =
    inherit MonadFxStrictBuilder<Validation<'e,'t>> ()
    member _.BindReturn (x, f: 'T -> 't) = Validation.map f x : Validation<'e,'t>

let validation<'e,'t> = AppicativeValidationBuilder<'e,'t> ()

in your sample code, it should work fine. Don't use result for the builder because it will pick the result function instead.

Let me know if you feel we can close this issue.

Is it still required to inherit from MonadFxStrictBuilder and add _.BindReturn in order to use applicative CE's with F#+ or has this been incorporated somewhere in the latest release?

If not would it be possible to add an applicative (or applicative') builder which extends the monad (or monad') builder with BindReturn?

Choc13 avatar Jul 08 '21 09:07 Choc13

Yes, that's the plan, either incorporate it in monad or create a separate applicative CE.

We started with the former but we found an F# bug which seems it will be solved soon.

Therefore we felt it was safer to wait until that bug is solved so we can evaluate properly which solution is the best, in the meantime adding the BindReturn method by hand should be the way to go.

gusty avatar Jul 08 '21 10:07 gusty

No, it doesn't. You are getting that error because the runValidations is not inline, and it has to have the inline modifier because it has a static requirement for the + operator, which means it's generic over semigroups. BTW: As discussed by slack, now with #425 you can do this:

#r "nuget: FSharpPlus,1.2.0-CI01958"

open FSharpPlus
open FSharpPlus.Data

let resultCE<'t,'e> = MonadFxStrictBuilder<Result<'t,'e>> ()

type AppicativeValidationBuilder<'e,'t> () =
    inherit MonadFxStrictBuilder<Validation<'e,'t>> ()
    member _.BindReturn (x, f: 'T -> 't) = Validation.map f x : Validation<'e,'t>

let validation<'e,'t> = AppicativeValidationBuilder<'e,'t> ()

in your sample code, it should work fine. Don't use result for the builder because it will pick the result function instead. Let me know if you feel we can close this issue.

Is it still required to inherit from MonadFxStrictBuilder and add _.BindReturn in order to use applicative CE's with F#+ or has this been incorporated somewhere in the latest release?

If not would it be possible to add an applicative (or applicative') builder which extends the monad (or monad') builder with BindReturn?

Fyi, this is what we have been using at my company for the past couple of months or so:

open FSharpPlus
open FSharpPlus.Data


let result'<'Ok,'Error> = MonadFxStrictBuilder<Result<'Ok,'Error>> ()
let result<'Ok,'Error> = MonadFxBuilder<Result<'Ok, 'Error>>()

let option'<'T> = MonadFxStrictBuilder<Option<'T>> ()
let option<'T> = MonadFxBuilder<Option<'T>> ()

let state'<'S, 'T> = MonadFxStrictBuilder<State<'S, 'T>> ()
let state<'S, 'T> = MonadFxBuilder<State<'S, 'T>> ()

type ApplicativeValidationStrictBuilder<'Failure,'Success> () =
    inherit MonadFxStrictBuilder<Validation<'Failure,'Success>> ()
    member _.BindReturn (x, f: 'T -> 'Success) = Validation.map f x : Validation<'Failure,'Success>

type ApplicativeValidationBuilder<'Failure,'Success> () =
    inherit MonadFxBuilder<Validation<'Failure,'Success>> ()
    member _.BindReturn (x, f: 'T -> 'Success) = Validation.map f x : Validation<'Failure,'Success>

let validation'<'Failure,'Success> = ApplicativeValidationStrictBuilder<'Failure,'Success> ()
let validation<'Failure,'Success> = ApplicativeValidationBuilder<'Failure,'Success> ()

natalie-o-perret avatar Jul 08 '21 10:07 natalie-o-perret

FWIW I've just got the following fairly generic version working

type ApplicativeBuilder<'a>() =
    inherit MonadFxStrictBuilder<'a>()
    member inline _.BindReturn(x, f) = map f x

let applicative = ApplicativeBuilder()

and using it like this

applicative {
    let! firstValue = getFirstValue()
    and! secondValue = getSecondValue()
    return
        { FirstValue = firstValue
          SecondValue = secondValue }
}

And in my case getFirstValue() etc were returning Validation<NonEmptyList<string>, _>

Choc13 avatar Jul 08 '21 10:07 Choc13

Cool, we should add a snippets section in the docs.

gusty avatar Jul 08 '21 11:07 gusty