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
}