├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── test.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── go.mod ├── internal └── assert │ └── assert.go ├── json.go ├── json_test.go ├── option.go └── option_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug 🐛]: " 5 | labels: "bug" 6 | assignees: "" 7 | --- 8 | 9 | **What happened?** 10 | 11 | _Provide a description of the issue._ 12 | 13 | **What did you expect to happen?** 14 | 15 | _Explain what you would have expected to happen instead._ 16 | 17 | **Which version of go-functional were you using?** 18 | 19 | _Provide the version number (such as `v1.0.0`)._ 20 | 21 | **To Reproduce** 22 | 23 | _Provide a code snippet or steps to reproduce the issue._ 24 | 25 | **Do you intend to fix this issue yourself?** 26 | 27 | _Yes or no. This is so that the maintainers know to leave you time to make a 28 | contribution rather than just fixing it themselves._ 29 | 30 | **Additional context** 31 | 32 | _Add any other context about the problem here._ 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature 🔨]: " 5 | labels: "enhancement" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always 12 | frustrated when [...]. 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Provide code snippets to show how this new feature might be used.** 19 | 20 | Ideally, provide an [`Example`](https://go.dev/blog/examples) test that 21 | demonstrates the feature's usage. 22 | 23 | **Does this incur a breaking change?** 24 | 25 | Yes / no. 26 | 27 | **Do you intend to build this feature yourself?** 28 | 29 | Yes / no (so that it's clear that maintainers should allow you to make a 30 | contribution). 31 | 32 | **Additional context** 33 | Add any other context or screenshots about the feature request here. 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: { interval: weekly } 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Please provide a brief description of the change.** 2 | 3 | A sentence or two is fine, the rest should be clear from the code change and related issue. 4 | 5 | **Which issue does this change relate to?** 6 | 7 | Please provide a link to the issue that this change resolved. 8 | 9 | If there is no such issue, consider creating one first. Discussions concerning proposed changes ought to take place in an issue and not in pull requests. Pull requests not associated with an issue are less likely to be merged and more likely to ask for changes. 10 | 11 | **Contribution checklist.** 12 | 13 | _Replace the space in each box with "X" to check it off._ 14 | 15 | - [ ] I have read and understood the CONTRIBUTING guidelines 16 | - [ ] My code is formatted (`make check`) 17 | - [ ] I have run tests (`make test`) 18 | - [ ] All commits in my PR conform to the commit hygiene section 19 | - [ ] I have added relevant tests 20 | - [ ] I have not added any dependencies 21 | 22 | **Additional context** 23 | 24 | _Add any other context about the problem here._ 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: { branches: [main] } 4 | pull_request: {} 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-go@v5 11 | with: 12 | go-version: "1.22" 13 | cache: false 14 | - run: make check 15 | env: { SKIP_LINT: true } 16 | - uses: golangci/golangci-lint-action@v6 17 | with: { version: "latest" } 18 | - run: make cov 19 | - uses: codecov/codecov-action@v5 20 | env: { CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" } 21 | test: 22 | strategy: { matrix: { go-version: ["1.21", "1.22"] } } 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-go@v5 27 | with: 28 | go-version: "${{ matrix.go-version }}" 29 | cache: false 30 | - run: make test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.txt 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behaviour that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behaviour include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behaviour and will take appropriate and fair corrective action in 43 | response to any behaviour that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 62 | reported to the community leaders responsible for enforcement at 63 | tomgodkin@pm.me. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behaviour deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behaviour was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behaviour. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behaviour. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behaviour, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to BooleanCat/option 2 | 3 | Contributions to this project are very welcome! This guide should help with 4 | instructions on how to submit changes. Contributions can be made in the form of 5 | GitHub [issues](https://github.com/BooleanCat/option/issues) or [pull 6 | requests](https://github.com/BooleanCat/option/pulls). 7 | 8 | When submitting an issue, please choose the relevant template or choose a blank 9 | issue if your query doesn't naturally fit into an existing template. 10 | 11 | ## TL;DR contribution checklist 12 | 13 | - [ ] My code is formatted (`make check`) 14 | - [ ] I have run tests (`make test`) 15 | - [ ] My code has no lint errors (`make lint`) 16 | - [ ] All commits in my PR conform to the commit hygiene section 17 | - [ ] I have added relevant tests 18 | - [ ] I have not added any dependencies 19 | 20 | ## Zero-dependency 21 | 22 | This project is a zero-dependency project - which means that consumers using 23 | this project's packages must only incur one dependency: `option`. 24 | 25 | Development dependencies are OK as they will not be included as dependencies to 26 | end-users (such as `golangci-lint`). 27 | 28 | ## Commit hygiene 29 | 30 | - Commits should contain only a single change 31 | - Commit messages must use imperative language (e.g. `Add iter.Fold collection 32 | function`) 33 | - Commit messages must explain what is changed, not how it is changed 34 | - The first line of a commit message should be a terse description of the change 35 | containing 72 characters or fewer 36 | 37 | ## Running tests 38 | 39 | Run tests with `make test` from the project root directory. 40 | 41 | Tests are written using Go's `testing` package and helpers are available in 42 | `internal/assert`. 43 | 44 | Code is linted using `golangci-lint`. The linter may be run using 45 | `make lint`. 46 | 47 | ## Different types of changes 48 | 49 | ### Bug fixes 50 | 51 | Bug reports are appreciated ahead of bug fixes as early reporting allows the 52 | community to be aware of any issues ahead of a fix being submitted. If you 53 | intend to fix a bug after reporting, that is greatly appreciated - just make 54 | sure to mention you intend to work on it on the issue report so the maintainers 55 | are aware and leave you the chance to make a contribution. 56 | 57 | When submitting a bug fix PR, a test must be added (or an existing test 58 | modified) that exposes the bug and your change must make that test pass. 59 | 60 | ### New features 61 | 62 | Issues should be opened ahead of submitting a PR to added a new feature. This is 63 | to prevent you wasting your time should a feature not be desirable and allows 64 | others to have input into the conversation. 65 | 66 | All new functionality must be fully tested and new public functions must include 67 | an [`Example` test](https://go.dev/blog/examples) that will be used by the 68 | reference docs to demonstrate its use. 69 | 70 | Mark pull requests as "Draft" if you intend to use the pull request as a 71 | workspace but are not yet ready to receive unsolicited feedback on specifics 72 | like commit messages or failing tests. 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tom Godkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .phony: test check lint cov 2 | 3 | check: lint 4 | @go vet ./... 5 | @gofmt -l . 6 | @test -z "$$( gofmt -l . )" 7 | 8 | lint: 9 | ifndef SKIP_LINT 10 | @golangci-lint run ./... 11 | endif 12 | 13 | test: 14 | go test -race -v ./... 15 | 16 | cov: SHELL:=/bin/bash 17 | cov: 18 | go test -race -coverprofile=coverage.txt -covermode=atomic $$( go list ./... | grep -v internal ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Optional Values in Go 2 | 3 | [![GitHub release (with filter)](https://img.shields.io/github/v/release/BooleanCat/option?sort=semver&logo=Go&color=%23007D9C)](https://github.com/BooleanCat/option/releases) [![Actions Status](https://github.com/BooleanCat/option/workflows/test/badge.svg)](https://github.com/BooleanCat/option/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/BooleanCat/option.svg)](https://pkg.go.dev/github.com/BooleanCat/option) [![Go Report Card](https://goreportcard.com/badge/github.com/BooleanCat/option)](https://goreportcard.com/report/github.com/BooleanCat/option) [![codecov](https://codecov.io/gh/BooleanCat/option/branch/main/graph/badge.svg?token=N2E43RSR14)](https://codecov.io/gh/BooleanCat/option) 4 | 5 | Support user-friendly, type-safe optionals in Go. 6 | 7 | ```go 8 | value = option.Some(4) 9 | no_value = option.None[int]() 10 | ``` 11 | 12 | _[Read the docs.](https://pkg.go.dev/github.com/BooleanCat/option)_ 13 | 14 | ## Usage 15 | 16 | This package adds a single type, the `Option`. `Option`'s are instantiated as 17 | one of two variants. `Some` denotes the presence of a value and `None` denotes 18 | the absence. 19 | 20 | Historically pointers have been used to denote optional values, this package 21 | removes the risk of null pointer exceptions by leveraging generics to implement 22 | type-safe optional values. 23 | 24 | `Options` can be tested for the presence of a value: 25 | 26 | ```go 27 | two = option.Some(2) 28 | if two.IsSome() { 29 | ... 30 | } 31 | ``` 32 | 33 | Values can be extracted along with a boolean test for their presence: 34 | 35 | ```go 36 | two = option.Some(2) 37 | if value, ok := two.Value(); ok { 38 | ... 39 | } 40 | ``` 41 | 42 | Optionals that you're sure have a value can be "unwrapped": 43 | 44 | ```go 45 | two := option.Some(2) 46 | two.Unwrap() // returns 2 47 | ``` 48 | 49 | Accessing a value on a `None` variant will cause a runtime panic. 50 | 51 | ```go 52 | none := option.None[int]() 53 | none.Unwrap() // panics 54 | ``` 55 | 56 | ## Ergonomics 57 | 58 | Use of a package like this may be pervasive if you really commit to it. This 59 | package was inspired by Rust's options implemenation. It might be worth 60 | considering dropping the repetative `option.` preceding the variants. Since 61 | importing names into the global namespace is to be avoided, the following 62 | import pattern may work for you: 63 | 64 | ```go 65 | import ( 66 | "fmt" 67 | 68 | "github.com/BooleanCat/option" 69 | ) 70 | 71 | var ( 72 | Some = option.Some 73 | None = option.None 74 | ) 75 | 76 | func main() { 77 | two := Some(2) 78 | fmt.Println(two) 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please send [an email](mailto:tomgodkin@pm.me) to the maintainer. 6 | 7 | Thank you for your report, genuine vulnerabilities reported shall grant you credit as a contributor. Please provide your GitHub username when submitting an issue if you wish to be credited when a new version is released that fixes the issue. 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/BooleanCat/option 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /internal/assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "testing" 4 | 5 | func Equal[T comparable](t *testing.T, a, b T) { 6 | t.Helper() 7 | 8 | if a != b { 9 | t.Errorf("expected `%v` to equal `%v`", a, b) 10 | } 11 | } 12 | 13 | func True(t *testing.T, b bool) { 14 | t.Helper() 15 | 16 | if !b { 17 | t.Error("expected `false` to be `true`") 18 | } 19 | } 20 | 21 | func False(t *testing.T, b bool) { 22 | t.Helper() 23 | 24 | if b { 25 | t.Error("expected `true` to be `false`") 26 | } 27 | } 28 | 29 | func Nil(t *testing.T, v interface{}) { 30 | t.Helper() 31 | 32 | if v != nil { 33 | t.Errorf("expected `%v` to equal `nil`", v) 34 | } 35 | } 36 | 37 | func NotNil(t *testing.T, v interface{}) { 38 | t.Helper() 39 | 40 | if v == nil { 41 | t.Error("expected `nil` not to equal `nil`") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | import "encoding/json" 4 | 5 | // MarshalJSON implements the [json.Marshaler] interface. 6 | // 7 | // - [Some] variants will be marshaled as their underlying value. 8 | // - [None] variants will be marshaled as "null". 9 | func (o Option[T]) MarshalJSON() ([]byte, error) { 10 | if value, ok := o.Value(); ok { 11 | return json.Marshal(value) 12 | } 13 | 14 | return []byte("null"), nil 15 | } 16 | 17 | // UnmarshalJSON implements the [json.Unmarshaler] interface. 18 | // 19 | // - Values will be unmarshaled as [Some] variants. 20 | // - "null"s will be unmarshaled as [None] variants. 21 | func (o *Option[T]) UnmarshalJSON(data []byte) error { 22 | *o = None[T]() 23 | 24 | if string(data) != "null" { 25 | var value T 26 | if err := json.Unmarshal(data, &value); err != nil { 27 | return err 28 | } 29 | *o = Some(value) 30 | } 31 | 32 | return nil 33 | } 34 | 35 | var ( 36 | _ json.Unmarshaler = new(Option[struct{}]) 37 | _ json.Marshaler = Option[struct{}]{} 38 | ) 39 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package option_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/BooleanCat/option" 8 | "github.com/BooleanCat/option/internal/assert" 9 | ) 10 | 11 | func TestMarshalSome(t *testing.T) { 12 | t.Parallel() 13 | 14 | data, err := json.Marshal(option.Some(4)) 15 | assert.Nil(t, err) 16 | assert.Equal(t, string(data), "4") 17 | } 18 | 19 | func TestMarshalNone(t *testing.T) { 20 | t.Parallel() 21 | 22 | data, err := json.Marshal(option.None[int]()) 23 | assert.Nil(t, err) 24 | assert.Equal(t, string(data), "null") 25 | } 26 | 27 | func TestMarshalSomeParsed(t *testing.T) { 28 | t.Parallel() 29 | 30 | type name struct { 31 | MiddleName option.Option[string] `json:"middle_name"` 32 | } 33 | 34 | data, err := json.Marshal(name{MiddleName: option.Some("Barry")}) 35 | assert.Nil(t, err) 36 | assert.Equal(t, string(data), `{"middle_name":"Barry"}`) 37 | } 38 | 39 | func TestMarshalNoneParsed(t *testing.T) { 40 | t.Parallel() 41 | 42 | type name struct { 43 | MiddleName option.Option[string] `json:"middle_name"` 44 | } 45 | 46 | data, err := json.Marshal(name{MiddleName: option.None[string]()}) 47 | assert.Nil(t, err) 48 | assert.Equal(t, string(data), `{"middle_name":null}`) 49 | } 50 | 51 | func TestUnmarshalSome(t *testing.T) { 52 | t.Parallel() 53 | 54 | var number option.Option[int] 55 | err := json.Unmarshal([]byte("4"), &number) 56 | assert.Nil(t, err) 57 | assert.Equal(t, number, option.Some(4)) 58 | } 59 | 60 | func TestUnmarshalNone(t *testing.T) { 61 | t.Parallel() 62 | 63 | var number option.Option[int] 64 | err := json.Unmarshal([]byte("null"), &number) 65 | assert.Nil(t, err) 66 | assert.True(t, number.IsNone()) 67 | } 68 | 69 | func TestUnmarshalEmpty(t *testing.T) { 70 | t.Parallel() 71 | 72 | type name struct { 73 | MiddleName option.Option[string] `json:"middle_name"` 74 | } 75 | 76 | var value name 77 | err := json.Unmarshal([]byte("{}"), &value) 78 | assert.Nil(t, err) 79 | assert.True(t, value.MiddleName.IsNone()) 80 | } 81 | 82 | func TestUnmarshalError(t *testing.T) { 83 | t.Parallel() 84 | 85 | var number option.Option[int] 86 | err := number.UnmarshalJSON([]byte("not a number")) 87 | assert.NotNil(t, err) 88 | assert.True(t, number.IsNone()) 89 | } 90 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | import "fmt" 4 | 5 | // Option represents an optional value. The [Some] variant contains a value and 6 | // the [None] variant represents the absence of a value. 7 | type Option[T any] struct { 8 | value T 9 | present bool 10 | } 11 | 12 | // Some instantiates an [Option] with a value. 13 | func Some[T any](value T) Option[T] { 14 | return Option[T]{value, true} 15 | } 16 | 17 | // None instantiates an [Option] with no value. 18 | func None[T any]() Option[T] { 19 | return Option[T]{} 20 | } 21 | 22 | // String implements the [fmt.Stringer] interface. 23 | func (o Option[T]) String() string { 24 | if o.present { 25 | return fmt.Sprintf("Some(%v)", o.value) 26 | } 27 | 28 | return "None" 29 | } 30 | 31 | var _ fmt.Stringer = Option[struct{}]{} 32 | 33 | // Unwrap returns the underlying value of a [Some] variant, or panics if called 34 | // on a [None] variant. 35 | func (o Option[T]) Unwrap() T { 36 | if o.present { 37 | return o.value 38 | } 39 | 40 | panic("called `Option.Unwrap()` on a `None` value") 41 | } 42 | 43 | // UnwrapOr returns the underlying value of a [Some] variant, or the provided 44 | // value on a [None] variant. 45 | func (o Option[T]) UnwrapOr(value T) T { 46 | if o.present { 47 | return o.value 48 | } 49 | 50 | return value 51 | } 52 | 53 | // UnwrapOrElse returns the underlying value of a [Some] variant, or the result 54 | // of calling the provided function on a [None] variant. 55 | func (o Option[T]) UnwrapOrElse(f func() T) T { 56 | if o.present { 57 | return o.value 58 | } 59 | 60 | return f() 61 | } 62 | 63 | // UnwrapOrZero returns the underlying value of a [Some] variant, or the zero 64 | // value on a [None] variant. 65 | func (o Option[T]) UnwrapOrZero() T { 66 | if o.present { 67 | return o.value 68 | } 69 | 70 | var value T 71 | return value 72 | } 73 | 74 | // IsSome returns true if the [Option] is a [Some] variant. 75 | func (o Option[T]) IsSome() bool { 76 | return o.present 77 | } 78 | 79 | // IsNone returns true if the [Option] is a [None] variant. 80 | func (o Option[T]) IsNone() bool { 81 | return !o.present 82 | } 83 | 84 | // Value returns the underlying value and true for a [Some] variant, or the 85 | // zero value and false for a [None] variant. 86 | func (o Option[T]) Value() (T, bool) { 87 | return o.value, o.present 88 | } 89 | 90 | // Expect returns the underlying value for a [Some] variant, or panics with the 91 | // provided message for a [None] variant. 92 | func (o Option[T]) Expect(message string) T { 93 | if o.present { 94 | return o.value 95 | } 96 | 97 | panic(message) 98 | } 99 | -------------------------------------------------------------------------------- /option_test.go: -------------------------------------------------------------------------------- 1 | package option_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/BooleanCat/option" 8 | "github.com/BooleanCat/option/internal/assert" 9 | ) 10 | 11 | func ExampleOption_Unwrap() { 12 | fmt.Println(option.Some(4).Unwrap()) 13 | // Output: 4 14 | } 15 | 16 | func ExampleOption_UnwrapOr() { 17 | fmt.Println(option.Some(4).UnwrapOr(3)) 18 | fmt.Println(option.None[int]().UnwrapOr(3)) 19 | // Output: 20 | // 4 21 | // 3 22 | } 23 | 24 | func ExampleOption_UnwrapOrElse() { 25 | fmt.Println(option.Some(4).UnwrapOrElse(func() int { 26 | return 3 27 | })) 28 | 29 | fmt.Println(option.None[int]().UnwrapOrElse(func() int { 30 | return 3 31 | })) 32 | 33 | // Output: 34 | // 4 35 | // 3 36 | } 37 | 38 | func ExampleOption_UnwrapOrZero() { 39 | fmt.Println(option.Some(4).UnwrapOrZero()) 40 | fmt.Println(option.None[int]().UnwrapOrZero()) 41 | 42 | // Output 43 | // 4 44 | // 0 45 | } 46 | 47 | func ExampleOption_IsSome() { 48 | fmt.Println(option.Some(4).IsSome()) 49 | fmt.Println(option.None[int]().IsSome()) 50 | 51 | // Output: 52 | // true 53 | // false 54 | } 55 | 56 | func ExampleOption_IsNone() { 57 | fmt.Println(option.Some(4).IsNone()) 58 | fmt.Println(option.None[int]().IsNone()) 59 | 60 | // Output: 61 | // false 62 | // true 63 | } 64 | 65 | func ExampleOption_Value() { 66 | value, ok := option.Some(4).Value() 67 | fmt.Println(value) 68 | fmt.Println(ok) 69 | 70 | // Output: 71 | // 4 72 | // true 73 | } 74 | 75 | func ExampleOption_Expect() { 76 | fmt.Println(option.Some(4).Expect("oops")) 77 | 78 | // Output: 4 79 | } 80 | 81 | func TestSomeStringer(t *testing.T) { 82 | t.Parallel() 83 | 84 | assert.Equal(t, fmt.Sprintf("%s", option.Some("foo")), "Some(foo)") //nolint:gosimple 85 | assert.Equal(t, fmt.Sprintf("%s", option.Some(42)), "Some(42)") //nolint:gosimple 86 | } 87 | 88 | func TestNoneStringer(t *testing.T) { 89 | t.Parallel() 90 | 91 | assert.Equal(t, fmt.Sprintf("%s", option.None[string]()), "None") //nolint:gosimple 92 | } 93 | 94 | func TestSomeUnwrap(t *testing.T) { 95 | t.Parallel() 96 | 97 | assert.Equal(t, option.Some(42).Unwrap(), 42) 98 | } 99 | 100 | func TestNoneUnwrap(t *testing.T) { 101 | t.Parallel() 102 | 103 | defer func() { 104 | assert.Equal(t, fmt.Sprint(recover()), "called `Option.Unwrap()` on a `None` value") 105 | }() 106 | 107 | option.None[string]().Unwrap() 108 | t.Error("did not panic") 109 | } 110 | 111 | func TestSomeUnwrapOr(t *testing.T) { 112 | t.Parallel() 113 | 114 | assert.Equal(t, option.Some(42).UnwrapOr(3), 42) 115 | } 116 | 117 | func TestNoneUnwrapOr(t *testing.T) { 118 | t.Parallel() 119 | 120 | assert.Equal(t, option.None[int]().UnwrapOr(3), 3) 121 | } 122 | 123 | func TestSomeUnwrapOrElse(t *testing.T) { 124 | t.Parallel() 125 | 126 | assert.Equal(t, option.Some(42).UnwrapOrElse(func() int { return 41 }), 42) 127 | } 128 | 129 | func TestNoneUnwrapOrElse(t *testing.T) { 130 | t.Parallel() 131 | 132 | assert.Equal(t, option.None[int]().UnwrapOrElse(func() int { return 41 }), 41) 133 | } 134 | 135 | func TestSomeUnwrapOrZero(t *testing.T) { 136 | t.Parallel() 137 | 138 | assert.Equal(t, option.Some(42).UnwrapOrZero(), 42) 139 | } 140 | 141 | func TestNoneUnwrapOrZero(t *testing.T) { 142 | t.Parallel() 143 | 144 | assert.Equal(t, option.None[int]().UnwrapOrZero(), 0) 145 | } 146 | 147 | func TestIsSome(t *testing.T) { 148 | t.Parallel() 149 | 150 | assert.True(t, option.Some(42).IsSome()) 151 | assert.False(t, option.None[int]().IsSome()) 152 | } 153 | 154 | func TestIsNone(t *testing.T) { 155 | t.Parallel() 156 | 157 | assert.False(t, option.Some(42).IsNone()) 158 | assert.True(t, option.None[int]().IsNone()) 159 | } 160 | 161 | func TestSomeValue(t *testing.T) { 162 | t.Parallel() 163 | 164 | value, ok := option.Some(42).Value() 165 | assert.Equal(t, value, 42) 166 | assert.True(t, ok) 167 | } 168 | 169 | func TestNoneValue(t *testing.T) { 170 | t.Parallel() 171 | 172 | value, ok := option.None[int]().Value() 173 | assert.Equal(t, value, 0) 174 | assert.False(t, ok) 175 | } 176 | 177 | func TestSomeExpect(t *testing.T) { 178 | t.Parallel() 179 | 180 | assert.Equal(t, option.Some(42).Expect("oops"), 42) 181 | } 182 | 183 | func TestNoneExpect(t *testing.T) { 184 | t.Parallel() 185 | 186 | defer func() { 187 | assert.Equal(t, fmt.Sprint(recover()), "oops") 188 | }() 189 | 190 | option.None[int]().Expect("oops") 191 | t.Error("did not panic") 192 | } 193 | --------------------------------------------------------------------------------