Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# - Nested types

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
    }
like image 755
Bremewoord Avatar asked Oct 19 '25 19:10

Bremewoord


1 Answers

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.

like image 170
Mark Seemann Avatar answered Oct 21 '25 19:10

Mark Seemann



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!