diff --git a/README.md b/README.md index a90727f..83340ad 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This library aims to provide a set of data types and functions that make it easy #### 🧘🏽 Each package fulfils a single purpose -✔️ Each of the top level packages (e.g. Option, Either, Task, ...) fulfils the purpose of defining the respective data type and implementing the set of common operations for this data type. +✔️ Each of the top level packages (e.g. Option, Either, ReaderIOEither, ...) fulfils the purpose of defining the respective data type and implementing the set of common operations for this data type. #### 🧘🏽 Handle errors explicitly @@ -41,7 +41,7 @@ This library aims to provide a set of data types and functions that make it easy #### 🧘🏽 Leave concurrency to the caller -✔️ All operations are synchronous by default, including `Task`. Concurrency must be coded by the consumer of these functions explicitly, but the implementation is ready to deal with concurrent usage. +✔️ All pure are synchronous by default. The I/O operations are asynchronous per default. #### 🧘🏽 Before you launch a goroutine, know when it will stop @@ -65,7 +65,7 @@ This library aims to provide a set of data types and functions that make it easy #### 🧘🏽 Moderation is a virtue -✔️ The library does not implement its own goroutines and also does not require any expensive synchronization primitives. Coordination of Tasks is implemented via atomic counters without additional primitives. Channels are only used in the `Wait` function of a Task that should be invoked at most once in a complete application. +✔️ The library does not implement its own goroutines and also does not require any expensive synchronization primitives. Coordination of IO operations is implemented via atomic counters without additional primitives. #### 🧘🏽 Maintainability counts @@ -81,33 +81,57 @@ All monadic operations are implemented via generics, i.e. they offer a type safe Downside is that this will result in different versions of each operation per type, these versions are generated by the golang compiler at build time (unlike type erasure in languages such as Java of TypeScript). This might lead to large binaries for codebases with many different types. If this is a concern, you can always implement type erasure on top, i.e. use the monadic operations with the `any` type as if generics were not supported. You loose type safety, but this might result in smaller binaries. +### Ordering of Generic Type Parameters + +In go we need to specify all type parameters of a function on the global function definition, even if the function returns a higher order function and some of the type parameters are only applicable to the higher order function. So the following is not possible: + +```go +func Map[A, B any](f func(A) B) [R, E any]func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B] +``` + +Note that the parameters `R` and `E` are not needed by the first level of `Map` but only by the resulting higher order function. Instead we need to specify the following: + +```go +func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B] +``` + +which overspecifies `Map` on the global scope. As a result the go compiler will not be able to auto-detect these parameters, it can only auto detect `A` and `B` since they appear in the argument of `Map`. We need to explicitly pass values for these type parameters when `Map` is being used. + +Because of this limitation the order of parameters on a function matters. We want to make sure that we define those parameters that cannot be auto-detected, first, and the parameters that can be auto-detected, last. This can lead to inconsistencies in parameter ordering, but we believe that the gain in convenience is worth it. The parameter order of `Ap` is e.g. different from that of `Map`: + +```go +func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(fab ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B] +``` + +because `R`, `E` and `A` can be determined from the argument to `Ap` but `B` cannot. + ### Use of the [~ Operator](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) The FP library attempts to be easy to consume and one aspect of this is the definition of higher level type definitions instead of having to use their low level equivalent. It is e.g. more convenient and readable to use ```go -TaskEither[E, A] +ReaderIOEither[R, E, A] ``` than ```go -func(func(Either.Either[E, A])) +func(R) func() Either.Either[E, A] ``` although both are logically equivalent. At the time of this writing the go type system does not support generic type aliases, only generic type definition, i.e. it is not possible to write: ```go -type TaskEither[E, A any] = T.Task[ET.Either[E, A]] +type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]] ``` only ```go -type TaskEither[E, A any] T.Task[ET.Either[E, A]] +type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]] ``` -This makes a big difference, because in the second case the type `TaskEither[E, A any]` is considered a completely new type, not compatible to its right hand side, so it's not just a shortcut but a fully new type. +This makes a big difference, because in the second case the type `ReaderIOEither[R, E, A any]` is considered a completely new type, not compatible to its right hand side, so it's not just a shortcut but a fully new type. From the implementation perspective however there is no reason to restrict the implementation to the new type, it can be generic for all compatible types. The way to express this in go is the [~](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) operator. This comes with some quite complicated type declarations in some cases, which undermines the goal of the library to be easy to use. @@ -117,16 +141,16 @@ For that reason there exist sub-packages called `Generic` for all higher level t Go does not support higher kinded types (HKT). Such types occur if a generic type itself is parametrized by another generic type. Example: -The `Map` operation for `Task` is defined as: +The `Map` operation for `ReaderIOEither` is defined as: ```go -func Map[A, B any](f func(A) B) func(Task[A]) Task[B] +func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B] ``` -and in fact the equivalent operations for all other mondas follow the same pattern, we could try to introduce a new type for `Task` (without a parameter) as a HKT, e.g. like so (made-up syntax, does not work in go): +and in fact the equivalent operations for all other mondas follow the same pattern, we could try to introduce a new type for `ReaderIOEither` (without a parameter) as a HKT, e.g. like so (made-up syntax, does not work in go): ```go -func Map[HKT, A, B any](f func(A) B) func(HKT[A]) HKT[B] +func Map[HKT, R, E, A, B any](f func(A) B) func(HKT[R, E, A]) HKT[R, E, B] ``` this would be the completely generic method signature for all possible monads. In particular in many cases it is possible to compose functions independent of the concrete knowledge of the actual `HKT`. From the perspective of a library this is the ideal situation because then a particular algorithm only has to be implemented and tested once. diff --git a/context/readerioeither/gen.go b/context/readerioeither/gen.go index 1fe27c2..c00ca3c 100644 --- a/context/readerioeither/gen.go +++ b/context/readerioeither/gen.go @@ -1,8 +1,8 @@ -// Code generated by go generate; DO NOT EDIT. -// This file was generated by robots at -// 2023-07-18 15:21:14.8906482 +0200 CEST m=+0.127356001 package readerioeither +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots at +// 2023-07-19 16:18:34.1521763 +0200 CEST m=+0.011558001 import ( "context" diff --git a/context/readerioeither/generic/eq.go b/context/readerioeither/generic/eq.go index 83cdb95..977bb8c 100644 --- a/context/readerioeither/generic/eq.go +++ b/context/readerioeither/generic/eq.go @@ -4,12 +4,11 @@ import ( "context" E "github.com/IBM/fp-go/either" - ET "github.com/IBM/fp-go/either" EQ "github.com/IBM/fp-go/eq" G "github.com/IBM/fp-go/readerioeither/generic" ) // Eq implements the equals predicate for values contained in the IOEither monad -func Eq[GRA ~func(context.Context) GIOA, GIOA ~func() E.Either[error, A], A any](eq EQ.Eq[ET.Either[error, A]]) func(context.Context) EQ.Eq[GRA] { +func Eq[GRA ~func(context.Context) GIOA, GIOA ~func() E.Either[error, A], A any](eq EQ.Eq[E.Either[error, A]]) func(context.Context) EQ.Eq[GRA] { return G.Eq[GRA](eq) } diff --git a/context/readerioeither/generic/gen.go b/context/readerioeither/generic/gen.go index 75c4b70..14ac992 100644 --- a/context/readerioeither/generic/gen.go +++ b/context/readerioeither/generic/gen.go @@ -1,8 +1,8 @@ -// Code generated by go generate; DO NOT EDIT. -// This file was generated by robots at -// 2023-07-18 15:21:14.8906482 +0200 CEST m=+0.127356001 package generic +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots at +// 2023-07-19 16:18:34.1526819 +0200 CEST m=+0.012063601 import ( "context" diff --git a/either/gen.go b/either/gen.go index 3847feb..82ae2ce 100644 --- a/either/gen.go +++ b/either/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:17.0339772 +0200 CEST m=+0.084638001 +// 2023-07-19 16:18:36.5482933 +0200 CEST m=+0.013837701 package either diff --git a/function/binds.go b/function/binds.go index e23ae9f..10c3219 100644 --- a/function/binds.go +++ b/function/binds.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:26.5345454 +0200 CEST m=+0.141115301 +// 2023-07-19 16:18:40.5224382 +0200 CEST m=+0.122863501 package function // Combinations for a total of 1 arguments diff --git a/function/gen.go b/function/gen.go index da59388..875a43e 100644 --- a/function/gen.go +++ b/function/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:22.6104564 +0200 CEST m=+0.036231301 +// 2023-07-19 16:18:38.55937 +0200 CEST m=+0.097297601 package function // Pipe0 takes an initial value t0 and successively applies 0 functions where the input of a function is the return value of the previous function diff --git a/identity/gen.go b/identity/gen.go index c2acc45..925ee56 100644 --- a/identity/gen.go +++ b/identity/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:31.1196366 +0200 CEST m=+0.033039101 +// 2023-07-19 16:18:43.8898665 +0200 CEST m=+0.018620401 package identity diff --git a/internal/apply/gen.go b/internal/apply/gen.go index 1a35085..c058a28 100644 --- a/internal/apply/gen.go +++ b/internal/apply/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:34.1598359 +0200 CEST m=+0.018814001 +// 2023-07-19 16:18:46.6106006 +0200 CEST m=+0.058609201 package apply diff --git a/option/gen.go b/option/gen.go index 5c43771..0bc7009 100644 --- a/option/gen.go +++ b/option/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:38.0536143 +0200 CEST m=+0.141220201 +// 2023-07-19 16:18:50.0533497 +0200 CEST m=+0.019645401 package option diff --git a/reader/gen.go b/reader/gen.go index 5754725..5bbc4fe 100644 --- a/reader/gen.go +++ b/reader/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:41.9222583 +0200 CEST m=+0.026140101 +// 2023-07-19 16:18:52.5144916 +0200 CEST m=+0.241563101 package reader diff --git a/reader/generic/gen.go b/reader/generic/gen.go index cf57064..f301422 100644 --- a/reader/generic/gen.go +++ b/reader/generic/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:41.9222583 +0200 CEST m=+0.026140101 +// 2023-07-19 16:18:52.5450262 +0200 CEST m=+0.272097701 package generic diff --git a/readerioeither/gen.go b/readerioeither/gen.go index 933a9fc..ab2f15c 100644 --- a/readerioeither/gen.go +++ b/readerioeither/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:45.9564072 +0200 CEST m=+0.081399101 +// 2023-07-19 16:18:55.4405883 +0200 CEST m=+0.019120301 package readerioeither diff --git a/readerioeither/generic/gen.go b/readerioeither/generic/gen.go index d0b0de7..b99d2f1 100644 --- a/readerioeither/generic/gen.go +++ b/readerioeither/generic/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:45.957526 +0200 CEST m=+0.082517901 +// 2023-07-19 16:18:55.4415885 +0200 CEST m=+0.020120501 package generic diff --git a/readerioeither/reader.go b/readerioeither/reader.go index 9363133..5325e35 100644 --- a/readerioeither/reader.go +++ b/readerioeither/reader.go @@ -114,7 +114,7 @@ func MonadAp[R, E, A, B any](fab ReaderIOEither[R, E, func(A) B], fa ReaderIOEit return G.MonadAp[ReaderIOEither[R, E, A], ReaderIOEither[R, E, B]](fab, fa) } -func Ap[R, E, A, B any](fa ReaderIOEither[R, E, A]) func(fab ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B] { +func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(fab ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B] { return G.Ap[ReaderIOEither[R, E, A], ReaderIOEither[R, E, B], ReaderIOEither[R, E, func(A) B]](fa) } diff --git a/readerioeither/reader_test.go b/readerioeither/reader_test.go index 4b2be79..d0f506e 100644 --- a/readerioeither/reader_test.go +++ b/readerioeither/reader_test.go @@ -45,7 +45,7 @@ func TestOrLeft(t *testing.T) { func TestAp(t *testing.T) { g := F.Pipe1( Right[context.Context, error](utils.Double), - Ap[context.Context, error, int, int](Right[context.Context, error](1)), + Ap[int](Right[context.Context, error](1)), ) assert.Equal(t, E.Right[error](2), g(context.Background())()) diff --git a/samples/readfile/file.json b/samples/readfile/data/file.json similarity index 100% rename from samples/readfile/file.json rename to samples/readfile/data/file.json diff --git a/samples/readfile/data/file1.json b/samples/readfile/data/file1.json new file mode 100644 index 0000000..5d25584 --- /dev/null +++ b/samples/readfile/data/file1.json @@ -0,0 +1,3 @@ +{ + "data": "file1" +} \ No newline at end of file diff --git a/samples/readfile/data/file2.json b/samples/readfile/data/file2.json new file mode 100644 index 0000000..03f5d1a --- /dev/null +++ b/samples/readfile/data/file2.json @@ -0,0 +1,3 @@ +{ + "data": "file2" +} \ No newline at end of file diff --git a/samples/readfile/data/file3.json b/samples/readfile/data/file3.json new file mode 100644 index 0000000..27ed7e8 --- /dev/null +++ b/samples/readfile/data/file3.json @@ -0,0 +1,3 @@ +{ + "data": "file3" +} \ No newline at end of file diff --git a/samples/readfile/readfile_test.go b/samples/readfile/readfile_test.go index 31ddcc4..d6e1125 100644 --- a/samples/readfile/readfile_test.go +++ b/samples/readfile/readfile_test.go @@ -2,8 +2,10 @@ package readfile import ( "context" + "fmt" "testing" + A "github.com/IBM/fp-go/array" R "github.com/IBM/fp-go/context/readerioeither" "github.com/IBM/fp-go/context/readerioeither/file" E "github.com/IBM/fp-go/either" @@ -22,7 +24,7 @@ type RecordType struct { func TestReadSingleFile(t *testing.T) { data := F.Pipe2( - file.ReadFile("./file.json"), + file.ReadFile("./data/file.json"), R.ChainEitherK(J.Unmarshal[RecordType]), R.ChainFirstIOK(IO.Logf[RecordType]("Log: %v")), ) @@ -31,3 +33,26 @@ func TestReadSingleFile(t *testing.T) { assert.Equal(t, E.Of[error](RecordType{"Carsten"}), result()) } + +func idxToFilename(idx int) string { + return fmt.Sprintf("./data/file%d.json", idx+1) +} + +// TestReadMultipleFiles reads the content of a multiple from disk and parses them into +// structs +func TestReadMultipleFiles(t *testing.T) { + + data := F.Pipe2( + A.MakeBy(3, idxToFilename), + R.TraverseArray(F.Flow3( + file.ReadFile, + R.ChainEitherK(J.Unmarshal[RecordType]), + R.ChainFirstIOK(IO.Logf[RecordType]("Log Single: %v")), + )), + R.ChainFirstIOK(IO.Logf[[]RecordType]("Log Result: %v")), + ) + + result := data(context.Background()) + + assert.Equal(t, E.Of[error](A.From(RecordType{"file1"}, RecordType{"file2"}, RecordType{"file3"})), result()) +} diff --git a/tuple/gen.go b/tuple/gen.go index 4a7d9d7..92bade9 100644 --- a/tuple/gen.go +++ b/tuple/gen.go @@ -1,6 +1,6 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2023-07-18 15:21:49.4411816 +0200 CEST m=+0.105633301 +// 2023-07-19 16:18:58.7230486 +0200 CEST m=+0.015290301 package tuple