4
\$\begingroup\$

The following code is an example of railway-oriented programming that allows a password string to be validated:

let (|SpecialChar|_|) (str : string) =
 let chars = [| '!'; '@'; '#' |]
 str.IndexOfAny(chars) >= 0
let (|AlphaNumeric|_|) (str : string) =
 Array.tryFind System.Char.IsLetterOrDigit (str.ToCharArray())
let lengthBeAtLeast8 (passwords : string) =
 match passwords.Length with
 | length when length < 8 -> Error "Passwords must be at least 8 characters"
 | _ -> Ok passwords
let containsAlphaNumeric (password : string) =
 match password with
 | AlphaNumeric _ -> Ok password
 | _ -> Error "Password must contain alphanumeric characters"
let containsSpecialCharacters (password : string) =
 match password with
 | SpecialChar -> Ok password
 | _ -> Error "Password must contain special characters"
let notBePassword (password : string) =
 match password with
 | "P@ssword" -> Error "Choose something else"
 | _ -> Ok password
// string -> Result<string,string>
let validatePasswordRailway =
 lengthBeAtLeast8
 >> Result.bind containsAlphaNumeric
 >> Result.bind containsSpecialCharacters
 >> Result.bind notBePassword

I could also use lazy seq to achieve the same:

let lengthBeAtLeast8Opt (passwords : string) =
 match passwords.Length with
 | length when length < 8 -> Some "Passwords must be at least 8 characters"
 | _ -> None
let containsAlphaNumericOpt (password : string) =
 match password with
 | AlphaNumeric _ -> None
 | _ -> Some "Password must contain alphanumeric characters"
let containsSpecialCharactersOpt (password : string) =
 match password with
 | SpecialChar -> None
 | _ -> Some "Password must contain special characters"
let notBePasswordOpt (password : string) =
 match password with
 | "P@ssword" -> Some "Choose something else"
 | _ -> None
let passwordValidations = seq {
 lengthBeAtLeast8Opt
 containsAlphaNumericOpt
 containsSpecialCharactersOpt
 notBePasswordOpt
}
// string -> string option
let validatePasswordOpt password =
 passwordValidations
 |> Seq.tryPick (fun f -> f password)

Which style is recommended? Am I correct that the seq version can potentially run faster?

toolic
14.4k5 gold badges29 silver badges201 bronze badges
asked Jan 12 at 11:21
\$\endgroup\$
2
  • \$\begingroup\$ Are both code blocks written or maintained by you? \$\endgroup\$ Commented Jan 12 at 19:02
  • 1
    \$\begingroup\$ New to F#, this I wrote just as an example. \$\endgroup\$ Commented Jan 12 at 19:05

2 Answers 2

5
\$\begingroup\$

Software engineers strive to satisfy a business need by writing small understandable functions that compose nicely. Using DbC with exceptions is one way to accomplish that, and railway-oriented programming is another way. We might, for example,

  • receive a POST request
  • do a DB query
  • INSERT a new row containing queried data
  • log that a new row was inserted
  • send a confirmation email
  • log that the email was sent

If a step fails, it doesn't make sense to continue with the rest, so we should bail out early with a helpful diagnostic.

railway switches

Scott Wlaschin mentions several reasons to avoid such a style, which I won't repeat here, and also a few situations where it can be a good fit:

  • Domain Errors
  • Panics
  • Infrastructure Errors

business problem

@Gebb correctly points out that this particular password use case maybe isn't the best fit for trying out the technique, as a UI should gather up all issues rather than bail out early. This reduces user frustration so all issues may be fixed before the next click on the submit button.

web-safe URLs

Having just three special chars seems to account for an unusually small amount of entropy being injected into the shared secret. It is especially surprising that _ underscore and - dash are left out. If there is some other goal at work here, like wanting only characters that may safely appear in URLs, then be sure to write a comment to that effect. Future maintainers will thank you. The OP code will happily accept e.g. unicode emojis, which I assume is intended.

consistent guards

nit: Some of the matches handle the Ok happy path first, and alas some instead report Error first. To the extent that we can easily invert a condition, such as length >= 8, consider preferring a consistent order, for the convenience of maintainers reading this code.

which style

Which style is recommended?

It's a matter of opinion and personal preference. Mine is for the seq version since it omits the red herring of returning password as though that value had been carefully computed. After all, we just have a glorified boolean here. Neither style is appreciably faster, since they both short-circuit. Both avoid evaluating later checks as soon as an earlier check fails.

Passing around a Some string works fine, but I feel it would be more self documenting to create an app-specific Error thin wrapper for strings, to clarify that it is pejorative, it is a Bad Thing being returned. Of course, this starts to get at Wlaschin's advice about "do not reinvent exceptions!". The diagnostic message wrapper would be much less powerful than true exceptions, since they alter control flow and give an informative stack trace.

nit: Maybe introducing a separate passwordValidations is a bit on the verbose side? The expression in validatePasswordRailway seems succinct and expressive.

answered Jan 12 at 18:36
\$\endgroup\$
0
3
\$\begingroup\$

Neither, I would say. It's more user friendly to accumulate errors and show them all to the user (or the client calling your API). Your implementation returns the first error detected, making the user potentially discover the next violation in the next call.

Consider implementing validation via an applicative functor, as described here.

An excerpt from that article:

let validateDateOfBirth input = // string -> Result<DateTime, 
ValidationFailure list>
 match input with
 | IsValidDate dob -> Ok dob //Add logic for DOB
 | _ -> Error [ DateOfBirthIsInvalidFailure ]
let apply fResult xResult = // Result<('a -> 'b), 
'c list> -> Result<'a,'c list> -> Result<'b,'c list>
 match fResult,xResult with
 | Ok f, Ok x -> Ok (f x)
 | Error ex, Ok _ -> Error ex
 | Ok _, Error ex -> Error ex
 | Error ex1, Error ex2 -> Error (List.concat [ex1; ex2])
let (<!>) = Result.map
let (<*>) = apply
let create name email dateOfBirth =
 { Name = name; Email = email; DateOfBirth = dateOfBirth }
let validate (input:UnvalidatedUser) : Result<ValidatedUser,
ValidationFailure list> =
 let validatedName = input.Name |> validateName
 let validatedEmail = input.Email |> validateEmail
 let validatedDateOfBirth = input.DateOfBirth |> 
validateDateOfBirth
 create <!> validatedName <*> validatedEmail 
<*> validatedDateOfBirth
let notValidTest = 
 let actual = validate' { Name = ""; Email = "hello"; 
DateOfBirth = "" }
 let expected = Error [ NameIsInvalidFailure; 
EmailIsInvalidFailure; DateOfBirthIsInvalidFailure ]
 expected = actual
toolic
14.4k5 gold badges29 silver badges201 bronze badges
answered Jan 12 at 16:15
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.