Home > other >  F# - Nested types
F# - Nested types

Time:09-17

I'm trying to understand functional programming using F# and to do so I started small project, but I run in following issue and can't seem to find any elegant solution for it. I created Validation<'a> which is pretty much specialized F# Result: Result<'a, Error list> which helps me to handle validation results.

I have two functions that perform some validation both with signatures:

'a -> Validation<'b>

There is also a third function that consumes validated arguments with signature:

'a -> 'b -> Validation<'c>

What I would like to achieve is to:

  1. Validate argument 'a
  2. If validation of argument 'a passes, validate argument 'b
  3. If validation of argument 'b passes, provide arguments 'a and 'b to final function

Thus far I used apply function to achieve such behaviour, but when I try to use it in this case the result type is nested Validation Validation<Validation<'c>>, since final function itself returns Validation. I would like to get rid of one of validations, so that result type would be Validation<'c>. I tried to experiment with bind and variants of lift functions which I found here, but result remains the same. Is nested match an only option here?

Edit #1: Here is a simplified code that I currently have:

Here are types that handle validation:

[<Struct>]
type Error = {
    Message: string
    Code: int
}
    
type Validation<'a> =
    | Success of 'a
    | Failure of Error list

let apply elevatedFunction elevatedValue =
    match elevatedFunction, elevatedValue with
    | Success func, Success value -> Success (func value)
    | Success _, Failure errors -> Failure errors
    | Failure errors, Success _ -> Failure errors
    | Failure currentErrors, Failure newErrors -> Failure (currentErrors@newErrors)

let (<*>) = apply

Problematic function is this one:

let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport): Validation<Validation<string>> =
    Success formatReportAsText
    <*> languageTranslatorFor unvalidatedLanguageName
    <*> reportFrom unvalidatedReport

Validation functions:

let languageTranslatorFor (unvalidatedLanguageName: string): Validation<Entry -> string> = ...

let reportFrom (unvalidatedReport: UnvalidatedReport): Validation<Report> = ...

Function that consumes validation arguments:

let formatReportAsText (languageTranslator: Entry -> string) (report: Report): Validation<string> = ...

Edit #2: I attempted to use solution provided by @brianberns and implemented computation expression for Validation<'a> type:

// Validation<'a> -> Validation<'b> -> Validation<'a * 'b>
let zip firstValidation secondValidation =
    match firstValidation, secondValidation with
    | Success firstValue, Success secondValue -> Success(firstValue, secondValue)
    | Failure errors, Success _ -> Failure errors
    | Success _, Failure errors -> Failure errors
    | Failure firstErrors, Failure secondErrors -> Failure (firstErrors @ secondErrors)

// Validation<'a> -> ('a -> 'b) -> Validation<'b>
let map elevatedValue func =
    match elevatedValue with
    | Success value -> Success(func value)
    | Failure validationErrors -> Failure validationErrors

type MergeValidationBuilder() =
    member _.BindReturn(validation: Validation<'a>, func) = Validation.map validation func
        
    member _.MergeSources(validation1, validation2) = Validation.zip validation1 validation2
    
let validate = MergeValidationBuilder()

and use it as such:

let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport): Validation<Validation<string>> =
    validate = {
        let! translator = languageTranslatorFor unvalidatedLanguageName
        and! report = reportFrom unvalidatedReport

        return formatReportAsText translator report
    }

While computation expression is definitely nicer to read the end result remains exactly the same [Validation<Validation>] due to fact that "formatReportAsText" function also returns result wrapped in Validation. To somewhat merge stacked validations I used below function, but it seems clunky to me:

// Validation<Validation<'a>> -> Validation<'a>
let merge (nestedValidation: Validation<Validation<'a>>): Validation<'a> =
    match nestedValidation with
    | Success innerValidation ->
        match innerValidation with
        | Success value -> Success value
        | Failure innerErrors -> Failure innerErrors
    | Failure outerErrors -> Failure outerErrors

Edit #3: After addition of "ReturnFrom" function to validation computation expression to flatten nested validations the validation function works as intended.

member _.ReturnFrom(validation) = validation

The final version of validation function that uses computation expression is:

let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport): Validation<string> =
    validate = {
        let! translator = languageTranslatorFor unvalidatedLanguageName
        and! report = reportFrom unvalidatedReport

        return! formatReportAsText translator report
    }

CodePudding user response:

There are lots of ways to skin that cat, but central to most of them is that whenever you encounter a nested container like Validation<Validation<string>>, you'll need some way to 'flatten' the nesting. For a type like Validation, that's easy:

// Validation<Validation<'a>> -> Validation<'a>
let join = function
    | Success x -> x
    | Failure errors -> Failure errors

You might also choose to call this function flatten, but it's often called join.

You may also find a map function useful. This one is also easy:

// ('a -> 'b) -> Validation<'a> -> Validation<'b>
let map f = function
    | Success x -> Success (f x)
    | Failure errors -> Failure errors

Such a map function makes Validation a functor.

When you have both map and join, you can always implement a method usually known as bind:

// ('a -> Validation<'b>) -> Validation<'a> -> Validation<'b>
let bind f = map f >> join

The ability to flatten or join a nested container is what makes it a monad. While it's a word surrounded by mystique and awe, it's really just that: it's a functor you can flatten.

Usually, however, join and bind are defined the other way around:

let bind f = function
    | Success x -> f x
    | Failure errors -> Failure errors

let join x = bind id x

With join you can adjust the problematic function by flattening the nested container:

// string -> UnvalidatedReport -> Validation<string>
let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport) =
    Success formatReportAsText
    <*> languageTranslatorFor unvalidatedLanguageName
    <*> reportFrom unvalidatedReport
    |> join

That's not how I'd do it, however. While such combinator gymnastics can be fun, they' aren't always the most readable solution.

Computation expression

I'd prefer defining a Computation Expression, which can minimally be done like this:

type ValidationBuilder () =
    member _.Bind (x, f) = bind f x
    member _.ReturnFrom x = x

let validate = ValidationBuilder ()

This now enables you to write the desired function like this:

// string -> UnvalidatedReport -> Validation<string>
let formatReport (unvalidatedLanguageName: string) (unvalidatedReport: UnvalidatedReport) =
    validate {
        let! l = languageTranslatorFor unvalidatedLanguageName
        let! r = reportFrom unvalidatedReport
        return! formatReportAsText l r
    }

The problem with this version, however, is that it doesn't use the apply function to append to errors together. In other words, it'll short-circuit on the first error it encounters.

In order to support collecting errors without short-circuiting, you're going to need a computation builder that supports applicative functors, like @brianberns pointed to. You can also see an example here.

CodePudding user response:

First of all, I think you are getting into a fairly advanced area of F# - but the most practical solution would be to use a computation builder as referenced in the previous answer linked by @brianberns in the comment.

If you wanted to stick to the somewhat simpler approach based on combinators, you could do this using the following functions:

val merge : Validation<'a> -> Validation<'b> -> Validation<'a * 'b>
val bind : ('a -> Validation<'b>) -> Validation<'a> -> Validation<'b>

Merge is a function that takes two possibly validated values and produces a new one that combines the errors (as your original apply function). The Bind function applies a function to a validated value and collapses the "nested" validation in the result. They can be implemented as:

let merge elevatedValue1 elevatedValue2 = 
    match elevatedValue1, elevatedValue2 with 
    | Success v1, Success v2 -> Success (v1, v2)
    | Success _, Failure errors -> Failure errors
    | Failure errors, Success _ -> Failure errors
    | Failure e1, Failure e2 -> Failure (e1 @ e2)

let bind f elevatedValue =
    match elevatedValue with
    | Success value -> 
        match f value with 
        | Success value -> Success value
        | Failure e -> Failure e
    | Failure e -> Failure e

Thanks to merge, you can validate the two inputs and (possibly) merge the errors. Thanks to bind, you can then continue the computation and handle the fact that the rest of it may also fail. You can write your composed function as:

let formatReport (unvalidatedLanguageName: string) 
      (unvalidatedReport: UnvalidatedReport): Validation<string> =
  merge 
    (languageTranslatorFor unvalidatedLanguageName)
    (reportFrom unvalidatedReport)
  |> bind formatReportAsText 

CodePudding user response:

Since formatReportAsText returns a validated string (rather than a plain string), you should use return! instead of return at the end of your computation expression:

return! formatReportAsText translator report

This is equivalent to:

let! value = formatReportAsText translator report   // value is a string
return value

If I understand your code correctly, the type of the computation expression will then be Validation<string> instead of Validation<Validation<string>>.

Note that you'll need a ReturnFrom method on your builder to make return! work:

member __.ReturnFrom(value) = value

See this page for details.

  • Related