-
-
Notifications
You must be signed in to change notification settings - Fork 111
Add PasswordChecker exercise for Result concept #1368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bb5e4eb
db1f99a
6c36302
578eb92
5d2949b
07b001b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "blurb": "Learn how to handle errors in a type-safe manner with the Result type", | ||
| "authors": [ | ||
| "blackk-foxx" | ||
| ], | ||
| "contributors": [ | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <value>` representing a successful result | ||
| * `Error <reason>` representing a failure | ||
|
|
||
| The following function demonstrates how to create a `Result` value: | ||
|
|
||
| ```fsharp | ||
| let validateName (name: string) : Result<string, string> = | ||
blackk-foxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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<int, FileOpenError> = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am guessing that the doc check is failing on this line (but it is hard to tell since the error message does not indicate a line number). Any suggestions on how to remedy this? I could add a function body like
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the only way to workaround that would be exclude this entire file. The code used is in https://github.com/exercism/fsharp/tree/main/tools/CodeFenceChecker
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the |
||
| ``` | ||
|
|
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <value>` representing a successful result | ||
| * `Error <reason>` representing a failure | ||
|
|
||
| The following function demonstrates how to create a `Result` value: | ||
|
|
||
| ```fsharp | ||
| let validateName (name: string) : Result<string, string> = | ||
| 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<int, FileOpenError> = | ||
| ``` | ||
|
|
||
| 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." | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [ | ||
| { | ||
| "url": "https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/results", | ||
| "description": "F# language reference on the Result type" | ||
| } | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add an example here? Tasks should ideally provide an example of a call, see https://github.com/exercism/fsharp/blob/main/exercises/concept/bird-watcher/.docs/instructions.md for an example
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That means the code won't be syntax highlighted though. But it's fine for now |
||
| ```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`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
|
||
| ```fsharp | ||
| getStatusMessage (Error MissingDigit) | ||
| // => "Error: does not have at least one digit" | ||
| ``` | ||
blackk-foxx marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would mention that it is a discriminated union
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| - 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 <value>` representing a successful result | ||
| * `Error <reason>` representing a failure | ||
|
|
||
| The following function demonstrates how to create a `Result` value: | ||
|
|
||
| ```fsharp | ||
| let validateName (name: string) : Result<string, string> = | ||
| 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<int, FileOpenError> = | ||
| ``` | ||
|
|
||
| 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." | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, PasswordError> = | ||
| 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, PasswordError>) : 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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, PasswordError> = | ||
| failwith "Please implement this function" | ||
|
|
||
| /// Return a human-readable message indicating the meaning of the given result value. | ||
| let getStatusMessage (result: Result<string, PasswordError>) : string = | ||
| failwith "Please implement this function" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net9.0</TargetFramework> | ||
|
|
||
| <IsPackable>false</IsPackable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Compile Include="PasswordChecker.fs" /> | ||
| <Compile Include="PasswordCheckerTests.fs" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" /> | ||
| <PackageReference Include="xunit" Version="2.4.1" /> | ||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> | ||
| <PackageReference Include="FsUnit.xUnit" Version="4.0.4" /> | ||
| <PackageReference Include="Exercism.Tests" Version="0.1.0-beta1" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably also mention that it is a discriminated union, maybe even include the definition here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.