diff --git a/concepts/results/.meta/config.json b/concepts/results/.meta/config.json new file mode 100644 index 000000000..60cce2b46 --- /dev/null +++ b/concepts/results/.meta/config.json @@ -0,0 +1,8 @@ +{ + "blurb": "Learn how to handle errors in a type-safe manner with the Result type", + "authors": [ + "blackk-foxx" + ], + "contributors": [ + ] +} diff --git a/concepts/results/about.md b/concepts/results/about.md new file mode 100644 index 000000000..e001b3574 --- /dev/null +++ b/concepts/results/about.md @@ -0,0 +1,74 @@ +# About + +The `Result` type makes it possible for a function to return a single value indicating all of the following things: +- Whether the operation succeeded or failed +- On success, the resulting value of the operation +- On failure, the reason for the failure + +With other programming languages that don't support something like a `Result` type, it is common to find the following patterns to accomplish the aforementioned goals: +- A function returning a numeric code indicating success or the reason for the failure and requiring an output parameter to accept the value on success. +- A function returning the value on success or NULL on error, and then a different function to get the error code indicating the reason for the failure. + +Another common error-handling mechanism is the exception, which abruptly breaks out of the current function and +transfers control to the first handler in the call stack when a failure occurs. +If no handler "catches" the exception, then the program aborts. + +## Benefits of using the `Result` type + +* __Compile-time safety__: By making the success or failure of a function call explicit in the type system, the compiler can ensure that calling code handles all of the failure cases, preventing a large category of bugs that could occur with nulls. + +* __Run-time safety__: Instead of using `null`, the `Result` type uses `Error` represent a failure, making it impossible for a `NullReferenceException` to occur. + +* __Explicit error handling__: Code that calls a function returning a `Result` must acknowledge that the +call can fail; calling code must handle such failures intentionally. There are oo silent failures and no surprise exceptions. + +* __Predictable control flow__: A `Result` is returned just like any other value; it does not jump out of the call stack like an exception. You always know where errors originate and how they propagate. + +* __Improved readability and maintainability__: By returning a `Result`, a function's signature clearly indicates the expected behavior on success and failure. + +## Usage + +The `Result` type is a generic type containing two underlying types: +- The type of the resultant value on a successful operation +- The type representing the reason for the failure on a failure + +The `Result` type is also a [discriminated union][discriminated-union] with the following possible cases: +* `Ok ` representing a successful result +* `Error ` representing a failure + +The following function demonstrates how to create a `Result` value: + +```fsharp +let validateName (name: string) : Result = + match name with + | null -> Error "Name not found." + | "" -> Error "Name is empty." + | _ -> Ok name +``` + +In this example, the `Ok` value is a string (the given name), and the `Error` value is also a string (the cause of the error, in human-readable form). + +## Reading the content of a `Result` value + +Consider the following type definition and function signature: + +``` +type FileOpenError = +| NotFound +| AccessDenied +| FileLocked + +let openFile (filename: string) : Result = +``` + +Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: + +```fsharp +match openFile(filename) with +| Ok handle -> doSomethingWithFile(handle) +| Error NotFound -> printfn $"Error: file {filename} was not found." +| Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." +| Error FileLocked -> printfn $"Error: file {filename} is already in use." +``` + +[discriminated-union]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions diff --git a/concepts/results/introduction.md b/concepts/results/introduction.md new file mode 100644 index 000000000..00c2839ea --- /dev/null +++ b/concepts/results/introduction.md @@ -0,0 +1,51 @@ +# Introduction + +The `Result` type makes it possible for a function to return a single value indicating all of the following things: +- Whether the operation succeeded or failed +- On success, the resulting value of the operation +- On failure, the reason for the failure + +## Usage + +The `Result` type is a generic type containing two underlying types: +- The type of the resultant value on a successful operation +- The type representing the reason for the failure on a failure + +The `Result` type is also a [discriminated union][discriminated-union] with the following possible cases: +* `Ok ` representing a successful result +* `Error ` representing a failure + +The following function demonstrates how to create a `Result` value: + +```fsharp +let validateName (name: string) : Result = + match name with + | null -> Error "Name not found." + | "" -> Error "Name is empty." + | _ -> Ok name +``` + +In this example, the `Ok` value is a string (the given name), and the `Error` value is also a string (the cause of the error, in human-readable form). + +## Reading the content of a `Result` value + +Consider the following type definition and function signature: + +``` +type FileOpenError = +| NotFound +| AccessDenied +| FileLocked + +let openFile (filename: string) : Result = +``` + +Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: + +```fsharp +match openFile(filename) with +| Ok handle -> doSomethingWithFile(handle) +| Error NotFound -> printfn $"Error: file {filename} was not found." +| Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." +| Error FileLocked -> printfn $"Error: file {filename} is already in use." +``` diff --git a/concepts/results/links.json b/concepts/results/links.json new file mode 100644 index 000000000..892487c5b --- /dev/null +++ b/concepts/results/links.json @@ -0,0 +1,6 @@ +[ + { + "url": "https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/results", + "description": "F# language reference on the Result type" + } +] diff --git a/config.json b/config.json index c95b77ec4..49a0de676 100644 --- a/config.json +++ b/config.json @@ -213,6 +213,18 @@ "records", "tuples" ] + }, + { + "slug": "password-checker", + "name": "Password Checker", + "uuid": "a5a67ac7-df9f-48ea-aeb8-2af3738f0577", + "concepts": [ + "results" + ], + "prerequisites": [ + "basics", + "pattern-matching" + ] } ], "practice": [ @@ -2366,6 +2378,11 @@ "slug": "recursion", "name": "Recursion" }, + { + "uuid": "c25353f4-9fc1-4fd9-978b-a7449af4d584", + "slug": "results", + "name": "Results" + }, { "uuid": "8a3e23fd-aa42-42c3-9dbd-c26159fd6774", "slug": "strings", diff --git a/exercises/concept/password-checker/.docs/hints.md b/exercises/concept/password-checker/.docs/hints.md new file mode 100644 index 000000000..e69de29bb diff --git a/exercises/concept/password-checker/.docs/instructions.md b/exercises/concept/password-checker/.docs/instructions.md new file mode 100644 index 000000000..8b6b6802b --- /dev/null +++ b/exercises/concept/password-checker/.docs/instructions.md @@ -0,0 +1,38 @@ +# Instructions + +Your task is to create a password checker. +A password checker validates a user's proposed password to ensure that it meets a set of requirements defined by the organization that controls access to the given resource. + +For this exercise, the password requirements are: +* Must have 12 or more characters +* Must have at least one uppercase letter +* Must have at least one lowercase letter +* Must have at least one digit +* Must have at least one symbol in the set !@#$%^&* + +Your solution must use a `Result` to encapsulate the success or failure status. +For the success case, the `Result` must convey the validated password as a string. +For the failure case, the `Result` must convey the rule that was violated in the failure case. + +~~~~exercism/note +For this exercise, the password checker will be simplistic -- it will indicate only when a single rule has been violated. +A subsequent exercise will explore a more realistic password checker that can indicate when multiple rules have been violated at the same time. +~~~~ + +## 1. Implement the `checkPassword` function + +The `checkPassword` function checks the given password against the aforementioned rules. On failure, it indicates the rule that was violated by encapsulating one of the `PasswordRule` values within the result value. + +```fsharp +checkPassword "abcdefghij5#" +// => Error MissingUppercaseLetter +``` + +## 2. Implement the ``getStatusMessage` function + +The `getStatusMessage` function returns a string containing a human-readable message indicating the meaning of the result returned from `checkPassword`. + +```fsharp +getStatusMessage (Error MissingDigit) +// => "Error: does not have at least one digit" +``` diff --git a/exercises/concept/password-checker/.docs/introduction.md b/exercises/concept/password-checker/.docs/introduction.md new file mode 100644 index 000000000..63b60cf1e --- /dev/null +++ b/exercises/concept/password-checker/.docs/introduction.md @@ -0,0 +1,51 @@ +# Introduction + +The `Result` type makes it possible for a function to return a single value indicating all of the following things: +- Whether the operation succeeded or failed +- On success, the resulting value of the operation +- On failure, the reason for the failure + +## Usage + +The `Result` type is a generic type containing two underlying types: +- The type of the resultant value on a successful operation +- The type of error on a failure + +The `Result` type is also a [discriminated union][discriminated-union] with the following possible cases: +* `Ok ` representing a successful result +* `Error ` representing a failure + +The following function demonstrates how to create a `Result` value: + +```fsharp +let validateName (name: string) : Result = + match name with + | null -> Error "Name not found." + | "" -> Error "Name is empty." + | _ -> Ok name +``` + +In this example, the `Ok` value is a string (the given name), and the `Error` value is also a string (the cause of the error, in human-readable form). + +## Reading the content of a `Result` value + +Consider the following type definition and function signature: + +``` +type FileOpenError = +| NotFound +| AccessDenied +| FileLocked + +let openFile (filename: string) : Result = +``` + +Code that calls the `openFile` function can use pattern matching to handle the success and failure cases, as in the following example: + +```fsharp +match openFile(filename) with +| Ok handle -> doSomethingWithFile(handle) +| Error NotFound -> printfn $"Error: file {filename} was not found." +| Error AccessDenied -> printfn $"Error: you do not have permission to open the file {filename}." +| Error FileLocked -> printfn $"Error: file {filename} is already in use." +``` diff --git a/exercises/concept/password-checker/.meta/Exemplar.fs b/exercises/concept/password-checker/.meta/Exemplar.fs new file mode 100644 index 000000000..1b7915da9 --- /dev/null +++ b/exercises/concept/password-checker/.meta/Exemplar.fs @@ -0,0 +1,35 @@ +module PasswordChecker + +type PasswordError = + | LessThan12Characters + | MissingUppercaseLetter + | MissingLowercaseLetter + | MissingDigit + | MissingSymbol + +/// Validate the given password against the rules defined in the instructions. If it meets all +/// of the rules, return a result indicating success; otherwise return a result indicating +/// failure and an error indicating which rule was violated. +let checkPassword (password: string) : Result = + if password.Length < 12 then + Error LessThan12Characters + elif password |> String.exists System.Char.IsUpper |> not then + Error MissingUppercaseLetter + elif password |> String.exists System.Char.IsLower |> not then + Error MissingLowercaseLetter + elif password |> String.exists System.Char.IsDigit |> not then + Error MissingDigit + elif password |> String.exists (fun c -> "!@#$%^&*".Contains c) |> not then + Error MissingSymbol + else Ok password + +/// Return a human-readable message indicating the meaning of the given result value. +let getStatusMessage (result: Result) : string = + let preamble = "Error: does not have at least " + match result with + | Error LessThan12Characters -> preamble + "12 characters" + | Error MissingUppercaseLetter -> preamble + "one uppercase letter" + | Error MissingLowercaseLetter -> preamble + "one lowercase letter" + | Error MissingDigit -> preamble + "one digit" + | Error MissingSymbol -> preamble + "one symbol" + | Ok _ -> "OK" diff --git a/exercises/concept/password-checker/.meta/config.json b/exercises/concept/password-checker/.meta/config.json new file mode 100644 index 000000000..a13a06700 --- /dev/null +++ b/exercises/concept/password-checker/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "blackk-foxx" + ], + "files": { + "solution": [ + "PasswordChecker.fs" + ], + "test": [ + "PasswordCheckerTests.fs" + ], + "exemplar": [ + ".meta/Exemplar.fs" + ], + "invalidator": [ + "PasswordChecker.fsproj" + ] + }, + "blurb": "Learn how to use the Result type to convey success/failure results" +} diff --git a/exercises/concept/password-checker/.meta/design.md b/exercises/concept/password-checker/.meta/design.md new file mode 100644 index 000000000..c6b1281ce --- /dev/null +++ b/exercises/concept/password-checker/.meta/design.md @@ -0,0 +1,19 @@ +# Design + +## Goal + +The goal of this exercise is to teach students about success/failure values enabled by the `Result` type and what you can do with them. + +## Learning objectives + +- Know of the existence of the `Result` type. +- Know how to create a `Result` value. +- Know how to pattern match on the success and failure cases conveyed in a `Result` value. + +## Out of scope + +- The generic concept of pattern matching; this exercise focuses pattern matching with `Result` patterns. + +## Concepts + +- `results` diff --git a/exercises/concept/password-checker/PasswordChecker.fs b/exercises/concept/password-checker/PasswordChecker.fs new file mode 100644 index 000000000..033ca82da --- /dev/null +++ b/exercises/concept/password-checker/PasswordChecker.fs @@ -0,0 +1,18 @@ +module PasswordChecker + +type PasswordError = + | LessThan12Characters + | MissingUppercaseLetter + | MissingLowercaseLetter + | MissingDigit + | MissingSymbol + +/// Validate the given password against the rules defined in the instructions. If it meets all +/// of the rules, return a result indicating success; otherwise return a result indicating +/// failure and an error indicating which rule was violated. +let checkPassword (password: string) : Result = + failwith "Please implement this function" + +/// Return a human-readable message indicating the meaning of the given result value. +let getStatusMessage (result: Result) : string = + failwith "Please implement this function" diff --git a/exercises/concept/password-checker/PasswordChecker.fsproj b/exercises/concept/password-checker/PasswordChecker.fsproj new file mode 100644 index 000000000..75f8a5818 --- /dev/null +++ b/exercises/concept/password-checker/PasswordChecker.fsproj @@ -0,0 +1,22 @@ + + + + net9.0 + + false + + + + + + + + + + + + + + + + diff --git a/exercises/concept/password-checker/PasswordCheckerTests.fs b/exercises/concept/password-checker/PasswordCheckerTests.fs new file mode 100644 index 000000000..26abe744d --- /dev/null +++ b/exercises/concept/password-checker/PasswordCheckerTests.fs @@ -0,0 +1,77 @@ +module PasswordCheckerTests + +open FsUnit.Xunit +open Xunit +open Exercism.Tests + +open PasswordChecker + +[] +[] +let ``Error when password too short`` () = + let expected: Result = Error LessThan12Characters + checkPassword "@bcd3fghijK" |> should equal expected + +[] +[] +let ``Error when password has no uppercase letters`` () = + let expected: Result = Error MissingUppercaseLetter + checkPassword "@bcd3fghijkl" |> should equal expected + +[] +[] +let ``Error when password has no lowercase letters`` () = + let expected: Result = Error MissingLowercaseLetter + checkPassword "@BCD3FGHIJKL" |> should equal expected + +[] +[] +let ``Error when password has no digits`` () = + let expected: Result = Error MissingDigit + checkPassword "@bcdefghijkL" |> should equal expected + +[] +[] +let ``Error when password has no symbols`` () = + let expected: Result = Error MissingSymbol + checkPassword "abcd3fghijkL" |> should equal expected + +[] +[] +[] +[] +[] +[] +let ``Ok when password is good`` (password: string) = + let expected: Result = Ok password + checkPassword password |> should equal expected + +[] +[] +let ``Insufficient length error message`` () = + getStatusMessage (Error LessThan12Characters) |> should equal "Error: does not have at least 12 characters" + +[] +[] +let ``Missing uppercase error message`` () = + getStatusMessage (Error MissingUppercaseLetter) |> should equal "Error: does not have at least one uppercase letter" + +[] +[] +let ``Missing lowercase error message`` () = + getStatusMessage (Error MissingLowercaseLetter) |> should equal "Error: does not have at least one lowercase letter" + +[] +[] +let ``Missing digit error message`` () = + getStatusMessage (Error MissingDigit) |> should equal "Error: does not have at least one digit" + +[] +[] +let ``Missing symbol error message`` () = + getStatusMessage (Error MissingSymbol) |> should equal "Error: does not have at least one symbol" + +[] +[] +let ``OK message`` () = + getStatusMessage (Ok "foo") |> should equal "OK"