diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..39cbd20 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,26 @@ +name: Go + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18.0-beta1 + stable: false + + - name: Build + run: make build + + - name: Test + run: make test diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..cc35c28 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,45 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v2 + with: + go-version: 1.18.0-beta1 + stable: false + - uses: actions/checkout@v2 + - name: golangci-lint + uses: golangci/golangci-lint-action@v2 + with: + # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version + version: latest + + # Optional: working directory, useful for monorepos + working-directory: ./ + + # Optional: golangci-lint command line arguments. + args: --timeout 60s --max-same-issues 50 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # Optional: if set to true then the action will use pre-installed Go. + # skip-go-installation: true + + # Optional: if set to true then the action don't cache or restore ~/go/pkg. + # skip-pkg-cache: true + + # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. + # skip-build-cache: true + + # optionally use a specific version of Go rather than the latest one + go_version: '1.18.0-beta1' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3aa3a0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Go Patch ### +/vendor/ +/Godeps/ + +# End of https://www.toptal.com/developers/gitignore/api/go + +cover.out +cover.html +.vscode diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ceb1708 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ + +FROM golang:1.18-bullseye + +WORKDIR /go/src/github.com/samber/mo + +COPY Makefile go.* ./ + +RUN make tools diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c3dc72d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Samuel Berthe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bb48b51 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ + +BIN=go + +build: + ${BIN} build -v ./... + +test: + go test -v ./... +watch-test: + reflex -t 50ms -s -- sh -c 'gotest -v ./...' + +bench: + go test -benchmem -count 3 -bench ./... +watch-bench: + reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...' + +coverage: + ${BIN} test -v -coverprofile cover.out . + ${BIN} tool cover -html=cover.out -o cover.html + +# tools +tools: + ${BIN} install github.com/cespare/reflex@latest + ${BIN} install github.com/rakyll/gotest@latest + ${BIN} install github.com/psampaz/go-mod-outdated@latest + ${BIN} install github.com/jondot/goweight@latest + ${BIN} install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + ${BIN} get -t -u golang.org/x/tools/cmd/cover + ${BIN} get -t -u github.com/sonatype-nexus-community/nancy@latest + go mod tidy + +lint: + golangci-lint run --timeout 60s --max-same-issues 50 ./... +lint-fix: + golangci-lint run --timeout 60s --max-same-issues 50 --fix ./... + +audit: + ${BIN} mod tidy + ${BIN} list -json -m all | nancy sleuth + +outdated: + ${BIN} mod tidy + ${BIN} list -u -m -json all | go-mod-outdated -update -direct + +weight: + goweight diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4b24bf --- /dev/null +++ b/README.md @@ -0,0 +1,336 @@ +# mo - Monads + +[![tag](https://img.shields.io/github/tag/samber/mo.svg)](https://github.com/samber/mo/releases) +[![codecov](https://codecov.io/gh/samber/mo/branch/master/graph/badge.svg)](https://codecov.io/gh/samber/mo) +![Build Status](https://github.com/samber/mo/actions/workflows/go.yml/badge.svg) +[![GoDoc](https://godoc.org/github.com/samber/mo?status.svg)](https://pkg.go.dev/github.com/samber/mo) +[![Go report](https://goreportcard.com/badge/github.com/samber/mo)](https://goreportcard.com/report/github.com/samber/mo) + +✨ **`mo` brings monads and populars FP abstractions to Go projects. `mo` uses the recent Go 1.18+ Generics.** + +**Inspired by:** + +- Scala +- Rust +- FP-TS + +**See also:** + +- [samber/lo](https://github.com/samber/lo): A Lodash-style Go library based on Go 1.18+ Generics +- [samber/do](https://github.com/samber/do): A dependency injection toolkit based on Go 1.18+ Generics + +### Why this name? + +I love **short name** for such utility library. This name is similar to "functional Go" and no Go package currently uses this name. + +## 💡 Features + +We currently support the following data types: + +- `Option[T]` (Maybe) +- `Result[T]` +- `Either[A, B]` +- `Future[T]` +- `IO[T]` +- `IOEither[T]` +- `Task[T]` +- `TaskEither[T]` +- `State[S, A]` + +## 🚀 Install + +```sh +go get github.com/samber/mo@v1 +``` + +This library is v1 and follows SemVer strictly. + +No breaking changes will be made to exported APIs before v2.0.0. + +## 💡 Quick start + +You can import `mo` using: + +```go +import ( + "github.com/samber/mo" +) +``` + +Then use one of the helpers below: + +```go +option1 := mo.Some(42) +// Some(42) + +option1. + FlatMap(func (value int) Option[int] { + return Some(value*2) + }). + FlatMap(func (value int) Option[int] { + return Some(value%2) + }). + FlatMap(func (value int) Option[int] { + return Some(value+21) + }). + OrElse(1234) +// 21 + +option2 := mo.None[int]() +// None + +option2.OrElse(1234) +// 1234 + +option3 := option1.Match( + func(i int) (int, bool) { + // when value is present + return i * 2, true + }, + func() (int, bool) { + // when value is absent + return 0, false + } +) +// Some(42) +``` + +More examples in [documentation](https://godoc.org/github.com/samber/mo). + +## 🤠 Documentation and examples + +[GoDoc: https://godoc.org/github.com/samber/mo](https://godoc.org/github.com/samber/mo) + +### Option[T any] + +`Option` is a container for an optional value of type `T`. If value exists, `Option` is of type `Some`. If the value is absent, `Option` is of type `None`. + +Constructors: + +- mo.Some() +- mo.None() +- mo.TupleToOption() + +Methods: + +- .IsPresent() +- .IsAbsent() +- .Size() +- .Get() +- .MustGet() +- .OrElse() +- .OrEmpty() +- .ForEach() +- .Match() +- .Map() +- .MapNone() +- .FlatMap() + +### Result[T any] + +`Result` respresent a result of an action having one of the following output: success or failure. An instance of `Result` is an instance of either `Ok` or `Err`. + +Constructors: + +- mo.Ok() +- mo.Err() +- mo.TupleToResult() + +Methods: + +- .IsOk() +- .IsError() +- .Error() +- .Get() +- .MustGet() +- .OrElse() +- .OrEmpty() +- .ToEither() +- .ForEach() +- .Match() +- .Map() +- .MapErr() +- .FlatMap() + +### Either[L any, R any] + +`Either` respresents a value of 2 possible types. An instance of `Either` is an instance of either `A` or `B`. + +Constructors: + +- mo.Left() +- mo.Right() + +Methods: + +- .IsLeft() +- .IsRight() +- .Left() +- .Right() +- .MustLeft() +- .MustRight() +- .LeftOrElse() +- .RightOrElse() +- .LeftOrEmpty() +- .RightOrEmpty() +- .Swap() +- .ForEach() +- .Match() +- .MapLeft() +- .MapRight() + +### Future[T any] + +`Future` represents a value which may or may not currently be available, but will be available at some point, or an exception if that value could not be made available. + +Constructors: + +- mo.NewFuture() + +Methods: + +- .Then() +- .Catch() +- .Finally() +- .Collect() +- .Result() +- .Cancel() + +### IO[T any] + +`IO` represents a non-deterministic synchronous computation that can cause side effects, yields a value of type `R` and never fails. + +Constructors: + +- mo.NewIO() +- mo.NewIO1() +- mo.NewIO2() +- mo.NewIO3() +- mo.NewIO4() +- mo.NewIO5() + +Methods: + +- .Run() + +### IOEither[T any] + +`IO` represents a non-deterministic synchronous computation that can cause side effects, yields a value of type `R` and can fail. + +Constructors: + +- mo.NewIOEither() +- mo.NewIOEither1() +- mo.NewIOEither2() +- mo.NewIOEither3() +- mo.NewIOEither4() +- mo.NewIOEither5() + +Methods: + +- .Run() + +### Task[T any] + +`Task` represents a non-deterministic asynchronous computation that can cause side effects, yields a value of type `R` and never fails. + +Constructors: + +- mo.NewTask() +- mo.NewTask1() +- mo.NewTask2() +- mo.NewTask3() +- mo.NewTask4() +- mo.NewTask5() +- mo.NewTaskFromIO() +- mo.NewTaskFromIO1() +- mo.NewTaskFromIO2() +- mo.NewTaskFromIO3() +- mo.NewTaskFromIO4() +- mo.NewTaskFromIO5() + +Methods: + +- .Run() + +### TaskEither[T any] + +`TaskEither` represents a non-deterministic asynchronous computation that can cause side effects, yields a value of type `R` and can fail. + +Constructors: + +- mo.NewTaskEither() +- mo.NewTaskEitherFromIOEither() + +Methods: + +- .Run() +- .OrElse() +- .Match() +- .TryCatch() +- .ToTask() +- .ToEither() + +### State[S any, A any] + +`State` represents a function `(S) -> (A, S)`, where `S` is state, `A` is result. + +Constructors: + +- mo.NewState() +- mo.ReturnState() + +Methods: + +- .Run() +- .Get() +- .Modify() +- .Put() + +## 🛩 Benchmark + +// @TODO + +This library does not use `reflect` package. We don't expect overhead. + +## 🤝 Contributing + +- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :)) +- Fork the [project](https://github.com/samber/mo) +- Fix [open issues](https://github.com/samber/mo/issues) or request new features + +Don't hesitate ;) + +### With Docker + +```bash +docker-compose run --rm dev +``` + +### Without Docker + +```bash +# Install some dev dependencies +make tools + +# Run tests +make test +# or +make watch-test +``` + +## 👤 Authors + +- Samuel Berthe + +## 💫 Show your support + +Give a ⭐️ if this project helped you! + +[![support us](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/samber) + +## 📝 License + +Copyright © 2022 [Samuel Berthe](https://github.com/samber). + +This project is [MIT](./LICENSE) licensed. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5139336 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3' + +services: + dev: + build: . + volumes: + - ./:/go/src/github.com/samber/mo + working_dir: /go/src/github.com/samber/mo + command: bash -c 'make tools ; make watch-test' diff --git a/either.go b/either.go new file mode 100644 index 0000000..251405d --- /dev/null +++ b/either.go @@ -0,0 +1,166 @@ +package mo + +import "fmt" + +var eitherShouldBeLeftOrRight = fmt.Errorf("either should be Left or Right") +var eitherMissingLeftValue = fmt.Errorf("no such Left value") +var eitherMissingRightValue = fmt.Errorf("no such Right value") + +// Left builds the left side of the Either struct, as opposed to the Right side. +func Left[L any, R any](value L) Either[L, R] { + return Either[L, R]{ + left: value, + isLeft: true, + isRight: false, + } +} + +// Right builds the right side of the Either struct, as opposed to the Left side. +func Right[L any, R any](value R) Either[L, R] { + return Either[L, R]{ + right: value, + isLeft: false, + isRight: true, + } +} + +// Either respresents a value of 2 possible types. +// An instance of Either is an instance of either A or B. +type Either[L any, R any] struct { + left L + right R + + isLeft bool + isRight bool +} + +// IsLeft returns true if Either is an instance of Left. +func (e Either[L, R]) IsLeft() bool { + return e.isLeft +} + +// IsRight returns true if Either is an instance of Right. +func (e Either[L, R]) IsRight() bool { + return e.isRight +} + +// Left returns left value of a Either struct. +func (e Either[L, R]) Left() (L, bool) { + if e.isLeft { + return e.left, true + } + return empty[L](), false +} + +// Right returns right value of a Either struct. +func (e Either[L, R]) Right() (R, bool) { + if e.isRight { + return e.right, true + } + return empty[R](), false +} + +// MustLeft returns left value of a Either struct or panics. +func (e Either[L, R]) MustLeft() L { + if !e.isLeft { + panic(eitherMissingLeftValue) + } + + return e.left +} + +// MustRight returns right value of a Either struct or panics. +func (e Either[L, R]) MustRight() R { + if !e.isRight { + panic(eitherMissingRightValue) + } + + return e.right +} + +// LeftOrElse returns left value of a Either struct or fallback. +func (e Either[L, R]) LeftOrElse(fallback L) L { + if e.isLeft { + return e.left + } + + return fallback +} + +// RightOrElse returns right value of a Either struct or fallback. +func (e Either[L, R]) RightOrElse(fallback R) R { + if e.isRight { + return e.right + } + + return fallback +} + +// LeftOrEmpty returns left value of a Either struct or empty value. +func (e Either[L, R]) LeftOrEmpty() L { + if e.isLeft { + return e.left + } + + return empty[L]() +} + +// RightOrEmpty returns right value of a Either struct or empty value. +func (e Either[L, R]) RightOrEmpty() R { + if e.isRight { + return e.right + } + + return empty[R]() +} + +// Swap returns the left value in Right and vice versa. +func (e Either[L, R]) Swap() Either[R, L] { + if e.isLeft { + return Right[R, L](e.left) + } + + return Left[R, L](e.right) +} + +// ForEach executes the given side-effecting function, depending of value is Left or Right. +func (e Either[L, R]) ForEach(a func(L), b func(R)) { + if e.isLeft { + a(e.left) + } else if e.isRight { + b(e.right) + } +} + +// Match executes the given function, depending of value is Left or Right, and returns result. +func (e Either[L, R]) Match(onLeft func(L) Either[L, R], onRight func(R) Either[L, R]) Either[L, R] { + if e.isLeft { + return onLeft(e.left) + } else if e.isRight { + return onRight(e.right) + } + + panic(eitherShouldBeLeftOrRight) +} + +// MapLeft executes the given function, if Either is of type Left, and returns result. +func (e Either[L, R]) MapLeft(mapper func(L) Either[L, R]) Either[L, R] { + if e.isLeft { + return mapper(e.left) + } else if e.isRight { + return Right[L, R](e.right) + } + + panic(eitherShouldBeLeftOrRight) +} + +// MapRight executes the given function, if Either is of type Right, and returns result. +func (e Either[L, R]) MapRight(mapper func(R) Either[L, R]) Either[L, R] { + if e.isLeft { + return Left[L, R](e.left) + } else if e.isRight { + return mapper(e.right) + } + + panic(eitherShouldBeLeftOrRight) +} diff --git a/either_example_test.go b/either_example_test.go new file mode 100644 index 0000000..122f5ce --- /dev/null +++ b/either_example_test.go @@ -0,0 +1,350 @@ +package mo + +import "fmt" + +func ExampleLeft() { + left := Left[string, int]("hello") + result1 := left.LeftOrElse("world") + result2 := left.RightOrElse(1234) + + fmt.Println(result1, result2) + // Output: hello 1234 +} + +func ExampleRight() { + right := Right[string, int](42) + result1 := right.LeftOrElse("world") + result2 := right.RightOrElse(1234) + + fmt.Println(result1, result2) + // Output: world 42 +} + +func ExampleEither_IsLeft_left() { + left := Left[string, int]("hello") + result := left.IsLeft() + + fmt.Println(result) + // Output: true +} + +func ExampleEither_IsLeft_right() { + right := Right[string, int](42) + result := right.IsLeft() + + fmt.Println(result) + // Output: false +} + +func ExampleEither_IsRight_left() { + left := Left[string, int]("hello") + result := left.IsRight() + + fmt.Println(result) + // Output: false +} + +func ExampleEither_IsRight_right() { + right := Right[string, int](42) + result := right.IsRight() + + fmt.Println(result) + // Output: true +} + +func ExampleEither_Left_left() { + left := Left[string, int]("hello") + result, ok := left.Left() + + fmt.Println(result) + fmt.Println(ok) + // Output: + // hello + // true +} + +func ExampleEither_Left_right() { + right := Right[string, int](42) + result, ok := right.Left() + + fmt.Println(result) + fmt.Println(ok) + // Output: + // false +} + +func ExampleEither_Right_left() { + left := Left[string, int]("hello") + result, ok := left.Right() + + fmt.Println(result) + fmt.Println(ok) + // Output: + // 0 + // false +} + +func ExampleEither_Right_right() { + right := Right[string, int](42) + result, ok := right.Right() + + fmt.Println(result) + fmt.Println(ok) + // Output: + // 42 + // true +} + +func ExampleEither_MustLeft_left() { + left := Left[string, int]("hello") + result := left.MustLeft() + + fmt.Println(result) + // Output: hello +} + +// func ExampleEither_MustLeft_right() { +// right := Right[string, int](42) +// result := right.MustLeft() + +// fmt.Println(result) +// // Output: panics +// } + +// func ExampleEither_MustRight_left() { +// left := Left[string, int]("hello") +// result := left.MustRight() + +// fmt.Println(result) +// // Output: panics +// } + +func ExampleEither_MustRight_right() { + right := Right[string, int](42) + result := right.MustRight() + + fmt.Println(result) + // Output: 42 +} + +func ExampleEither_LeftOrElse_left() { + left := Left[string, int]("hello") + result := left.LeftOrElse("world") + + fmt.Println(result) + // Output: hello +} + +func ExampleEither_LeftOrElse_right() { + right := Right[string, int](42) + result := right.LeftOrElse("world") + + fmt.Println(result) + // Output: world +} + +func ExampleEither_RightOrElse_left() { + left := Left[string, int]("hello") + result := left.RightOrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleEither_RightOrElse_right() { + right := Right[string, int](42) + result := right.RightOrElse(1234) + + fmt.Println(result) + // Output: 42 +} + +func ExampleEither_LeftOrEmpty_left() { + left := Left[string, int]("hello") + result := left.LeftOrEmpty() + + fmt.Println(result) + // Output: hello +} + +func ExampleEither_LeftOrEmpty_right() { + right := Right[string, int](42) + result := right.LeftOrEmpty() + + fmt.Println(result) + // Output: +} + +func ExampleEither_RightOrEmpty_left() { + left := Left[string, int]("hello") + result := left.RightOrEmpty() + + fmt.Println(result) + // Output: 0 +} + +func ExampleEither_RightOrEmpty_right() { + right := Right[string, int](42) + result := right.RightOrEmpty() + + fmt.Println(result) + // Output: 42 +} + +func ExampleEither_Swap_left() { + left := Left[string, int]("hello") + right := left.Swap() + result1, ok1 := right.Left() + result2, ok2 := right.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // 0 + // false + // hello + // true +} + +func ExampleEither_Swap_right() { + right := Right[string, int](42) + left := right.Swap() + result1, ok1 := left.Left() + result2, ok2 := left.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // 42 + // true + // + // false +} + +func ExampleEither_Match_left() { + left := Left[string, int]("hello") + result := left.Match( + func(s string) Either[string, int] { + return Right[string, int](1234) + }, + func(i int) Either[string, int] { + return Right[string, int](i * 42) + }, + ) + result1, ok1 := result.Left() + result2, ok2 := result.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // false + // 1234 + // true +} +func ExampleEither_Match_right() { + right := Right[string, int](42) + result := right.Match( + func(s string) Either[string, int] { + return Left[string, int]("world") + }, + func(i int) Either[string, int] { + return Left[string, int]("foobar") + }, + ) + result1, ok1 := result.Left() + result2, ok2 := result.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // foobar + // true + // 0 + // false +} + +func ExampleEither_MapLeft_left() { + left := Left[string, int]("hello") + result := left.MapLeft( + func(s string) Either[string, int] { + return Right[string, int](1234) + }, + ) + result1, ok1 := result.Left() + result2, ok2 := result.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // false + // 1234 + // true +} +func ExampleEither_MapLeft_right() { + right := Right[string, int](42) + result := right.MapLeft( + func(s string) Either[string, int] { + return Left[string, int]("world") + }, + ) + result1, ok1 := result.Left() + result2, ok2 := result.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // false + // 42 + // true +} + +func ExampleEither_MapRight_left() { + left := Left[string, int]("hello") + result := left.MapRight( + func(i int) Either[string, int] { + return Right[string, int](1234) + }, + ) + result1, ok1 := result.Left() + result2, ok2 := result.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // hello + // true + // 0 + // false +} +func ExampleEither_MapRight_right() { + right := Right[string, int](42) + result := right.MapRight( + func(i int) Either[string, int] { + return Right[string, int](1234) + }, + ) + result1, ok1 := result.Left() + result2, ok2 := result.Right() + + fmt.Println(result1) + fmt.Println(ok1) + fmt.Println(result2) + fmt.Println(ok2) + // Output: + // false + // 1234 + // true +} diff --git a/either_test.go b/either_test.go new file mode 100644 index 0000000..22e67ae --- /dev/null +++ b/either_test.go @@ -0,0 +1,201 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEitherLeft(t *testing.T) { + is := assert.New(t) + + left := Left[int, bool](42) + is.Equal(Either[int, bool]{left: 42, right: false, isLeft: true, isRight: false}, left) +} + +func TestEitherRight(t *testing.T) { + is := assert.New(t) + + right := Right[int, bool](true) + is.Equal(Either[int, bool]{left: 0, right: true, isLeft: false, isRight: true}, right) +} + +func TestEitherIsLeftOrRight(t *testing.T) { + is := assert.New(t) + + left := Left[int, bool](42) + right := Right[int, bool](true) + + is.True(left.IsLeft()) + is.False(left.IsRight()) + is.False(right.IsLeft()) + is.True(right.IsRight()) +} + +func TestEitherLeftOrRight(t *testing.T) { + is := assert.New(t) + + left := Left[int, bool](42) + right := Right[int, bool](true) + + result1, ok1 := left.Left() + result2, ok2 := left.Right() + result3, ok3 := right.Left() + result4, ok4 := right.Right() + + is.Equal(42, result1) + is.True(ok1) + is.Equal(false, result2) + is.False(ok2) + is.Equal(0, result3) + is.False(ok3) + is.Equal(true, result4) + is.True(ok4) +} + +func TestEitherMustLeftOrRight(t *testing.T) { + is := assert.New(t) + + left := Left[int, bool](42) + right := Right[int, bool](true) + + is.NotPanics(func() { + is.Equal(42, left.MustLeft()) + }) + is.Panics(func() { + left.MustRight() + }) + is.Panics(func() { + right.MustLeft() + }) + is.NotPanics(func() { + is.Equal(true, right.MustRight()) + }) +} + +func TestEitherGetOrElse(t *testing.T) { + is := assert.New(t) + + left := Left[int, string](42) + right := Right[int, string]("foobar") + + is.Equal(42, left.LeftOrElse(21)) + is.Equal(21, right.LeftOrElse(21)) + is.Equal("baz", left.RightOrElse("baz")) + is.Equal("foobar", right.RightOrElse("baz")) +} + +func TestEitherGetOrEmpty(t *testing.T) { + is := assert.New(t) + + left := Left[int, string](42) + right := Right[int, string]("foobar") + + is.Equal(42, left.LeftOrEmpty()) + is.Equal(0, right.LeftOrEmpty()) + is.Equal("", left.RightOrEmpty()) + is.Equal("foobar", right.RightOrEmpty()) +} + +func TestEitherSwap(t *testing.T) { + is := assert.New(t) + + left := Left[int, string](42) + right := Right[int, string]("foobar") + + is.Equal(Either[string, int]{left: "", right: 42, isLeft: false, isRight: true}, left.Swap()) + is.Equal(Either[string, int]{left: "foobar", right: 0, isLeft: true, isRight: false}, right.Swap()) +} + +func TestEitherForEach(t *testing.T) { + is := assert.New(t) + + Left[int, string](42).ForEach( + func(a int) { + is.Equal(42, a) + }, + func(b string) { + is.Fail("should not enter here") + }, + ) + + Right[int, string]("foobar").ForEach( + func(a int) { + is.Fail("should not enter here") + }, + func(b string) { + is.Equal("foobar", b) + }, + ) +} + +func TestEitherMatch(t *testing.T) { + is := assert.New(t) + + e1 := Left[int, string](42).Match( + func(a int) Either[int, string] { + is.Equal(42, a) + return Left[int, string](21) + }, + func(b string) Either[int, string] { + is.Fail("should not enter here") + return Left[int, string](1) + }, + ) + + e2 := Right[int, string]("foobar").Match( + func(a int) Either[int, string] { + is.Fail("should not enter here") + return Right[int, string]("baz") + }, + func(b string) Either[int, string] { + is.Equal("foobar", b) + return Right[int, string]("plop") + }, + ) + + is.Equal(Either[int, string]{left: 21, right: "", isLeft: true, isRight: false}, e1) + is.Equal(Either[int, string]{left: 0, right: "plop", isLeft: false, isRight: true}, e2) +} + +func TestEitherMapLeft(t *testing.T) { + is := assert.New(t) + + e1 := Left[int, string](42).MapLeft( + func(a int) Either[int, string] { + is.Equal(42, a) + return Left[int, string](21) + }, + ) + + e2 := Right[int, string]("foobar").MapLeft( + func(a int) Either[int, string] { + is.Fail("should not enter here") + return Right[int, string]("plop") + }, + ) + + is.Equal(Either[int, string]{left: 21, right: "", isLeft: true, isRight: false}, e1) + is.Equal(Either[int, string]{left: 0, right: "foobar", isLeft: false, isRight: true}, e2) +} + +func TestEitherMapRight(t *testing.T) { + is := assert.New(t) + + e1 := Left[int, string](42).MapRight( + func(b string) Either[int, string] { + is.Fail("should not enter here") + return Left[int, string](21) + }, + ) + + e2 := Right[int, string]("foobar").MapRight( + func(b string) Either[int, string] { + is.Equal("foobar", b) + return Right[int, string]("plop") + }, + ) + + is.Equal(Either[int, string]{left: 42, right: "", isLeft: true, isRight: false}, e1) + is.Equal(Either[int, string]{left: 0, right: "plop", isLeft: false, isRight: true}, e2) +} diff --git a/future.go b/future.go new file mode 100644 index 0000000..c69d383 --- /dev/null +++ b/future.go @@ -0,0 +1,187 @@ +package mo + +import ( + "sync" +) + +type Resolver[T any] func(T) +type Rejection func(error) +type ThenCb[T any] func(T) (T, error) +type CatchCb[T any] func(error) (T, error) +type FinalCb[T any] func(T, error) (T, error) + +// NewFuture instanciate a new future. +func NewFuture[T any](cb func(resolve Resolver[T], reject Rejection)) *Future[T] { + future := Future[T]{ + mu: sync.RWMutex{}, + next: nil, + cancelCb: func() {}, + } + + go func() { + cb(future.resolve, future.reject) + }() + + return &future +} + +// Future represents a value which may or may not currently be available, but will be +// available at some point, or an exception if that value could not be made available. +type Future[T any] struct { + mu sync.RWMutex + + next func(T, error) + cancelCb func() +} + +func (f *Future[T]) resolve(value T) { + f.mu.RLock() + defer f.mu.RUnlock() + + if f.next != nil { + f.next(value, nil) + } +} + +func (f *Future[T]) reject(err error) { + f.mu.RLock() + defer f.mu.RUnlock() + + if f.next != nil { + f.next(empty[T](), err) + } +} + +// Catch is called when Future is resolved. It returns a new Future. +func (f *Future[T]) Then(cb ThenCb[T]) *Future[T] { + f.mu.Lock() + defer f.mu.Unlock() + + future := &Future[T]{ + mu: sync.RWMutex{}, + next: nil, + cancelCb: func() { + f.Cancel() + }, + } + + f.next = func(value T, err error) { + if err != nil { + future.reject(err) + return + } + + newValue, err := cb(value) + if err != nil { + future.reject(err) + return + } + + future.resolve(newValue) + } + + return future +} + +// Catch is called when Future is rejected. It returns a new Future. +func (f *Future[T]) Catch(cb CatchCb[T]) *Future[T] { + f.mu.Lock() + defer f.mu.Unlock() + + future := &Future[T]{ + mu: sync.RWMutex{}, + next: nil, + cancelCb: func() { + f.Cancel() + }, + } + + f.next = func(value T, err error) { + if err == nil { + future.resolve(value) + return + } + + newValue, err := cb(err) + if err != nil { + future.reject(err) + return + } + + future.resolve(newValue) + } + + return future +} + +// Finally is called when Future is processed either resolved or rejected. It returns a new Future. +func (f *Future[T]) Finally(cb FinalCb[T]) *Future[T] { + f.mu.Lock() + defer f.mu.Unlock() + + future := &Future[T]{ + mu: sync.RWMutex{}, + next: nil, + cancelCb: func() { + f.Cancel() + }, + } + + f.next = func(value T, err error) { + newValue, err := cb(value, err) + if err != nil { + future.reject(err) + return + } + + future.resolve(newValue) + } + + return future +} + +// Cancel cancels the Future chain. +func (f *Future[T]) Cancel() { + f.mu.Lock() + defer f.mu.Unlock() + + f.next = nil + if f.cancelCb != nil { + f.cancelCb() + } +} + +// Collect awaits and return result of the Future. +func (f *Future[T]) Collect() (T, error) { + done := make(chan struct{}) + + var a T + var b error + + f.mu.Lock() + f.next = func(value T, err error) { + a = value + b = err + + done <- struct{}{} + } + f.mu.Unlock() + + <-done + + return a, b +} + +// Result wraps Collect and returns a Result. +func (f *Future[T]) Result() Result[T] { + return TupleToResult(f.Collect()) +} + +// Result wraps Collect and returns a Result. +func (f *Future[T]) Either() Either[error, T] { + v, err := f.Collect() + if err != nil { + return Left[error, T](err) + } + return Right[error, T](v) +} diff --git a/future_example_test.go b/future_example_test.go new file mode 100644 index 0000000..beac32f --- /dev/null +++ b/future_example_test.go @@ -0,0 +1,171 @@ +package mo + +import "fmt" + +func ExampleNewFuture_resolve() { + value, err := NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Collect() + + fmt.Println(value) + fmt.Println(err) + // Output: + // foobar + // +} + +func ExampleNewFuture_reject() { + value, err := NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Collect() + + fmt.Println(value) + fmt.Println(err) + // Output: + // + // failure +} + +func ExampleFuture_Collect_resolve() { + value, err := NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Collect() + + fmt.Println(value) + fmt.Println(err) + // Output: + // foobar + // +} + +func ExampleFuture_Collect_reject() { + value, err := NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Collect() + + fmt.Println(value) + fmt.Println(err) + // Output: + // + // failure +} + +func ExampleFuture_Result_resolve() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // foobar + // +} + +func ExampleFuture_Result_reject() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // + // failure +} + +func ExampleFuture_Then_resolve() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Then(func(s string) (string, error) { + return "baz", nil + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // baz + // +} + +func ExampleFuture_Then_reject() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Then(func(s string) (string, error) { + return "foobar", nil + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // + // failure +} + +func ExampleFuture_Catch_resolve() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Catch(func(err error) (string, error) { + return "baz", nil + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // foobar + // +} + +func ExampleFuture_Catch_reject() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Catch(func(err error) (string, error) { + return "foobar", nil + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // foobar + // +} + +func ExampleFuture_Finally_resolve() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Finally(func(value string, err error) (string, error) { + return "baz", nil + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // baz + // +} + +func ExampleFuture_Finally_reject() { + result := NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Finally(func(value string, err error) (string, error) { + return "foobar", nil + }).Result() + + fmt.Println(result.OrEmpty()) + fmt.Println(result.Error()) + // Output: + // foobar + // +} + +func ExampleFuture_Cancel_resolve() { + NewFuture(func(resolve Resolver[string], reject Rejection) { + resolve("foobar") + }).Cancel() +} + +func ExampleFuture_Cancel_reject() { + NewFuture(func(resolve Resolver[string], reject Rejection) { + reject(fmt.Errorf("failure")) + }).Cancel() +} diff --git a/future_test.go b/future_test.go new file mode 100644 index 0000000..33ec914 --- /dev/null +++ b/future_test.go @@ -0,0 +1,310 @@ +package mo + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func assertAndIncrement(is *assert.Assertions, expected int, i *int32) { + got := atomic.LoadInt32(i) + + is.Equal(int32(expected), got) + + atomic.AddInt32(i, 1) +} + +func TestFuture(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + resolve(42) + }).Then(func(value int) (int, error) { + is.Equal(42, value) + return 21, assert.AnError + }).Catch(func(err error) (int, error) { + is.Equal(assert.AnError, err) + return 0, nil + }).Then(func(value int) (int, error) { + is.Equal(0, value) + return 84, nil + }).Collect() + + is.Equal(84, result) + is.Nil(err) +} + +func TestFutureSimpleResolve(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + resolve(42) + }).Collect() + + is.Equal(42, result) + is.Nil(err) +} + +func TestFutureSimpleReject(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }).Collect() + + is.Equal(0, result) + is.NotNil(err) + is.Equal(assert.AnError, err) +} + +func TestFutureMultipleResolve(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + resolve(42) + }).Then(func(value int) (int, error) { + is.Equal(42, value) + return 84, nil + }).Then(func(value int) (int, error) { + is.Equal(84, value) + return 21, nil + }).Collect() + + is.Equal(21, result) + is.Nil(err) +} + +func TestFutureMultipleReject(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + resolve(42) + }).Catch(func(err error) (int, error) { + is.Fail("should not enter here") + return 84, assert.AnError + }).Then(func(value int) (int, error) { + is.Equal(42, value) + return 21, assert.AnError + }).Catch(func(err error) (int, error) { + is.Equal(assert.AnError, err) + return 1, nil + }).Collect() + + is.Equal(1, result) + is.Nil(err) +} + +func TestFutureSingleReject(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }).Catch(func(err error) (int, error) { + is.Equal(assert.AnError, err) + return 84, nil + }).Collect() + + is.Equal(84, result) + is.Nil(err) +} + +func TestFutureErrorResult(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }).Collect() + + is.Equal(0, result) + is.NotNil(err) + is.Equal(assert.AnError, err) +} + +func TestFutureFinallyResolve(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + resolve(21) + }).Finally(func(value int, err error) (int, error) { + is.Equal(21, value) + is.Nil(err) + + return 42, nil + }).Collect() + + is.Equal(42, result) + is.Nil(err) +} + +func TestFutureFinallyReject(t *testing.T) { + is := assert.New(t) + + result, err := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }).Finally(func(value int, err error) (int, error) { + is.Equal(0, value) + is.NotNil(err) + is.Equal(assert.AnError, err) + + return 42, nil + }).Collect() + + is.Equal(42, result) + is.Nil(err) +} + +func TestFutureOrder(t *testing.T) { + is := assert.New(t) + + var i int32 = 0 + + _ = NewFuture[int](func(resolve Resolver[int], reject Rejection) { + assertAndIncrement(is, 1, &i) + + resolve(42) + }).Then(func(value int) (int, error) { + assertAndIncrement(is, 2, &i) + + return 21, assert.AnError + }).Catch(func(err error) (int, error) { + assertAndIncrement(is, 3, &i) + + return 1, nil + }).Finally(func(value int, err error) (int, error) { + assertAndIncrement(is, 4, &i) + + return 21, nil + }) + + assertAndIncrement(is, 0, &i) +} + +func TestFutureOrderCollect(t *testing.T) { + is := assert.New(t) + + var i int32 = 0 + + _, _ = NewFuture[int](func(resolve Resolver[int], reject Rejection) { + assertAndIncrement(is, 0, &i) + + resolve(42) + }).Then(func(value int) (int, error) { + assertAndIncrement(is, 1, &i) + + return 21, assert.AnError + }).Catch(func(err error) (int, error) { + assertAndIncrement(is, 2, &i) + + return 1, nil + }).Finally(func(value int, err error) (int, error) { + assertAndIncrement(is, 3, &i) + + return 1, nil + }).Collect() + + assertAndIncrement(is, 4, &i) +} + +func TestFutureCancel(t *testing.T) { + is := assert.New(t) + + var i int32 = 0 + + future := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + assertAndIncrement(is, 0, &i) + + time.Sleep(5 * time.Millisecond) + + resolve(42) + }).Then(func(value int) (int, error) { + assertAndIncrement(is, 3, &i) + is.Fail("should not enter here") + + return 21, assert.AnError + }) + + time.Sleep(1 * time.Millisecond) + assertAndIncrement(is, 1, &i) + future.Cancel() + + time.Sleep(10 * time.Millisecond) + assertAndIncrement(is, 2, &i) +} + +func TestFutureCancelDelayed(t *testing.T) { + is := assert.New(t) + + var i int32 = 0 + + future := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + time.Sleep(1 * time.Millisecond) + assertAndIncrement(is, 1, &i) + + resolve(42) + }).Then(func(value int) (int, error) { + assertAndIncrement(is, 2, &i) + + return 21, assert.AnError + }) + + assertAndIncrement(is, 0, &i) + + time.Sleep(10 * time.Millisecond) + + future.Cancel() + + assertAndIncrement(is, 3, &i) +} + +func TestFutureCancelTerminated(t *testing.T) { + is := assert.New(t) + + var i int32 = 0 + + future := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + time.Sleep(1 * time.Millisecond) + assertAndIncrement(is, 1, &i) + + resolve(42) + }).Then(func(value int) (int, error) { + assertAndIncrement(is, 2, &i) + + return 21, assert.AnError + }) + + assertAndIncrement(is, 0, &i) + + _, _ = future.Collect() + + assertAndIncrement(is, 3, &i) + + future.Cancel() + + assertAndIncrement(is, 4, &i) +} + +func TestFutureResultResult(t *testing.T) { + is := assert.New(t) + + result := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }).Result() + + is.Equal(Err[int](assert.AnError), result) + is.NotNil(result.Error()) + is.Equal(assert.AnError, result.Error()) +} + +func TestFutureResultEither(t *testing.T) { + is := assert.New(t) + + either := NewFuture[int](func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }).Either() + + is.Equal(Left[error, int](assert.AnError), either) + is.NotNil(either.Left()) + is.Equal(assert.AnError, either.MustLeft()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d18ece4 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/samber/mo + +go 1.18 + +require github.com/stretchr/testify v1.7.0 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb88a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/io.go b/io.go new file mode 100644 index 0000000..6dae4f3 --- /dev/null +++ b/io.go @@ -0,0 +1,109 @@ +package mo + +// NewIO instanciates a new IO. +func NewIO[R any](f f0[R]) IO[R] { + return IO[R]{ + unsafePerform: f, + } +} + +// IO represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type IO[R any] struct { + unsafePerform f0[R] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IO[R]) Run() R { + return io.unsafePerform() +} + +// NewIO1 instanciates a new IO1. +func NewIO1[R any, A any](f f1[R, A]) IO1[R, A] { + return IO1[R, A]{ + unsafePerform: f, + } +} + +// IO1 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type IO1[R any, A any] struct { + unsafePerform f1[R, A] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IO1[R, A]) Run(a A) R { + return io.unsafePerform(a) +} + +// NewIO2 instanciates a new IO2. +func NewIO2[R any, A any, B any](f f2[R, A, B]) IO2[R, A, B] { + return IO2[R, A, B]{ + unsafePerform: f, + } +} + +// IO2 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type IO2[R any, A any, B any] struct { + unsafePerform f2[R, A, B] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IO2[R, A, B]) Run(a A, b B) R { + return io.unsafePerform(a, b) +} + +// NewIO3 instanciates a new IO3. +func NewIO3[R any, A any, B any, C any](f f3[R, A, B, C]) IO3[R, A, B, C] { + return IO3[R, A, B, C]{ + unsafePerform: f, + } +} + +// IO3 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type IO3[R any, A any, B any, C any] struct { + unsafePerform f3[R, A, B, C] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IO3[R, A, B, C]) Run(a A, b B, c C) R { + return io.unsafePerform(a, b, c) +} + +// NewIO4 instanciates a new IO4. +func NewIO4[R any, A any, B any, C any, D any](f f4[R, A, B, C, D]) IO4[R, A, B, C, D] { + return IO4[R, A, B, C, D]{ + unsafePerform: f, + } +} + +// IO4 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type IO4[R any, A any, B any, C any, D any] struct { + unsafePerform f4[R, A, B, C, D] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IO4[R, A, B, C, D]) Run(a A, b B, c C, d D) R { + return io.unsafePerform(a, b, c, d) +} + +// NewIO5 instanciates a new IO5. +func NewIO5[R any, A any, B any, C any, D any, E any](f f5[R, A, B, C, D, E]) IO5[R, A, B, C, D, E] { + return IO5[R, A, B, C, D, E]{ + unsafePerform: f, + } +} + +// IO5 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type IO5[R any, A any, B any, C any, D any, E any] struct { + unsafePerform f5[R, A, B, C, D, E] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IO5[R, A, B, C, D, E]) Run(a A, b B, c C, d D, e E) R { + return io.unsafePerform(a, b, c, d, e) +} diff --git a/io_either.go b/io_either.go new file mode 100644 index 0000000..079683f --- /dev/null +++ b/io_either.go @@ -0,0 +1,139 @@ +package mo + +// NewIOEither instanciates a new IO. +func NewIOEither[R any](f fe0[R]) IOEither[R] { + return IOEither[R]{ + unsafePerform: f, + } +} + +// IO represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type IOEither[R any] struct { + unsafePerform fe0[R] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IOEither[R]) Run() Either[error, R] { + v, err := io.unsafePerform() + if err != nil { + return Left[error, R](err) + } + + return Right[error, R](v) +} + +// NewIOEither1 instanciates a new IO1. +func NewIOEither1[R any, A any](f fe1[R, A]) IOEither1[R, A] { + return IOEither1[R, A]{ + unsafePerform: f, + } +} + +// IO1 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type IOEither1[R any, A any] struct { + unsafePerform fe1[R, A] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IOEither1[R, A]) Run(a A) Either[error, R] { + v, err := io.unsafePerform(a) + if err != nil { + return Left[error, R](err) + } + + return Right[error, R](v) +} + +// NewIOEither2 instanciates a new IO2. +func NewIOEither2[R any, A any, B any](f fe2[R, A, B]) IOEither2[R, A, B] { + return IOEither2[R, A, B]{ + unsafePerform: f, + } +} + +// IO2 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type IOEither2[R any, A any, B any] struct { + unsafePerform fe2[R, A, B] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IOEither2[R, A, B]) Run(a A, b B) Either[error, R] { + v, err := io.unsafePerform(a, b) + if err != nil { + return Left[error, R](err) + } + + return Right[error, R](v) +} + +// NewIOEither3 instanciates a new IO3. +func NewIOEither3[R any, A any, B any, C any](f fe3[R, A, B, C]) IOEither3[R, A, B, C] { + return IOEither3[R, A, B, C]{ + unsafePerform: f, + } +} + +// IO3 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type IOEither3[R any, A any, B any, C any] struct { + unsafePerform fe3[R, A, B, C] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IOEither3[R, A, B, C]) Run(a A, b B, c C) Either[error, R] { + v, err := io.unsafePerform(a, b, c) + if err != nil { + return Left[error, R](err) + } + + return Right[error, R](v) +} + +// NewIOEither4 instanciates a new IO4. +func NewIOEither4[R any, A any, B any, C any, D any](f fe4[R, A, B, C, D]) IOEither4[R, A, B, C, D] { + return IOEither4[R, A, B, C, D]{ + unsafePerform: f, + } +} + +// IO4 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type IOEither4[R any, A any, B any, C any, D any] struct { + unsafePerform fe4[R, A, B, C, D] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IOEither4[R, A, B, C, D]) Run(a A, b B, c C, d D) Either[error, R] { + v, err := io.unsafePerform(a, b, c, d) + if err != nil { + return Left[error, R](err) + } + + return Right[error, R](v) +} + +// NewIOEither5 instanciates a new IO5. +func NewIOEither5[R any, A any, B any, C any, D any, E any](f fe5[R, A, B, C, D, E]) IOEither5[R, A, B, C, D, E] { + return IOEither5[R, A, B, C, D, E]{ + unsafePerform: f, + } +} + +// IO5 represents a non-deterministic synchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type IOEither5[R any, A any, B any, C any, D any, E any] struct { + unsafePerform fe5[R, A, B, C, D, E] +} + +// Run execute the non-deterministic synchronous computation, with side effect. +func (io IOEither5[R, A, B, C, D, E]) Run(a A, b B, c C, d D, e E) Either[error, R] { + v, err := io.unsafePerform(a, b, c, d, e) + if err != nil { + return Left[error, R](err) + } + + return Right[error, R](v) +} diff --git a/io_either_example_test.go b/io_either_example_test.go new file mode 100644 index 0000000..3e21fca --- /dev/null +++ b/io_either_example_test.go @@ -0,0 +1,34 @@ +package mo + +import ( + "errors" + "fmt" + "os" +) + +func ExampleIOEither1() { + io := NewIOEither1(func(path string) (bool, error) { + _, err := os.Stat(path) + + if errors.Is(err, os.ErrNotExist) { + return false, nil + } else if err != nil { + // other errors + return false, err + } + + return true, nil + }) + + either1 := io.Run("./io_either.go") + either2 := io.Run("./foo_bar.go") + + exist1, _ := either1.Right() + exist2, _ := either2.Right() + + fmt.Println(exist1) + fmt.Println(exist2) + // Output: + // true + // false +} diff --git a/io_either_test.go b/io_either_test.go new file mode 100644 index 0000000..1d18705 --- /dev/null +++ b/io_either_test.go @@ -0,0 +1,106 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIOEither(t *testing.T) { + is := assert.New(t) + + ioEither := NewIOEither(func() (int, error) { + return 42, nil + }) + result := ioEither.Run() + + is.False(result.isLeft) + is.Nil(result.Left()) + is.True(result.isRight) + is.Equal(42, result.MustRight()) +} + +func TestIOEither1(t *testing.T) { + is := assert.New(t) + + ioEither := NewIOEither1(func(a string) (int, error) { + is.Equal("foo", a) + return 42, nil + }) + result := ioEither.Run("foo") + + is.False(result.isLeft) + is.Nil(result.Left()) + is.True(result.isRight) + is.Equal(42, result.MustRight()) +} + +func TestIOEither2(t *testing.T) { + is := assert.New(t) + + ioEither := NewIOEither2(func(a string, b string) (int, error) { + is.Equal("foo", a) + is.Equal("bar", b) + return 42, nil + }) + result := ioEither.Run("foo", "bar") + + is.False(result.isLeft) + is.Nil(result.Left()) + is.True(result.isRight) + is.Equal(42, result.MustRight()) +} + +func TestIOEither3(t *testing.T) { + is := assert.New(t) + + ioEither := NewIOEither3(func(a string, b string, c string) (int, error) { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + return 42, nil + }) + result := ioEither.Run("foo", "bar", "hello") + + is.False(result.isLeft) + is.Nil(result.Left()) + is.True(result.isRight) + is.Equal(42, result.MustRight()) +} + +func TestIOEither4(t *testing.T) { + is := assert.New(t) + + ioEither := NewIOEither4(func(a string, b string, c string, d string) (int, error) { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + is.Equal("world", d) + return 42, nil + }) + result := ioEither.Run("foo", "bar", "hello", "world") + + is.False(result.isLeft) + is.Nil(result.Left()) + is.True(result.isRight) + is.Equal(42, result.MustRight()) +} + +func TestIOEither5(t *testing.T) { + is := assert.New(t) + + ioEither := NewIOEither5(func(a string, b string, c string, d string, e bool) (int, error) { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + is.Equal("world", d) + is.True(e) + return 42, nil + }) + result := ioEither.Run("foo", "bar", "hello", "world", true) + + is.False(result.isLeft) + is.Nil(result.Left()) + is.True(result.isRight) + is.Equal(42, result.MustRight()) +} diff --git a/io_example_test.go b/io_example_test.go new file mode 100644 index 0000000..4d01c10 --- /dev/null +++ b/io_example_test.go @@ -0,0 +1,24 @@ +package mo + +import ( + "fmt" + "time" +) + +func ExampleIO() { + io := NewIO(func() int { + return time.Now().Year() + }) + + result1 := io.Run() + result2 := io.Run() + result3 := io.Run() + + fmt.Println(result1) + fmt.Println(result2) + fmt.Println(result3) + // Output: + // 2022 + // 2022 + // 2022 +} diff --git a/io_test.go b/io_test.go new file mode 100644 index 0000000..61e1499 --- /dev/null +++ b/io_test.go @@ -0,0 +1,88 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIO(t *testing.T) { + is := assert.New(t) + + io := NewIO(func() int { + return 42 + }) + result := io.Run() + + is.Equal(42, result) +} + +func TestIO1(t *testing.T) { + is := assert.New(t) + + io := NewIO1(func(a string) int { + is.Equal("foo", a) + return 42 + }) + result := io.Run("foo") + + is.Equal(42, result) +} + +func TestIO2(t *testing.T) { + is := assert.New(t) + + io := NewIO2(func(a string, b string) int { + is.Equal("foo", a) + is.Equal("bar", b) + return 42 + }) + result := io.Run("foo", "bar") + + is.Equal(42, result) +} + +func TestIO3(t *testing.T) { + is := assert.New(t) + + io := NewIO3(func(a string, b string, c string) int { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + return 42 + }) + result := io.Run("foo", "bar", "hello") + + is.Equal(42, result) +} + +func TestIO4(t *testing.T) { + is := assert.New(t) + + io := NewIO4(func(a string, b string, c string, d string) int { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + is.Equal("world", d) + return 42 + }) + result := io.Run("foo", "bar", "hello", "world") + + is.Equal(42, result) +} + +func TestIO5(t *testing.T) { + is := assert.New(t) + + io := NewIO5(func(a string, b string, c string, d string, e bool) int { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + is.Equal("world", d) + is.True(e) + return 42 + }) + result := io.Run("foo", "bar", "hello", "world", true) + + is.Equal(42, result) +} diff --git a/option.go b/option.go new file mode 100644 index 0000000..b971d33 --- /dev/null +++ b/option.go @@ -0,0 +1,128 @@ +package mo + +import "fmt" + +var optionNoSuchElement = fmt.Errorf("no such element") + +// Some builds an Option when value is present. +func Some[T any](value T) Option[T] { + return Option[T]{ + value: value, + isPresent: true, + } +} + +// None builds an Option when value is absent. +func None[T any]() Option[T] { + return Option[T]{ + isPresent: false, + } +} + +func TupleToOption[T any](v T, ok bool) Option[T] { + if ok { + return Some(v) + } + return None[T]() +} + +// Option is a container for an optional value of type T. If value exists, Option is +// of type Some. If the value is absent, Option is of type None. +type Option[T any] struct { + value T + isPresent bool +} + +// IsPresent returns true when value is absent. +func (o Option[T]) IsPresent() bool { + return o.isPresent +} + +// IsAbsent returns true when value is present. +func (o Option[T]) IsAbsent() bool { + return !o.isPresent +} + +// Size returns 1 when value is present or 0 instead. +func (o Option[T]) Size() int { + if o.isPresent { + return 1 + } + + return 0 +} + +// Get returns value and presence. +func (o Option[T]) Get() (T, bool) { + if !o.isPresent { + return empty[T](), false + } + + return o.value, true +} + +// MustGet returns value if present or panics instead. +func (o Option[T]) MustGet() T { + if !o.isPresent { + panic(optionNoSuchElement) + } + + return o.value +} + +// OrElse returns value if present or default value. +func (o Option[T]) OrElse(fallback T) T { + if !o.isPresent { + return fallback + } + + return o.value +} + +// OrEmpty returns value if present or empty value. +func (o Option[T]) OrEmpty() T { + return o.value +} + +// ForEach executes the given side-effecting function of value is present. +func (o Option[T]) ForEach(f func(T)) { + if o.isPresent { + f(o.value) + } +} + +// Match executes the first function if value is present and second function if absent. +// It returns a new Option. +func (o Option[T]) Match(onValue func(T) (T, bool), onNone func() (T, bool)) Option[T] { + if o.isPresent { + return TupleToOption(onValue(o.value)) + } + return TupleToOption(onNone()) +} + +// Map executes the mapper function if value is present or returns None if absent. +func (o Option[T]) Map(mapper func(T) (T, bool)) Option[T] { + if o.isPresent { + return TupleToOption(mapper(o.value)) + } + + return None[T]() +} + +// MapNone executes the mapper function if value is absent or returns Option. +func (o Option[T]) MapNone(mapper func() (T, bool)) Option[T] { + if o.isPresent { + return Some(o.value) + } + + return TupleToOption(mapper()) +} + +// FlatMap executes the mapper function if value is present or returns None if absent. +func (o Option[T]) FlatMap(mapper func(T) Option[T]) Option[T] { + if o.isPresent { + return mapper(o.value) + } + + return None[T]() +} diff --git a/option_example_test.go b/option_example_test.go new file mode 100644 index 0000000..8ad9432 --- /dev/null +++ b/option_example_test.go @@ -0,0 +1,259 @@ +package mo + +import "fmt" + +func ExampleSome() { + some := Some(42) + result := some.OrElse(1234) + + fmt.Println(result) + // Output: 42 +} + +func ExampleNone() { + none := None[int]() + result := none.OrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleTupleToOption() { + m := map[string]int{ + "foo": 21, + "bar": 42, + "baz": 84, + } + + value, ok := m["hello world"] + + none := TupleToOption(value, ok) + result := none.OrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleOption_some() { + some := Some(42) + result := some.OrElse(1234) + + fmt.Println(result) + // Output: 42 +} + +func ExampleOption_none() { + none := None[int]() + result := none.OrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleOption_IsPresent_some() { + some := Some(42) + result := some.IsPresent() + + fmt.Println(result) + // Output: true +} + +func ExampleOption_IsPresent_none() { + none := None[int]() + result := none.IsPresent() + + fmt.Println(result) + // Output: false +} + +func ExampleOption_IsAbsent_some() { + some := Some(42) + result := some.IsAbsent() + + fmt.Println(result) + // Output: false +} + +func ExampleOption_IsAbsent_none() { + none := None[int]() + result := none.IsAbsent() + + fmt.Println(result) + // Output: true +} + +func ExampleOption_Size_some() { + some := Some(42) + result := some.Size() + + fmt.Println(result) + // Output: 1 +} + +func ExampleOption_Size_none() { + none := None[int]() + result := none.Size() + + fmt.Println(result) + // Output: 0 +} + +func ExampleOption_Get_some() { + some := Some(42) + result, ok := some.Get() + + fmt.Println(result) + fmt.Println(ok) + // Output: + // 42 + // true +} + +func ExampleOption_Get_none() { + none := None[int]() + result, ok := none.Get() + + fmt.Println(result) + fmt.Println(ok) + // Output: + // 0 + // false +} + +func ExampleOption_MustGet_some() { + some := Some(42) + result := some.MustGet() + + fmt.Println(result) + // Output: 42 +} + +// func ExampleOption_MustGet_none() { +// none := None[int]() +// result := none.MustGet() + +// fmt.Println(result) +// // Output: panics +// } + +func ExampleOption_OrElse_some() { + some := Some(42) + result := some.OrElse(1234) + + fmt.Println(result) + // Output: 42 +} + +func ExampleOption_OrElse_none() { + none := None[int]() + result := none.OrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleOption_OrEmpty_some() { + some := Some(42) + result := some.OrEmpty() + + fmt.Println(result) + // Output: 42 +} + +func ExampleOption_OrEmpty_none() { + none := None[int]() + result := none.OrEmpty() + + fmt.Println(result) + // Output: 0 +} + +func ExampleOption_Match_some() { + some := Some(42) + result := some.Match( + func(i int) (int, bool) { + return 0, false + }, + func() (int, bool) { + return 2, true + }, + ) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: false 0 +} + +func ExampleOption_Match_none() { + none := None[int]() + result := none.Match( + func(i int) (int, bool) { + return 0, false + }, + func() (int, bool) { + return 2, true + }, + ) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: true 2 +} + +func ExampleOption_Map_some() { + some := Some(42) + result := some.Map(func(i int) (int, bool) { + return 1234, true + }) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: true 1234 +} + +func ExampleOption_Map_none() { + none := None[int]() + result := none.Map(func(i int) (int, bool) { + return 1234, true + }) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: false 0 +} + +func ExampleOption_MapNone_some() { + some := Some(42) + result := some.MapNone(func() (int, bool) { + return 1234, true + }) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: true 42 +} + +func ExampleOption_MapNone_none() { + none := None[int]() + result := none.MapNone(func() (int, bool) { + return 1234, true + }) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: true 1234 +} + +func ExampleOption_FlatMap_some() { + some := Some(42) + result := some.FlatMap(func(i int) Option[int] { + return Some(21) + }) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: true 21 +} + +func ExampleOption_FlatMap_none() { + none := None[int]() + result := none.FlatMap(func(i int) Option[int] { + return Some(21) + }) + + fmt.Println(result.IsPresent(), result.OrEmpty()) + // Output: false 0 +} diff --git a/option_test.go b/option_test.go new file mode 100644 index 0000000..aecee9d --- /dev/null +++ b/option_test.go @@ -0,0 +1,155 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOptionSome(t *testing.T) { + is := assert.New(t) + + is.Equal(Option[int]{value: 42, isPresent: true}, Some(42)) +} + +func TestOptionNone(t *testing.T) { + is := assert.New(t) + + is.Equal(Option[int]{isPresent: false}, None[int]()) +} + +func TestOptionIsPresent(t *testing.T) { + is := assert.New(t) + + is.True(Some(42).IsPresent()) + is.False(None[int]().IsPresent()) +} + +func TestOptionIsAbsent(t *testing.T) { + is := assert.New(t) + + is.False(Some(42).IsAbsent()) + is.True(None[int]().IsAbsent()) +} + +func TestOptionSize(t *testing.T) { + is := assert.New(t) + + is.Equal(1, Some(42).Size()) + is.Equal(0, None[int]().Size()) +} + +func TestOptionGet(t *testing.T) { + is := assert.New(t) + + v1, ok1 := Some(42).Get() + v2, ok2 := None[int]().Get() + + is.Equal(42, v1) + is.Equal(true, ok1) + is.Equal(0, v2) + is.Equal(false, ok2) +} + +func TestOptionMustGet(t *testing.T) { + is := assert.New(t) + + is.NotPanics(func() { + Some(42).MustGet() + }) + is.Panics(func() { + None[int]().MustGet() + }) + + is.Equal(42, Some(42).MustGet()) +} + +func TestOptionOrElse(t *testing.T) { + is := assert.New(t) + + is.Equal(42, Some(42).OrElse(21)) + is.Equal(21, None[int]().OrElse(21)) +} + +func TestOptionOrEmpty(t *testing.T) { + is := assert.New(t) + + is.Equal(42, Some(42).OrEmpty()) + is.Equal(0, None[int]().OrEmpty()) +} + +func TestOptionForEach(t *testing.T) { + is := assert.New(t) + + tmp := 0 + f := func(x int) { + tmp = x + } + + None[int]().ForEach(f) + is.Equal(0, tmp) + + Some(42).ForEach(f) + is.Equal(42, tmp) +} + +func TestOptionMatch(t *testing.T) { + is := assert.New(t) + + onValue := func(i int) (int, bool) { + return i * 2, true + } + onNone := func() (int, bool) { + return 0, false + } + + opt1 := Some(21).Match(onValue, onNone) + opt2 := None[int]().Match(onValue, onNone) + + is.Equal(Option[int]{value: 42, isPresent: true}, opt1) + is.Equal(Option[int]{value: 0, isPresent: false}, opt2) +} + +func TestOptionMap(t *testing.T) { + is := assert.New(t) + + opt1 := Some(21).Map(func(i int) (int, bool) { + return i * 2, true + }) + opt2 := None[int]().Map(func(i int) (int, bool) { + is.Fail("should not be called") + return 42, true + }) + + is.Equal(Option[int]{value: 42, isPresent: true}, opt1) + is.Equal(Option[int]{value: 0, isPresent: false}, opt2) +} + +func TestOptionMapNone(t *testing.T) { + is := assert.New(t) + + opt1 := Some(21).MapNone(func() (int, bool) { + is.Fail("should not be called") + return 42, true + }) + opt2 := None[int]().MapNone(func() (int, bool) { + return 42, true + }) + + is.Equal(Option[int]{value: 21, isPresent: true}, opt1) + is.Equal(Option[int]{value: 42, isPresent: true}, opt2) +} + +func TestOptionFlatMap(t *testing.T) { + is := assert.New(t) + + opt1 := Some(21).FlatMap(func(i int) Option[int] { + return Some(42) + }) + opt2 := None[int]().FlatMap(func(i int) Option[int] { + return Some(42) + }) + + is.Equal(Option[int]{value: 42, isPresent: true}, opt1) + is.Equal(Option[int]{value: 0, isPresent: false}, opt2) +} diff --git a/result.go b/result.go new file mode 100644 index 0000000..301a4d3 --- /dev/null +++ b/result.go @@ -0,0 +1,133 @@ +package mo + +// Ok builds a Result when value is valid. +func Ok[T any](value T) Result[T] { + return Result[T]{ + value: value, + isErr: false, + } +} + +// Err builds a Result when value is invalid. +func Err[T any](err error) Result[T] { + return Result[T]{ + err: err, + isErr: true, + } +} + +// TupleToResult convert a pair of T and error into a Result. +func TupleToResult[T any](v T, err error) Result[T] { + if err != nil { + return Err[T](err) + } + return Ok(v) +} + +// Result respresent a result of an action having one +// of the following output: success or failure. +// An instance of Result is an instance of either Ok or Err. +type Result[T any] struct { + value T + err error + isErr bool +} + +// IsOk returns true when value is valid. +func (r Result[T]) IsOk() bool { + return !r.isErr +} + +// IsError returns true when value is invalid. +func (r Result[T]) IsError() bool { + return r.isErr +} + +// Error returns error when value is invalid or nil. +func (r Result[T]) Error() error { + return r.err +} + +// MustGet returns value and error. +func (r Result[T]) Get() (T, error) { + if r.isErr { + return empty[T](), r.err + } + + return r.value, nil +} + +// MustGet returns value when Result is valid or panics. +func (r Result[T]) MustGet() T { + if r.isErr { + panic(r.err) + } + + return r.value +} + +// OrElse returns value when Result is valid or default value. +func (r Result[T]) OrElse(fallback T) T { + if r.isErr { + return fallback + } + + return r.value +} + +// OrEmpty returns value when Result is valid or empty value. +func (r Result[T]) OrEmpty() T { + return r.value +} + +// ToEither transforms a Result into an Either type. +func (r Result[T]) ToEither() Either[error, T] { + if r.isErr { + return Left[error, T](r.err) + } + + return Right[error, T](r.value) +} + +// ForEach executes the given side-effecting function if Result is valid. +func (r Result[T]) ForEach(f func(T)) { + if !r.isErr { + f(r.value) + } +} + +// Match executes the first function if Result is valid and second function if invalid. +// It returns a new Result. +func (r Result[T]) Match(onSuccess func(T) (T, error), onError func(error) (T, error)) Result[T] { + if r.isErr { + return TupleToResult(onError(r.err)) + } + return TupleToResult(onSuccess(r.value)) +} + +// Map executes the mapper function if Result is valid. It returns a new Result. +func (r Result[T]) Map(mapper func(T) (T, error)) Result[T] { + if !r.isErr { + return TupleToResult(mapper(r.value)) + } + + return Err[T](r.err) +} + +// MapErr executes the mapper function if Result is invalid. It returns a new Result. +func (r Result[T]) MapErr(mapper func(error) (T, error)) Result[T] { + if r.isErr { + return TupleToResult(mapper(r.err)) + } + + return Ok(r.value) +} + +// FlatMap executes the mapper function if Result is valid. It returns a new Result. +func (r Result[T]) FlatMap(mapper func(T) Result[T]) Result[T] { + if !r.isErr { + return mapper(r.value) + } + + return Err[T](r.err) +} diff --git a/result_example_test.go b/result_example_test.go new file mode 100644 index 0000000..622909d --- /dev/null +++ b/result_example_test.go @@ -0,0 +1,309 @@ +package mo + +import ( + "fmt" +) + +var err = fmt.Errorf("error") + +func ExampleOk() { + ok := Ok(42) + result := ok.OrElse(1234) + _err := ok.Error() + + fmt.Println(result, _err) + // Output: 42 +} + +func ExampleErr() { + ko := Err[int](err) + result := ko.OrElse(1234) + _err := ko.Error() + + fmt.Println(result, _err) + // Output: 1234 error +} + +func ExampleTupleToResult() { + randomFunc := func() (int, error) { + return 42, err + } + + value, _err := randomFunc() + + none := TupleToResult(value, _err) + result := none.OrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleResult_ok() { + ok := Ok(42) + result := ok.OrElse(1234) + _err := ok.Error() + + fmt.Println(result, _err) + // Output: 42 +} + +func ExampleResult_err() { + ko := Err[int](err) + result := ko.OrElse(1234) + _err := ko.Error() + + fmt.Println(result, _err) + // Output: 1234 error +} + +func ExampleResult_IsOk_ok() { + ok := Ok(42) + result := ok.IsOk() + + fmt.Println(result) + // Output: true +} + +func ExampleResult_IsOk_err() { + ko := Err[int](err) + result := ko.IsOk() + + fmt.Println(result) + // Output: false +} + +func ExampleResult_IsError_ok() { + ok := Ok(42) + result := ok.IsError() + + fmt.Println(result) + // Output: false +} + +func ExampleResult_IsError_err() { + ko := Err[int](err) + result := ko.IsError() + + fmt.Println(result) + // Output: true +} + +func ExampleResult_Error_ok() { + ok := Ok(42) + result := ok.Error() + + fmt.Println(result) + // Output: +} + +func ExampleResult_Error_err() { + ko := Err[int](err) + result := ko.Error() + + fmt.Println(result) + // Output: error +} + +func ExampleResult_Get_ok() { + ok := Ok(42) + result, err := ok.Get() + + fmt.Println(result) + fmt.Println(err) + // Output: + // 42 + // +} + +func ExampleResult_Get_err() { + ko := Err[int](err) + result, err := ko.Get() + + fmt.Println(result) + fmt.Println(err) + // Output: + // 0 + // error +} + +func ExampleResult_MustGet_ok() { + ok := Ok(42) + result := ok.MustGet() + + fmt.Println(result) + // Output: 42 +} + +// func ExampleResult_MustGet_err() { +// ko := Err[int](err) +// result := ko.MustGet() + +// fmt.Println(result) +// // Output: panics +// } + +func ExampleResult_OrElse_ok() { + ok := Ok(42) + result := ok.OrElse(1234) + + fmt.Println(result) + // Output: 42 +} + +func ExampleResult_OrElse_err() { + ko := Err[int](err) + result := ko.OrElse(1234) + + fmt.Println(result) + // Output: 1234 +} + +func ExampleResult_OrEmpty_ok() { + ok := Ok(42) + result := ok.OrEmpty() + + fmt.Println(result) + // Output: 42 +} + +func ExampleResult_OrEmpty_err() { + ko := Err[int](err) + result := ko.OrEmpty() + + fmt.Println(result) + // Output: 0 +} + +func ExampleResult_ToEither_ok() { + ok := Ok(42) + either := ok.ToEither() + + err, isLeft := either.Left() + value, isRight := either.Right() + + fmt.Println(isLeft, isRight) + fmt.Println(err) + fmt.Println(value) + // Output: + // false true + // + // 42 +} + +func ExampleResult_ToEither_err() { + ko := Err[int](err) + either := ko.ToEither() + + err, isLeft := either.Left() + value, isRight := either.Right() + + fmt.Println(isLeft, isRight) + fmt.Println(err) + fmt.Println(value) + // Output: + // true false + // error + // 0 +} + +func ExampleResult_Match_ok() { + ok := Ok(42) + result := ok.Match( + func(i int) (int, error) { + return i * 2, nil + }, + func(err error) (int, error) { + return 21, nil + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: false 84 +} + +func ExampleResult_Match_err() { + ko := Err[int](err) + result := ko.Match( + func(i int) (int, error) { + return i * 2, nil + }, + func(err error) (int, error) { + return 21, nil + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: false 21 +} + +func ExampleResult_Map_ok() { + ok := Ok(42) + result := ok.Map( + func(i int) (int, error) { + return i * 2, nil + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: false 84 +} + +func ExampleResult_Map_err() { + ko := Err[int](err) + result := ko.Map( + func(i int) (int, error) { + return i * 2, nil + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: true 0 error +} + +func ExampleResult_MapErr_ok() { + ok := Ok(42) + result := ok.MapErr( + func(_err error) (int, error) { + return 1234, nil + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: false 42 +} + +func ExampleResult_MapErr_err() { + ko := Err[int](err) + result := ko.MapErr( + func(_err error) (int, error) { + return 1234, nil + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: false 1234 +} + +func ExampleResult_FlatMap_ok() { + ok := Ok(42) + result := ok.FlatMap( + func(i int) Result[int] { + return Ok(1234) + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: false 1234 +} + +func ExampleResult_FlatMap_err() { + ko := Err[int](err) + result := ko.FlatMap( + func(i int) Result[int] { + return Ok(1234) + }, + ) + + fmt.Println(result.IsError(), result.OrEmpty(), result.Error()) + // Output: true 0 error +} diff --git a/result_test.go b/result_test.go new file mode 100644 index 0000000..c8537d2 --- /dev/null +++ b/result_test.go @@ -0,0 +1,180 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResultOk(t *testing.T) { + is := assert.New(t) + + is.Equal(Result[int]{value: 42, isErr: false, err: nil}, Ok(42)) +} + +func TestResultErr(t *testing.T) { + is := assert.New(t) + + is.Equal(Result[int]{value: 0, isErr: true, err: assert.AnError}, Err[int](assert.AnError)) +} + +func TestResultIsOk(t *testing.T) { + is := assert.New(t) + + is.True(Ok(42).IsOk()) + is.False(Err[int](assert.AnError).IsOk()) +} + +func TestResultIsError(t *testing.T) { + is := assert.New(t) + + is.False(Ok(42).IsError()) + is.True(Err[int](assert.AnError).IsError()) +} + +func TestResultError(t *testing.T) { + is := assert.New(t) + + is.Nil(Ok(42).Error()) + is.NotNil(Err[int](assert.AnError).Error()) + is.Equal(assert.AnError, Err[int](assert.AnError).Error()) +} + +func TestResultGet(t *testing.T) { + is := assert.New(t) + + v1, err1 := Ok(42).Get() + v2, err2 := Err[int](assert.AnError).Get() + + is.Equal(42, v1) + is.Nil(err1) + is.Error(assert.AnError, err1) + + is.Equal(0, v2) + is.NotNil(err2) + is.Error(assert.AnError, err2) +} + +func TestResultMustGet(t *testing.T) { + is := assert.New(t) + + is.NotPanics(func() { + Ok(42).MustGet() + }) + is.Panics(func() { + Err[int](assert.AnError).MustGet() + }) + + is.Equal(42, Ok(42).MustGet()) +} + +func TestResultOrElse(t *testing.T) { + is := assert.New(t) + + is.Equal(42, Ok(42).OrElse(21)) + is.Equal(21, Err[int](assert.AnError).OrElse(21)) +} + +func TestResultOrEmpty(t *testing.T) { + is := assert.New(t) + + is.Equal(42, Ok(42).OrEmpty()) + is.Equal(0, Err[int](assert.AnError).OrEmpty()) +} + +func TestResultToEither(t *testing.T) { + is := assert.New(t) + + right, ok1 := Ok(42).ToEither().Right() + left, ok2 := Err[int](assert.AnError).ToEither().Left() + + is.Equal(42, right) + is.True(ok1) + is.Equal(assert.AnError, left) + is.True(ok2) +} + +func TestResultForEach(t *testing.T) { + is := assert.New(t) + + Err[int](assert.AnError).ForEach(func(i int) { + is.Fail("should not enter here") + }) + + Ok(42).ForEach(func(i int) { + is.Equal(42, i) + }) +} + +func TestResultMatch(t *testing.T) { + is := assert.New(t) + + opt1 := Ok(21).Match( + func(i int) (int, error) { + is.Equal(21, i) + return i * 2, nil + }, + func(err error) (int, error) { + is.Fail("should not enter here") + return 0, err + }, + ) + opt2 := Err[int](assert.AnError).Match( + func(i int) (int, error) { + is.Fail("should not enter here") + return i * 2, nil + }, + func(err error) (int, error) { + is.Equal(assert.AnError, err) + return 0, err + }, + ) + + is.Equal(Result[int]{value: 42, isErr: false, err: nil}, opt1) + is.Equal(Result[int]{value: 0, isErr: true, err: assert.AnError}, opt2) +} + +func TestResultMap(t *testing.T) { + is := assert.New(t) + + opt1 := Ok(21).Map(func(i int) (int, error) { + return i * 2, nil + }) + opt2 := Err[int](assert.AnError).Map(func(i int) (int, error) { + is.Fail("should not be called") + return 42, nil + }) + + is.Equal(Result[int]{value: 42, isErr: false, err: nil}, opt1) + is.Equal(Result[int]{value: 0, isErr: true, err: assert.AnError}, opt2) +} + +func TestResultMapErr(t *testing.T) { + is := assert.New(t) + + opt1 := Ok(21).MapErr(func(err error) (int, error) { + is.Fail("should not be called") + return 42, nil + }) + opt2 := Err[int](assert.AnError).MapErr(func(err error) (int, error) { + return 42, nil + }) + + is.Equal(Result[int]{value: 21, isErr: false, err: nil}, opt1) + is.Equal(Result[int]{value: 42, isErr: false, err: nil}, opt2) +} + +func TestResultFlatMap(t *testing.T) { + is := assert.New(t) + + opt1 := Ok(21).FlatMap(func(i int) Result[int] { + return Ok(42) + }) + opt2 := Err[int](assert.AnError).FlatMap(func(i int) Result[int] { + is.Fail("should not be called") + return Ok(42) + }) + + is.Equal(Result[int]{value: 42, isErr: false, err: nil}, opt1) + is.Equal(Result[int]{value: 0, isErr: true, err: assert.AnError}, opt2) +} diff --git a/state.go b/state.go new file mode 100644 index 0000000..a1bb0a6 --- /dev/null +++ b/state.go @@ -0,0 +1,52 @@ +package mo + +func NewState[S any, A any](f func(state S) (A, S)) State[S, A] { + return State[S, A]{ + run: f, + } +} + +func ReturnState[S any, A any](x A) State[S, A] { + return State[S, A]{ + run: func(state S) (A, S) { + return x, state + }, + } +} + +// State represents a function `(S) -> (A, S)`, where `S` is state, `A` is result. +type State[S any, A any] struct { + run func(state S) (A, S) +} + +// Run executes a computation in the State monad. +func (s State[S, A]) Run(state S) (A, S) { + return s.run(state) +} + +// Get returns the current state. +func (s State[S, A]) Get() State[S, S] { + return State[S, S]{ + run: func(state S) (S, S) { + return state, state + }, + } +} + +// Modify the state by applying a function to the current state. +func (s State[S, A]) Modify(f func(state S) S) State[S, A] { + return State[S, A]{ + run: func(state S) (A, S) { + return empty[A](), f(state) + }, + } +} + +// Put set the state. +func (s State[S, A]) Put(state S) State[S, A] { + return State[S, A]{ + run: func(state S) (A, S) { + return empty[A](), state + }, + } +} diff --git a/task.go b/task.go new file mode 100644 index 0000000..3bcdd73 --- /dev/null +++ b/task.go @@ -0,0 +1,175 @@ +package mo + +// NewTask instanciates a new Task. +func NewTask[R any](f ff0[R]) Task[R] { + return Task[R]{ + unsafePerform: f, + } +} + +// NewTaskFromIO instanciates a new Task from an existing IO. +func NewTaskFromIO[R any](io IO[R]) Task[R] { + return Task[R]{ + unsafePerform: func() *Future[R] { + return NewFuture[R](func(resolve Resolver[R], reject Rejection) { + resolve(io.unsafePerform()) + }) + }, + } +} + +// Task represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type Task[R any] struct { + unsafePerform ff0[R] +} + +// Run execute the non-deterministic asynchronous computation, with side effect. +func (t Task[R]) Run() *Future[R] { + return t.unsafePerform() +} + +// NewTask1 instanciates a new Task1. +func NewTask1[R any, A any](f ff1[R, A]) Task1[R, A] { + return Task1[R, A]{ + unsafePerform: f, + } +} + +// NewTaskFromIO1 instanciates a new Task1 from an existing IO1. +func NewTaskFromIO1[R any, A any](io IO1[R, A]) Task1[R, A] { + return Task1[R, A]{ + unsafePerform: func(a A) *Future[R] { + return NewFuture[R](func(resolve Resolver[R], reject Rejection) { + resolve(io.unsafePerform(a)) + }) + }, + } +} + +// Task1 represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type Task1[R any, A any] struct { + unsafePerform ff1[R, A] +} + +// Run execute the non-deterministic asynchronous computation, with side effect. +func (t Task1[R, A]) Run(a A) *Future[R] { + return t.unsafePerform(a) +} + +// NewTask2 instanciates a new Task2. +func NewTask2[R any, A any, B any](f ff2[R, A, B]) Task2[R, A, B] { + return Task2[R, A, B]{ + unsafePerform: f, + } +} + +// NewTaskFromIO2 instanciates a new Task2 from an existing IO2. +func NewTaskFromIO2[R any, A any, B any](io IO2[R, A, B]) Task2[R, A, B] { + return Task2[R, A, B]{ + unsafePerform: func(a A, b B) *Future[R] { + return NewFuture[R](func(resolve Resolver[R], reject Rejection) { + resolve(io.unsafePerform(a, b)) + }) + }, + } +} + +// Task2 represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type Task2[R any, A any, B any] struct { + unsafePerform ff2[R, A, B] +} + +// Run execute the non-deterministic asynchronous computation, with side effect. +func (t Task2[R, A, B]) Run(a A, b B) *Future[R] { + return t.unsafePerform(a, b) +} + +// NewTask3 instanciates a new Task3. +func NewTask3[R any, A any, B any, C any](f ff3[R, A, B, C]) Task3[R, A, B, C] { + return Task3[R, A, B, C]{ + unsafePerform: f, + } +} + +// NewTaskFromIO3 instanciates a new Task3 from an existing IO3. +func NewTaskFromIO3[R any, A any, B any, C any](io IO3[R, A, B, C]) Task3[R, A, B, C] { + return Task3[R, A, B, C]{ + unsafePerform: func(a A, b B, c C) *Future[R] { + return NewFuture[R](func(resolve Resolver[R], reject Rejection) { + resolve(io.unsafePerform(a, b, c)) + }) + }, + } +} + +// Task3 represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type Task3[R any, A any, B any, C any] struct { + unsafePerform ff3[R, A, B, C] +} + +// Run execute the non-deterministic asynchronous computation, with side effect. +func (t Task3[R, A, B, C]) Run(a A, b B, c C) *Future[R] { + return t.unsafePerform(a, b, c) +} + +// NewTask4 instanciates a new Task4. +func NewTask4[R any, A any, B any, C any, D any](f ff4[R, A, B, C, D]) Task4[R, A, B, C, D] { + return Task4[R, A, B, C, D]{ + unsafePerform: f, + } +} + +// NewTaskFromIO4 instanciates a new Task4 from an existing IO4. +func NewTaskFromIO4[R any, A any, B any, C any, D any](io IO4[R, A, B, C, D]) Task4[R, A, B, C, D] { + return Task4[R, A, B, C, D]{ + unsafePerform: func(a A, b B, c C, d D) *Future[R] { + return NewFuture[R](func(resolve Resolver[R], reject Rejection) { + resolve(io.unsafePerform(a, b, c, d)) + }) + }, + } +} + +// Task4 represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type Task4[R any, A any, B any, C any, D any] struct { + unsafePerform ff4[R, A, B, C, D] +} + +// Run execute the non-deterministic asynchronous computation, with side effect. +func (t Task4[R, A, B, C, D]) Run(a A, b B, c C, d D) *Future[R] { + return t.unsafePerform(a, b, c, d) +} + +// NewTask5 instanciates a new Task5. +func NewTask5[R any, A any, B any, C any, D any, E any](f ff5[R, A, B, C, D, E]) Task5[R, A, B, C, D, E] { + return Task5[R, A, B, C, D, E]{ + unsafePerform: f, + } +} + +// NewTaskFromIO5 instanciates a new Task5 from an existing IO5. +func NewTaskFromIO5[R any, A any, B any, C any, D any, E any](io IO5[R, A, B, C, D, E]) Task5[R, A, B, C, D, E] { + return Task5[R, A, B, C, D, E]{ + unsafePerform: func(a A, b B, c C, d D, e E) *Future[R] { + return NewFuture[R](func(resolve Resolver[R], reject Rejection) { + resolve(io.unsafePerform(a, b, c, d, e)) + }) + }, + } +} + +// Task5 represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and never fails. +type Task5[R any, A any, B any, C any, D any, E any] struct { + unsafePerform ff5[R, A, B, C, D, E] +} + +// Run execute the non-deterministic asynchronous computation, with side effect. +func (t Task5[R, A, B, C, D, E]) Run(a A, b B, c C, d D, e E) *Future[R] { + return t.unsafePerform(a, b, c, d, e) +} diff --git a/task_either.go b/task_either.go new file mode 100644 index 0000000..e899f7a --- /dev/null +++ b/task_either.go @@ -0,0 +1,56 @@ +package mo + +// NewTaskEither instanciates a new TaskEither. +func NewTaskEither[R any](f ff0[R]) TaskEither[R] { + return TaskEither[R]{NewTask[R](f)} +} + +// NewTaskEitherFromIO instanciates a new TaskEither from an existing IO. +func NewTaskEitherFromIO[R any](io IO[R]) TaskEither[R] { + return TaskEither[R]{NewTaskFromIO[R](io)} +} + +// TaskEither represents a non-deterministic asynchronous computation that +// can cause side effects, yields a value of type `R` and can fail. +type TaskEither[R any] struct { + Task[R] +} + +// OrElse returns value if task succeded or default value. +func (t TaskEither[R]) OrElse(fallback R) R { + either := t.Run().Either() + + right, isRight := either.Right() + if !isRight { + return fallback + } + + return right +} + +// Match executes the first function if task succeded and second function if task failed. +// It returns a new Option. +func (t TaskEither[R]) Match(onLeft func(error) Either[error, R], onRight func(R) Either[error, R]) Either[error, R] { + either := t.Run().Either() + return either.Match(onLeft, onRight) +} + +// TryCatch is an alias to Match +func (t TaskEither[R]) TryCatch(onLeft func(error) Either[error, R], onRight func(R) Either[error, R]) Either[error, R] { + return t.Match(onLeft, onRight) +} + +// ToTask converts TaskEither to Task +func (t TaskEither[R]) ToTask(fallback R) Task[R] { + return NewTask(func() *Future[R] { + return t.Run(). + Catch(func(err error) (R, error) { + return fallback, nil + }) + }) +} + +// ToTask converts TaskEither to Task +func (t TaskEither[R]) ToEither() Either[error, R] { + return t.Run().Either() +} diff --git a/task_either_test.go b/task_either_test.go new file mode 100644 index 0000000..a940342 --- /dev/null +++ b/task_either_test.go @@ -0,0 +1,123 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTaskEither(t *testing.T) { + is := assert.New(t) + + taskEither := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + resolve(42) + }) + }) + + result := taskEither.Run().Result().MustGet() + + is.Equal(42, result) +} + +func TestTaskEitherOrElse(t *testing.T) { + is := assert.New(t) + + taskEither1 := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + resolve(42) + }) + }) + taskEither2 := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }) + }) + + result1 := taskEither1.OrElse(1234) + result2 := taskEither2.OrElse(1234) + + is.Equal(42, result1) + is.Equal(1234, result2) +} + +func TestTaskEitherMatch(t *testing.T) { + is := assert.New(t) + + taskEither := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + resolve(42) + }) + }) + + mapped := taskEither.Match( + func(err error) Either[error, int] { + return Right[error, int](1234) + }, + func(i int) Either[error, int] { + return Right[error, int](i) + }, + ) + + v, ok := mapped.Right() + + is.Equal(42, v) + is.True(ok) +} + +func TestTaskEitherTryCatch(t *testing.T) { + is := assert.New(t) + + taskEither := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + resolve(42) + }) + }) + + mapped := taskEither.TryCatch( + func(err error) Either[error, int] { + return Right[error, int](1234) + }, + func(i int) Either[error, int] { + return Right[error, int](i) + }, + ) + + v, ok := mapped.Right() + + is.Equal(42, v) + is.True(ok) +} + +func TestTaskEitherToTask(t *testing.T) { + is := assert.New(t) + + taskEither := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }) + }) + + task := taskEither.ToTask(1234) + + result := task.Run().Result().MustGet() + + is.Equal(1234, result) +} + +func TestTaskEitherToEither(t *testing.T) { + is := assert.New(t) + + taskEither := NewTaskEither(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + reject(assert.AnError) + }) + }) + + either := taskEither.ToEither() + err, isError := either.Left() + + is.True(isError) + is.NotNil(err) + is.Equal(assert.AnError, err) +} diff --git a/task_example_test.go b/task_example_test.go new file mode 100644 index 0000000..8c4276a --- /dev/null +++ b/task_example_test.go @@ -0,0 +1,24 @@ +package mo + +import ( + "fmt" + "time" +) + +func ExampleTask() { + task := NewTask(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + resolve(time.Now().Year()) + }) + }) + + // returns a future + future := task.Run() + + // a Task never fail + result, _ := future.Collect() + + fmt.Println(result) + // Output: + // 2022 +} diff --git a/task_test.go b/task_test.go new file mode 100644 index 0000000..3b9e582 --- /dev/null +++ b/task_test.go @@ -0,0 +1,100 @@ +package mo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTask(t *testing.T) { + is := assert.New(t) + + task := NewTask(func() *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + resolve(42) + }) + }) + result := task.Run().Result().MustGet() + + is.Equal(42, result) +} + +func TestTask1(t *testing.T) { + is := assert.New(t) + + task := NewTask1(func(a string) *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + is.Equal("foo", a) + resolve(42) + }) + }) + result := task.Run("foo").Result().MustGet() + + is.Equal(42, result) +} + +func TestTask2(t *testing.T) { + is := assert.New(t) + + task := NewTask2(func(a string, b string) *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + is.Equal("foo", a) + is.Equal("bar", b) + resolve(42) + }) + }) + result := task.Run("foo", "bar").Result().MustGet() + + is.Equal(42, result) +} + +func TestTask3(t *testing.T) { + is := assert.New(t) + + task := NewTask3(func(a string, b string, c string) *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + resolve(42) + }) + }) + result := task.Run("foo", "bar", "hello").Result().MustGet() + + is.Equal(42, result) +} + +func TestTask4(t *testing.T) { + is := assert.New(t) + + task := NewTask4(func(a string, b string, c string, d string) *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + is.Equal("world", d) + resolve(42) + }) + }) + result := task.Run("foo", "bar", "hello", "world").Result().MustGet() + + is.Equal(42, result) +} + +func TestTask5(t *testing.T) { + is := assert.New(t) + + task := NewTask5(func(a string, b string, c string, d string, e bool) *Future[int] { + return NewFuture(func(resolve Resolver[int], reject Rejection) { + is.Equal("foo", a) + is.Equal("bar", b) + is.Equal("hello", c) + is.Equal("world", d) + is.True(e) + resolve(42) + }) + }) + result := task.Run("foo", "bar", "hello", "world", true).Result().MustGet() + + is.Equal(42, result) +} diff --git a/typeclass/filterable.go b/typeclass/filterable.go new file mode 100644 index 0000000..9f51eaf --- /dev/null +++ b/typeclass/filterable.go @@ -0,0 +1,5 @@ +package typeclass + +type Filterable[T any] interface { + Filter(func(T) bool) T +} diff --git a/typeclass/foldable.go b/typeclass/foldable.go new file mode 100644 index 0000000..ceb6927 --- /dev/null +++ b/typeclass/foldable.go @@ -0,0 +1,7 @@ +package typeclass + +type Foldable[T any, R any] interface { + Map(func(T) R) R + FoldLeft(func(T) R) R + FoldRight(func(T) R) R +} diff --git a/typeclass/monadic.go b/typeclass/monadic.go new file mode 100644 index 0000000..98bfef9 --- /dev/null +++ b/typeclass/monadic.go @@ -0,0 +1,7 @@ +package typeclass + +type Monadic[T any] interface { + ForEach(func(T)) + Map(func(T) T) T + FlatMap(func(T) Monadic[T]) T +} diff --git a/typeclass/monoid.go b/typeclass/monoid.go new file mode 100644 index 0000000..5b0fda4 --- /dev/null +++ b/typeclass/monoid.go @@ -0,0 +1,4 @@ +package typeclass + +type Monoid[T any] interface { +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..c85d403 --- /dev/null +++ b/types.go @@ -0,0 +1,22 @@ +package mo + +type f0[R any] func() R +type f1[R any, A any] func(A) R +type f2[R any, A any, B any] func(A, B) R +type f3[R any, A any, B any, C any] func(A, B, C) R +type f4[R any, A any, B any, C any, D any] func(A, B, C, D) R +type f5[R any, A any, B any, C any, D any, E any] func(A, B, C, D, E) R + +type ff0[R any] func() *Future[R] +type ff1[R any, A any] func(A) *Future[R] +type ff2[R any, A any, B any] func(A, B) *Future[R] +type ff3[R any, A any, B any, C any] func(A, B, C) *Future[R] +type ff4[R any, A any, B any, C any, D any] func(A, B, C, D) *Future[R] +type ff5[R any, A any, B any, C any, D any, E any] func(A, B, C, D, E) *Future[R] + +type fe0[R any] func() (R, error) +type fe1[R any, A any] func(A) (R, error) +type fe2[R any, A any, B any] func(A, B) (R, error) +type fe3[R any, A any, B any, C any] func(A, B, C) (R, error) +type fe4[R any, A any, B any, C any, D any] func(A, B, C, D) (R, error) +type fe5[R any, A any, B any, C any, D any, E any] func(A, B, C, D, E) (R, error) diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..5233f67 --- /dev/null +++ b/utils.go @@ -0,0 +1,5 @@ +package mo + +func empty[T any]() (t T) { + return +}