From 1f48f701e5707cabd218f60e8e4d4cdc6fa69342 Mon Sep 17 00:00:00 2001 From: Ben Sarah Golightly Date: Wed, 17 May 2023 02:07:18 +0100 Subject: [PATCH] fun/* additions, particularly either, promise and future --- LICENSE.txt | 4 +- README.md | 44 +++++--- SECURITY.md | 49 ++++---- TRADEMARKS.md | 2 +- fun/either/either.go | 71 ++++++++++++ fun/future/async.go | 85 ++++++++++++++ fun/future/future.go | 87 +++++++++++++++ fun/future/sync.go | 53 +++++++++ fun/maybe/maybe.go | 16 +++ fun/partial/example_test.go | 45 +++++++- fun/partial/partial.go | 61 ++++++++-- fun/promise/promise.go | 217 ++++++++++++++++++++++++++++++++++++ fun/result/result.go | 18 +-- fun/slices/slices.go | 12 ++ 14 files changed, 694 insertions(+), 70 deletions(-) create mode 100644 fun/either/either.go create mode 100644 fun/future/async.go create mode 100644 fun/future/future.go create mode 100644 fun/future/sync.go create mode 100644 fun/promise/promise.go diff --git a/LICENSE.txt b/LICENSE.txt index 09ce338..c5f8145 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ github.com/tawesoft/golib -Copyright © 2019 - 2022 Ben Golightly -Copyright © 2019 - 2022 Tawesoft Ltd +Copyright © 2019 - 2023 Ben Golightly +Copyright © 2019 - 2023 Tawesoft Ltd Copyright © Contributors (api.github.com/repos/tawesoft/golib/contributors) Copyright © Unicode, W3C & others (some portions; see LICENSE-PARTS.txt) diff --git a/README.md b/README.md index fd28570..9af51e0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Coverage Status](https://coveralls.io/repos/github/tawesoft/golib/badge.svg?branch=v2)](https://coveralls.io/github/tawesoft/golib?branch=v2) A monorepo for small Go (v1.20+) modules maintained by -[Tawesoft®](https://www.tawesoft.co.uk). +[Tawesoft®](https://www.tawesoft.co.uk), with few dependencies. ```go import "github.com/tawesoft/golib/v2/..." @@ -27,17 +27,20 @@ support, are additionally covered by compatible [MIT-like licences](/LICENSE-PAR | dialog | [v2][d01] | - | cross-platform message boxes & file pickers | | digraph | - | [v2][d02] | *(unstable)* directed graphs (including DAGs) | | drop | - | - | *(TODO)* drop process privileges and inherit handles | -| fun/maybe | [v2][f01] | - | "Maybe" sum type | -| fun/partial | [v2][f02] | - | partial function application | -| fun/result | [v2][f03] | - | "Result" sum type | -| fun/slices | [v2][f04] | - | higher-order functions for slices | +| fun/either | [v2][f01] | - | "Either" sum type | +| fun/future | [v2][f02] | - | synchronous and asynchronous future values | +| fun/maybe | [v2][f03] | - | "Maybe" sum type | +| fun/partial | [v2][f04] | - | partial function application | +| fun/promise | [v2][f05] | - | store computations to be performed later | +| fun/result | [v2][f06] | - | "Result" sum type | +| fun/slices | [v2][f07] | - | higher-order functions for slices | | grace | - | - | *(TODO)* start and gracefully shutdown processes | | humanize | - | - | *(TODO)* locale-aware numbers & quantities | | iter | [v2][i01] | - | composable lazy iteration | | ks | - | [v2][k01] | *(unstable)* "kitchen sink" of extras | | loader | - | - | *(TODO)* concurrent dependency graph solver | -| html/meta/opengraph | [v2][m01] | - | HTML meta tags for Facebook's Open Graph protocol | -| html/meta/twittercard | [v2][m02] | - | HTML meta tags for Twitter Cards | +| html/meta/opengraph | [v2][h01] | - | HTML meta tags for Facebook's Open Graph protocol | +| html/meta/twittercard | [v2][h02] | - | HTML meta tags for Twitter Cards | | must | [v2][m03] | - | assertions | | operator | [v2][o01] | - | operators as functions | | tuple | [v2][p01] | - | convert to/from tuples | @@ -61,9 +64,10 @@ expected for a Go package of v2 or higher. "Latest" packages, or | text/fold | - | [v2][t04] | Unicode text folding | | text/np | - | [v2][t05] | Unicode numeric properties | | text/number/algorithmic | [v2][t07] | - | CLDR algorithmic (non-decimal) numbering systems | -| text/number/rbnf | - | [v2][t08] | CLDR Rule-Based Number Formats | -| text/number/symbols | - | [v2][t09] | CLDR locale-appropriate Number Symbols | -| text/runeio | - | [v2][t06] | *(unstable)* Unicode streams with lookahead & rewind | +| text/number/plurals | [v2][t08] | - | CLDR plural rules with a simple interface | +| text/number/rbnf | - | [v2][t09] | CLDR Rule-Based Number Formats | +| text/number/symbols | - | [v2][t10] | CLDR locale-appropriate Number Symbols | + **Note:** "Stable" packages have the [normal stability guarantees](https://go.dev/doc/modules/version-numbers) @@ -74,14 +78,17 @@ expected for a Go package of v2 or higher. "Latest" packages, or [c01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/css/tokenizer [d01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/dialog [d02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/digraph -[f01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/maybe -[f02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/partial -[f03]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/result -[f04]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/slices +[f01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/either +[f02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/future +[f03]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/maybe +[f04]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/partial +[f05]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/promise +[f06]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/result +[f07]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/slices [i01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/iter [k01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/ks -[m01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/meta/opengraph -[m02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/meta/twittercard +[h01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/meta/opengraph +[h02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/meta/twittercard [m03]: https://pkg.go.dev/github.com/tawesoft/golib/v2/must [o01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/operator [p01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/tuple @@ -92,8 +99,9 @@ expected for a Go package of v2 or higher. "Latest" packages, or [t05]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/np [t06]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/runeio [t07]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/number/algorithmic -[t08]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/number/rbnf -[t09]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/number/symbols +[t08]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/number/plurals +[t09]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/number/rbnf +[t10]: https://pkg.go.dev/github.com/tawesoft/golib/v2/text/number/symbols [v01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/view diff --git a/SECURITY.md b/SECURITY.md index c6145a9..cf08334 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,23 +1,27 @@ # Security Policy -Contact: [security@tawesoft.co.uk](mailto:security@tawesoft.co.uk) +[![Report A Vulnerability]](https://github.com/tawesoft/golib/security/advisories/new) +[![View Security Advisories]](https://github.com/tawesoft/golib/security/advisories) +Contact: [security@tawesoft.co.uk](mailto:security@tawesoft.co.uk) ## Announcements It is our policy to publicly announce security issues and fixes through the GitHub "Security Advisories" feature for this repository. -You can subscribe to security announcements for this repository by -[configuring your "watching" settings](https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#configuring-your-watch-settings-for-an-individual-repository) -and subscribing to security alerts. +To subscribe, [configure your watch settings] and tick either "All Activity" +or select "custom" and tick "security alerts". + +[configure your watch settings]: (https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/configuring-notifications#configuring-your-watch-settings-for-an-individual-repository) + +## Pre-announcements On a case-by-case basis, we are prepared to pre-announce security issues and fixes to any downstream consumer of this repository who can provide evidence that any security issues would have a particularly high impact on their -services, such as operators of a service that processes personal data or is -used by a large volume of users. Email us with the subject -"join security-preannounce list" for details. +services. Email [security@tawesoft.co.uk](mailto:security@tawesoft.co.uk) +to discuss this. ## Backporting fixes @@ -33,24 +37,23 @@ if necessary. Please disclose responsibly so that we can notify the users of our software with a fix and/or instructions, including a pre-announcement where appropriate. -Do not report security issues through the public issue tracker in the first -instance, unless the vulnerability is being actively exploited in the wild or -is already public knowledge. -Instead, please email information about vulnerabilities to -[security@tawesoft.co.uk](mailto:security@tawesoft.co.uk). +**Please do not report security issues through the public issue tracker** in the +first instance, unless the vulnerability is being actively exploited in the +wild or is already public knowledge. Please use the option to securely +report a vulnerability through GitHub at the top of this page. Alternatively, +please email us at [security@tawesoft.co.uk](mailto:security@tawesoft.co.uk) +and if necessary we can make other arrangements with you for secure disclosure. -To help prioritise your report, format the subject line as follows: - -`vulnerability: repository-url - description` - -For example: +If you don't receive an acknowledgement of your report within 48 hours, +and you believe that urgent action is required, then please contact us through +any contact method listed on the +[Tawesoft website](https://www.tawesoft.co.uk/). -`vulnerability: github.com/example/repo - denial of service in foo/bar` +If we have not fixed or disclosed a vulnerability within 90 days of being +notified, then we respect your right to disclose it publicly. -If you don't receive an acknowledgement within 48 hours, please contact us -through any contact method listed on the -[Tawesoft website](https://www.tawesoft.co.uk/). -If, after being notified, we have not fixed or disclosed a vulnerability after -90 days, then you may exercise your right to disclose it publicly. + +[Report A Vulnerability]: https://img.shields.io/badge/Report_A_Vulnerability-1f883d?style=for-the-badge&logo=github +[View Security Advisories]: https://img.shields.io/badge/View_advisories-d86900?style=for-the-badge&logo=github diff --git a/TRADEMARKS.md b/TRADEMARKS.md index 9ec9e05..752e68f 100644 --- a/TRADEMARKS.md +++ b/TRADEMARKS.md @@ -32,7 +32,7 @@ Windows is a trade mark of [Microsoft Corporation](https://www.microsoft.com/en-gb). -## Disclaimed +## Not trade marks The name of this "golib" software is used as a generic descriptive term for a [software library](https://en.wikipedia.org/wiki/Library_(computing)) diff --git a/fun/either/either.go b/fun/either/either.go new file mode 100644 index 0000000..6a3d933 --- /dev/null +++ b/fun/either/either.go @@ -0,0 +1,71 @@ +// Package either implements a simple generic "Either" type that can represent +// exactly one value out of two options. +package either + +import ( + "fmt" +) + +// E represents a type that can hold either a value "a" of type A or a +// value "b" of type B. +type E[A any, B any] struct { + a A + b B + index byte // 'a' or 'b' +} + +// Pack returns an E that contains eotjer a value "a" of type A (if index == +// 'a'), a value "b" of type B (if index == 'b'), or panics if index is not 'a' +// or 'b'. +func Pack[A any, B any](a A, b B, index byte) E[A, B] { + if index == 'a' { + return E[A, B]{a: a, index: index} + } else if index == 'b' { + return E[A, B]{b: b, index: index} + } else { + panic(fmt.Errorf("either: Pack[%T, %T](): invalid index value", a, b)) + } +} + +// Unpack returns the components of an E. The last return value is a +// discriminator with the value 'a' or 'b' representing the existence of the +// value "a" of type A or the value "b" of type B. +func (e E[A, B]) Unpack() (A, B, byte) { + return e.a, e.b, e.index +} + +// A returns a new E that holds a value "a" of Type A. +func A[A any, B any](a A) E[A, B] { + return E[A, B]{ + a: a, + index: 'a', + } +} + +// B returns a new E that holds a value "b" of type B. +func B[A any, B any](b B) E[A, B] { + return E[A, B]{ + b: b, + index: 'b', + } +} + +// A returns the value "a" of type A and true if the E contains that value, +// or the zero value and false otherwise. +func (e E[A, B]) A() (result A, ok bool) { + if e.index == 'a' { + result = e.a + ok = true + } + return +} + +// B returns the value "b" of type B and true if the E contains that value, +// or the zero value and false otherwise. +func (e E[A, B]) B() (result B, ok bool) { + if e.index == 'b' { + result = e.b + ok = true + } + return +} diff --git a/fun/future/async.go b/fun/future/async.go new file mode 100644 index 0000000..739bdb4 --- /dev/null +++ b/fun/future/async.go @@ -0,0 +1,85 @@ +package future + +import ( + "context" + + "github.com/tawesoft/golib/v2/fun/partial" + "github.com/tawesoft/golib/v2/fun/promise" + "github.com/tawesoft/golib/v2/fun/result" + "github.com/tawesoft/golib/v2/fun/slices" +) + +type async[T any] struct { + context context.Context + cancel context.CancelFunc + channel chan result.R[T] +} + +func start[T any](ctx context.Context, promise promise.P[T], channel chan result.R[T]) { + value := result.New(promise.ComputeCtx(ctx)) + for { + select { + case <- ctx.Done(): return + default: channel <- value + } + } + close(channel) +} + +// NewAsync creates a new future from a promise, and begins computing that +// promise asynchronously in a new goroutine. +func NewAsync[T any](ctx context.Context, promise promise.P[T]) F[T] { + ctxWithCancel, cancel := context.WithCancel(ctx) + f := async[T]{ + context: ctxWithCancel, + cancel: cancel, + channel: make(chan result.R[T]), + } + go start(ctxWithCancel, promise, f.channel) + return f +} + +// NewAsyncs is like [NewAsync], but accepts a slice of promises and returns a +// slice of futures. +func NewAsyncs[T any](ctx context.Context, xs []promise.P[T]) []F[T] { + return slices.Map( + partial.Left2(NewAsync[T])(ctx), + xs, + ) +} + +func (f async[T]) Collect() (result T, err error) { + return f.CollectCtx(context.TODO()) +} + +func (f async[T]) CollectCtx(ctx context.Context) (result T, err error) { + select { + case <- ctx.Done(): + err = ctx.Err() + return + case <- f.context.Done(): + err = f.context.Err() + return + case r := <- f.channel: + result, err = r.Unpack() + return + } +} + +func (f async[T]) Stop() { + f.cancel() +} + +func (f async[T]) Peek() (result T, err error) { + select { + case <- f.context.Done(): + err = f.context.Err() + return + case r := <- f.channel: + result, err = r.Unpack() + return + default: + err = NotReady + return + } +} diff --git a/fun/future/future.go b/fun/future/future.go new file mode 100644 index 0000000..1ccf7b9 --- /dev/null +++ b/fun/future/future.go @@ -0,0 +1,87 @@ +// Package future implements "Futures", which represent a placeholder handle +// for a value that may not yet be ready, but is (eventually) computed by a +// promise. +package future + +import ( + "context" + "errors" + + "github.com/tawesoft/golib/v2/fun/promise" +) + +// F represents a "future", a placeholder handle backed by a promise to +// compute a value at some later point. +// +// It is up to the implementation of the interface if the promise will run +// synchronously or asynchronously, or if it is safe to collect the future +// using concurrent code. See [NewSync] and [NewAsync] for synchronous and +// concurrent implementations, respectively. +// +// Collect and CollectCtx return the value or error returned by the computed +// promise, blocking if necessary. A promise is computed exactly once. In +// synchronous code, this happens at the time of the first call to Collect or +// CollectCtx. The cached value or error is returned to each subsequent call to +// Collect or CollectCtx. +// +// Peek is like Collect, but if the value is not yet available, returns +// immediately with the error [NotReady]. In the case of [Sync], Peek always +// returns with [NotReady] if the value has yet to be computed (i.e. by a call +// to Collect or CollectCtx). +// +// The error return value of Collect, Peek, and CollectCtx may include context +// errors such as [context.Cancelled]. +// +// The "Ctx" method variants support cancellation only so far as the underlying +// promise supports being cancelled. +// +// Stop indicates that the future is no longer needed and its resources can be +// released. In the case of asynchronous code, this includes terminating the +// backing goroutine so that future calls to Collect, CollectCtx, and Peek +// return [context.Cancelled]. Futures created with [NewSync] do not need to be +// closed, but it is not an error to do so. An asynchronous future may not +// actually stop until the underlying promise has finished computing or +// accepted a cancellation signal. It is not an error to stop a future multiple +// times. +type F[T any] interface { + Collect() (T, error) + CollectCtx(ctx context.Context) (T, error) + Peek() (T, error) + Stop() +} + +// NotReady is the error returned by the Peek methods on a future when the +// computed value or error is not yet available. +var NotReady = errors.New("future.NotReady") + +// ForEach applies function f(x) to every future that is both computed +// and whose promise did not return an error i.e. for every successful Peek. +func ForEach[T any](f func(x T), xs []F[T]) { + for _, x := range xs { + value, err := x.Peek() + if err != nil { continue} + f(value) + } +} + +// CollectAll returns a promise to compute the slice of the values of the input +// futures, stopping at the first error. +func CollectAll[T any](xs []F[T]) promise.P[[]T] { + return CollectAllCtx(context.TODO(), xs) +} + +// CollectAllCtx is like [CollectAll] but uses the given context while +// computing the promises. +func CollectAllCtx[T any](ctx context.Context, xs []F[T]) promise.P[[]T] { + return promise.FromResultFunc(func () ([]T, error) { + values := make([]T, 0, len(xs)) + for _, x := range xs { + value, err := x.CollectCtx(ctx) + if err != nil { + return nil, err + } + values = append(values, value) + } + return values, nil + }) +} diff --git a/fun/future/sync.go b/fun/future/sync.go new file mode 100644 index 0000000..1f7d6d4 --- /dev/null +++ b/fun/future/sync.go @@ -0,0 +1,53 @@ +package future + +import ( + "context" + + "github.com/tawesoft/golib/v2/fun/maybe" + "github.com/tawesoft/golib/v2/fun/promise" + "github.com/tawesoft/golib/v2/fun/result" + "github.com/tawesoft/golib/v2/fun/slices" + "github.com/tawesoft/golib/v2/operator" +) + +type sync[T any] struct { + promise promise.P[T] + result maybe.M[result.R[T]] +} + +// NewSync creates a new future to be run synchronously based on a given +// promise to compute a value. +// +// It's methods are not safe for concurrent access. +func NewSync[T any](promise promise.P[T]) F[T] { + return &sync[T]{promise: promise,} +} + +// NewSyncs is like [NewSync], but accepts a slice of promises and returns a +// slice of futures. +func NewSyncs[T any](xs []promise.P[T]) []F[T] { + return slices.Map(NewSync[T], xs) +} + +func (f *sync[T]) Collect() (T, error) { + return f.CollectCtx(context.TODO()) +} + +func (f *sync[T]) CollectCtx(ctx context.Context) (T, error) { + if !f.result.Ok { + r, err := f.promise.ComputeCtx(ctx) + f.result = maybe.Some(result.New(r, err)) + } + return f.result.Value.Unpack() +} + +func (f *sync[T]) Peek() (T, error) { + if !f.result.Ok { + return operator.Zero[T](), NotReady + } + return f.result.Value.Unpack() +} + +func (f *sync[T]) Stop() { + f.result = maybe.Some(result.Error[T](context.Canceled)) +} diff --git a/fun/maybe/maybe.go b/fun/maybe/maybe.go index 8610678..d77baab 100644 --- a/fun/maybe/maybe.go +++ b/fun/maybe/maybe.go @@ -83,6 +83,22 @@ func FlatMap[X any, Y any]( } } +// Collect takes a slice of Maybe[X] and returns a slice of []X and true iff +// every element contains a value. +func Collect[X any]( + xs []M[X], +) ([]X, bool) { + if len(xs) == 0 { return nil, true } + for _, x := range xs { + if !x.Ok { return nil, false } + } + result := make([]X, 0, len(xs)) + for _, x := range xs { + result = append(result, x.Value) + } + return result, true +} + // Applicator turns function "M[f: X => Y]" into "f: X => M[Y]". func Applicator[X any, Y any]( f M[func(x X) Y], diff --git a/fun/partial/example_test.go b/fun/partial/example_test.go index 4835261..5fc49cd 100644 --- a/fun/partial/example_test.go +++ b/fun/partial/example_test.go @@ -2,12 +2,13 @@ package partial_test import ( "fmt" + "math" "github.com/tawesoft/golib/v2/fun/maybe" "github.com/tawesoft/golib/v2/fun/partial" ) -func Example() { +func Example_Line() { // The formula for a line can be given by "y = mx + c", where m is the // gradient, and c is the offset where the line crosses the x-axis. line := func(x int, m int, c int) int { // solves for y @@ -39,7 +40,7 @@ func Example() { // 11 } -func ExampleMaybe() { +func Example_Maybe() { // divides two numbers, while checking for divide by zero. divide := func(x int, y int) (value int, ok bool) { if y == 0 { return 0, false } @@ -50,10 +51,14 @@ func ExampleMaybe() { // returns a single [maybe.M]. maybeDivide := maybe.WrapFunc2(divide) - // bind y to the divide function, and also convert it back from a function - // that returns (value int, ok bool) instead of a [maybe.M]. - divideByTwo := maybe.UnwrapFunc(partial.Right2(maybeDivide)(2)) - divideByZero := maybe.UnwrapFunc(partial.Right2(maybeDivide)(0)) + // we create a function, divider, with one argument, that can be used to + // construct new functions that divide by a constant factor. + divider := partial.Right2(maybeDivide) + + // bind a constant factor to the divide function, and also convert it back + // to a function that returns (value int, ok bool) instead of a [maybe.M]. + divideByTwo := maybe.UnwrapFunc(divider(2)) + divideByZero := maybe.UnwrapFunc(divider(0)) { result, ok := divideByTwo(10) @@ -68,3 +73,31 @@ func ExampleMaybe() { // divideByTwo(10) = 5, true // divideByZero(10) = 0, false } + +func Example_All() { + // Pythagoras theorem for calculating the hypotenuse of a triangle: + // a squared + b squared = c squared. + hyp := func(a float64, b float64) float64 { + return math.Sqrt((a*a) + (b*b)) + } + + // Suppose we want a function with no arguments that returns the answer + // for a specific triangle. Imagine it's an expensive computation, we've + // got lots of computations like this, and they're ones we might want to + // run asynchronously across multiple workers. This is a simple "promise" + // construct. + // + // Normally we could define it like this: + hyp_2_3_verbose := func() float64 { return hyp(2, 3) } + + // But we can use "partial.All*" functions to do this for us (here, 2, + // for the two arguments). + hyp_2_3_terse := partial.All2(hyp)(2, 3) + + fmt.Printf("hyp_2_3_verbose = %.3f\n", hyp_2_3_verbose()) + fmt.Printf("hyp_2_3_terse = %.3f\n", hyp_2_3_terse()) + + // Output: + // hyp_2_3_verbose = 3.606 + // hyp_2_3_terse = 3.606 +} diff --git a/fun/partial/partial.go b/fun/partial/partial.go index 97a18b0..ef7ffd4 100644 --- a/fun/partial/partial.go +++ b/fun/partial/partial.go @@ -1,22 +1,31 @@ // Package partial provides helpers for partial function application. // -// Each function, f, in this package has a suffix for arity that indicates the -// number of arguments to the provided input function, i. Each function f -// returns a function g with a single argument that can be used to "partially -// apply" function i with one argument already bound. +// Each function in this package has a suffix for arity that indicates the +// number of arguments of the provided input function f. +// +// Each Left/Right function returns a function with a single argument that can +// be used to "partially apply" input function f with one argument already +// bound. // // Functions have the prefix "Left" if they bind the left-most argument first, // or "Right" if they bind the right-most argument first. // -// Each input function must return exactly one value. The [fun/result] and -// [fun/maybe] packages provide useful functions that can wrap functions that -// return (value, ok) or (value, error). +// Each input function must return exactly one value. The "fun/result" and +// "fun/maybe" packages provide useful functions that can wrap functions that +// return (value, ok) or (value, error) result types into single-value return +// variants. // // For example, // -// f(a, b, c) becomes f(b, c), with a bound, by calling Left3(f)(a). +// f(a, b, c) => x becomes g(b, c) => x, with a bound, by calling Left3(f)(a). +// +// f(a, b, c) => x becomes g(a, b) => x, with c bound, by calling Right3(f)(c). +// +// f(a, b, c) => x becomes g() => x, with a, b, and c bound, by calling +// All3(f)(a, b, c). +// +// f(a) => x becomes g() => x, with a bound, by calling Single(f)(a). // -// f(a, b, c) becomes f(a, b), with c bound, by calling Right3(f)(c). package partial // Single takes a function with a single argument and return value and @@ -26,8 +35,8 @@ package partial // For example, // // opener := partial.Single(result.WrapFunc(os.Open)) -// openFoo := opener("foo.txt") -// f, err := openFoo().Unpack() +// fooOpener := opener("foo.txt") +// f, err := fooOpener().Unpack() func Single[T any, Return any]( f func(t T) Return, ) func (t T) func () Return { @@ -97,3 +106,33 @@ func Right4[A any, B any, C any, D any, Return any]( } } } + +func All2[A any, B any, Return any]( + f func(A, B) Return, +) func(A, B) func() Return { + return func (a A, b B) func () Return { + return func() Return { + return f(a, b) + } + } +} + +func All3[A any, B any, C any, Return any]( + f func(A, B, C) Return, +) func(A, B, C) func() Return { + return func (a A, b B, c C) func () Return { + return func() Return { + return f(a, b, c) + } + } +} + +func All4[A any, B any, C any, D any, Return any]( + f func(A, B, C, D) Return, +) func(A, B, C, D) func() Return { + return func (a A, b B, c C, d D) func () Return { + return func() Return { + return f(a, b, c, d) + } + } +} diff --git a/fun/promise/promise.go b/fun/promise/promise.go new file mode 100644 index 0000000..6a18673 --- /dev/null +++ b/fun/promise/promise.go @@ -0,0 +1,217 @@ +// Package promise implements a simple Promise type that can be used to +// represent a computation to be performed at a later stage. +// +// This composes nicely with the idea of futures (see fun/future). +package promise + +import ( + "context" + "errors" +) + +var NotOk = errors.New("promised value is not ok") + +// P represents a promise to calculate and return some value when Compute or +// ComputeCtx is called. +// +// Compute, or ComputeCtx, should only be called once, unless an implementation +// otherwise indicates that it is safe to do so. A promise is not safe for +// concurrent use, unless an implementation indicates otherwise. +// +// A promise may ignore the provided context if it cannot be cancelled. The +// plain Compute method computes the promise with a context that is never +// cancelled. +// +// The error return value of Compute and ComputeCtx may be an error returned by +// the computation, or, in the case of ComputeCtx, a context error such as +// [context.Cancelled]. +type P[X any] interface { + Compute() (X, error) + ComputeCtx(ctx context.Context) (X, error) +} + +// Func is the type of a function with no arguments that satisfies the promise +// interface by calling the function, ignoring any context. +type Func[X any] func() (X, error) +func (f Func[X]) Compute() (X, error) { + return f() +} +func (f Func[X]) ComputeCtx(ctx context.Context) (X, error) { + return f() +} + +// FromFunc creates a promise to call function f, where f returns any single +// value and has no facility to indicate an error. +func FromFunc[T any](f func() T) P[T] { + return FromResultFunc(func() (T, error) { + return f(), nil + }) +} + +// FromResultFunc creates a promise to call function f, where f returns a +// (result, error) tuple. +func FromResultFunc[X any](f func() (X, error)) P[X] { + return Func[X](f) +} + +// WrapResultFunc wraps an existing function "f() => (X, error)" so that it +// becomes "f() => P[X]", a function that returns a promise. +func WrapResultFunc[X any](f func() (X, error)) func() P[X] { + return func() P[X] { + return FromValueErr(f()) + } +} + +// WrapResultFunc1 wraps an existing function "f(A) => (X, error)" so that it +// becomes "f(A) => P[X]", a function that returns a promise. +func WrapResultFunc1[A, X any](f func(A) (X, error)) func(A) P[X] { + return func(a A) P[X] { + return FromValueErr(f(a)) + } +} + +// WrapResultFunc2 wraps an existing function "f(A, B) => (X, error)" so that it +// becomes "f(A, B) => P[X]", a function that returns a promise. +func WrapResultFunc2[A, B, X any](f func(A, B) (X, error)) func(A, B) P[X] { + return func(a A, b B) P[X] { + return FromValueErr(f(a, b)) + } +} + +// WrapResultFunc3 wraps an existing function "f(A, B, C) => (X, error)" so +// that it becomes "f(A, B, C) => P[X]", a function that returns a promise. +func WrapResultFunc3[A, B, C, X any](f func(A, B, C) (X, error)) func(A, B, C) P[X] { + return func(a A, b B, c C) P[X] { + return FromValueErr(f(a, b, c)) + } +} + +// WrapResultFunc4 wraps an existing function "f(A, B, C, D) => (X, error)" so +// that it becomes "f(A, B, C, D) => P[X]", a function that returns a promise. +func WrapResultFunc4[A, B, C, D, X any](f func(A, B, C, D) (X, error)) func(A, B, C, D) P[X] { + return func(a A, b B, c C, d D) P[X] { + return FromValueErr(f(a, b, c, d)) + } +} + +// FromOkFunc creates a promise to call function f, where f returns a +// (value, ok) tuple. If the returned ok is false, the promise computes the +// error [NotOk]. +func FromOkFunc[X any](f func() (X, bool)) P[X] { + return Func[X](func() (result X, err error) { + v, ok := f() + if !ok { err = NotOk; return } + return v, nil + }) +} + +// FuncCtx is the type of a function with a context argument that satisfies the +// promise interface by calling the function with a context. +type FuncCtx[X any] func(ctx context.Context) (X, error) +func (f FuncCtx[X]) Compute() (X, error) { + return f(context.TODO()) +} +func (f FuncCtx[X]) ComputeCtx(ctx context.Context) (X, error) { + return f(ctx) +} + +// FromFuncCtx creates a promise to call function f, where f accepts a context +// and returns any single value and has no facility to indicate an error, +// other than a context error. +func FromFuncCtx[T any](f func(context.Context) T) P[T] { + return FromResultFuncCtx(func(ctx context.Context) (T, error) { + return f(ctx), nil + }) +} + +// FromResultFuncCtx creates a promise to call function f, where f accepts a +// context and returns a (result, error) tuple. +func FromResultFuncCtx[X any](f func(context.Context) (X, error)) P[X] { + return FuncCtx[X](f) +} + +// FromOkFuncCtx creates a promise to call function f, where f accepts a +// context and returns a (value, ok) tuple. If the returned ok is false, the +// promise computes the error [NotOk]. +func FromOkFuncCtx[X any](f func(ctx context.Context) (X, bool)) P[X] { + return FuncCtx[X](func(ctx context.Context) (result X, err error) { + v, ok := f(ctx) + if !ok { err = NotOk; return } + return v, nil + }) +} + +type value[X any] struct {x X} +func (v value[X]) Compute() (X, error) { + return v.x, nil +} +func (v value[X]) ComputeCtx(ctx context.Context) (X, error) { + return v.x, nil +} + +// FromValue creates a promise that simply returns the provided argument and +// a nil error when computed. +func FromValue[X any](x X) P[X] { + return value[X]{x: x} +} + +// FromValueErr creates a promise that simply returns the provided argument or, +// if the provided error was non-nil, the provided error when computed. +func FromValueErr[X any](value X, err error) P[X] { + if err != nil { + return FromError[X](err) + } else { + return FromValue(value) + } +} + +type perror[X any] struct {err error} +func (e perror[X]) Compute() (X, error) { + var zero X + return zero, e.err +} +func (e perror[X]) ComputeCtx(ctx context.Context) (X, error) { + var zero X + return zero, e.err +} + +// FromError creates a promise that simply returns the provided error when +// computed. +func FromError[X any](err error) P[X] { + return perror[X]{err: err} +} + +// Chain returns a new promise to compute function f on the result of promise +// p. +func Chain[X any, Y any](p P[X], f func(X) (Y, error)) P[Y] { + return FromResultFuncCtx[Y](func(ctx context.Context) (Y, error) { + v, err := p.ComputeCtx(ctx) + if err != nil { + var zero Y + return zero, err + } + return f(v) + }) +} + +/* +Example: + + a := promise.FromFunc(func() (int, error) { + return 0, nil + }) + half := promise.Chain(a, func(x int) (float64, error) { + return float64(x) * 0.5, nil + }) + double := promise.Chain(half, func(x float64) (float64, error) { + return float64(x) * 2.0, nil + }) + inverse := promise.Chain(double, func(x float64) (float64, error) { + if x == 0.0 { + return 0.0, fmt.Errorf("divide by zero error") + } + return 1.0 / x, nil + }) + + fmt.Printf("got %f\n", must.Result(inverse.Compute())) + */ diff --git a/fun/result/result.go b/fun/result/result.go index c3024bb..cea9524 100644 --- a/fun/result/result.go +++ b/fun/result/result.go @@ -111,8 +111,8 @@ func Applicator[X any, Y any]( return func(x X) R[Y] { return Some(f.Value(x)) } } -// WrapFunc converts a function of the form "f: X => (Y, error)" to the form -// "f: X => R[X]. +// WrapFunc converts a function of the form "f(a) => (a, error)" to the form +// "f(a) => R[Y]". func WrapFunc[X any, Y any]( f func(x X) (Y, error), ) func(x X) R[Y] { @@ -121,13 +121,13 @@ func WrapFunc[X any, Y any]( } } -// UnwrapFunc converts a function of the form "f: X => R[Y]" to the -// form "f: X => (Y, error)". -func UnwrapFunc[X any, Y any]( - f func(x X) R[Y], -) func(x X) (Y, error) { - return func(x X) (Y, error) { - return f(x).Unpack() +// UnwrapFunc converts a function of the form "f(a) => R[T]" to the +// form "f(a) => (T, error)". +func UnwrapFunc[A any, T any]( + f func(A) R[T], +) func(A) (T, error) { + return func(a A) (T, error) { + return f(a).Unpack() } } diff --git a/fun/slices/slices.go b/fun/slices/slices.go index aae5648..1f2c0a9 100644 --- a/fun/slices/slices.go +++ b/fun/slices/slices.go @@ -1,6 +1,18 @@ // Package slices provides generic higher-order functions over slices of values. package slices +// FromArgs returns the slice of the variadic arguments list. +func FromArgs[X any](xs ... X) []X { + return xs +} + +// ForEach applies the void function "f(x)" to each value X of the input slice. +func ForEach[X any](f func(X), xs []X) { + for _, x := range xs { + f(x) + } +} + // Filter applies function "f : X => bool" to each value X of the input slice, // and returns a new slice containing only each X for which f(X) is true. //