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:
- Validate argument 'a
- If validation of argument 'a passes, validate argument 'b
- 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.