1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-04-09 15:26:02 +02:00

Compare commits

...

111 Commits

Author SHA1 Message Date
Dr. Carsten Leue
21b517d388 fix: better doc and NonEmptyString
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 14:51:39 +02:00
Dr. Carsten Leue
0df62c0031 fix: unroll array Fold and FoldMap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 11:11:00 +02:00
Dr. Carsten Leue
57318e2d1d fix: make use of Empty lazy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-08 15:00:39 +02:00
Dr. Carsten Leue
2b937d3e93 doc: add marble diagrams
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-30 10:15:27 +02:00
Dr. Carsten Leue
747a1794e5 fix: add more iter operators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-30 10:04:20 +02:00
renovate[bot]
c754cacf1f fix(deps): update module github.com/urfave/cli/v3 to v3.8.0 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 20:40:07 +00:00
Dr. Carsten Leue
d357b32847 fix: add TapThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-23 18:43:49 +01:00
Dr. Carsten Leue
a3af003e74 fix: undo Pipe and Flow changes, did not have the desired effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-20 23:20:07 +01:00
Dr. Carsten Leue
c81235827b fix: try to change the way Pipe and Flow are structured
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-20 12:05:25 +01:00
Dr. Carsten Leue
f35430cf18 fix: introduce async iterators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-18 10:19:03 +01:00
Dr. Carsten Leue
d3ffc71808 fix: add ModifyF
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-17 15:23:10 +01:00
Dr. Carsten Leue
62844b7030 fix: add Filter and FilterMap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 23:33:08 +01:00
Dr. Carsten Leue
99a0ddd4b6 fix: implement filter and filtermap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 23:18:14 +01:00
Dr. Carsten Leue
02acbae8f6 fix: add lenses for Hostname and Port
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 22:49:11 +01:00
Dr. Carsten Leue
eb27ecdc01 fix: clarify behaviour of array.Concat
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-13 18:39:55 +01:00
Dr. Carsten Leue
e5eb7d343c fix: add inline flags
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-13 09:26:39 +01:00
Dr. Carsten Leue
d5a3217251 fix: add FromIso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-12 22:38:26 +01:00
Dr. Carsten Leue
c5cbdaad68 fix: add FromIso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-12 22:38:01 +01:00
Dr. Carsten Leue
5d0f27ad10 fix: add SequenceSeq and TraverseSeq
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-12 20:38:52 +01:00
Dr. Carsten Leue
3a954e0d1f fix: introduce Promap for Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-10 16:10:12 +01:00
Dr. Carsten Leue
cb2e0b23e8 fix: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-09 20:41:56 +01:00
Dr. Carsten Leue
8d5dc7ea1f fix: increase test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:52:08 +01:00
Dr. Carsten Leue
69a11bc681 fix: increase test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:51:44 +01:00
Dr. Carsten Leue
a0910b8279 fix: add -coverpkg=./... to v2
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:34:17 +01:00
Dr. Carsten Leue
029d7be52d fix: better collection of coverage results
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:32:14 +01:00
Dr. Carsten Leue
c6d30bb642 fix: increase test timeout
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:21:27 +01:00
Dr. Carsten Leue
1821f00fbe fix: introduce effect.LocalReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:52:20 +01:00
Dr. Carsten Leue
f0ec0b2541 fix: optimize record performance
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:20:19 +01:00
Dr. Carsten Leue
ce3c7d9359 fix: documentation of endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:02:11 +01:00
Dr. Carsten Leue
3ed354cc8c fix: implement endomorphism.Read
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 19:01:22 +01:00
Dr. Carsten Leue
0932c8c464 fix: add tests for totality and move skills
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 14:12:41 +01:00
Dr. Carsten Leue
475d09e987 fix: add skills
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-07 22:39:33 +01:00
Dr. Carsten Leue
fd21bdeabf fix: signature of local for context/readerresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-07 22:03:17 +01:00
Dr. Carsten Leue
6834f72856 fix: make signature of Local for context more generic, but backwards compatible
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-07 21:02:24 +01:00
Dr. Carsten Leue
8cfb7ef659 fix: logging implementation for context sensitive operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 23:54:42 +01:00
Dr. Carsten Leue
622c87d734 fix: logging implementation for context sensitive operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 23:50:54 +01:00
Dr. Carsten Leue
2ce406a410 fix: add context7 badge
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 15:03:05 +01:00
Dr. Carsten Leue
3743361b9f fix: context7 at correct location
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 14:41:47 +01:00
Dr. Carsten Leue
69d11f0a4b fix: claim context7
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 14:39:58 +01:00
Dr. Carsten Leue
e4dd1169c4 fix: recursion in Errors()
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-04 11:12:22 +01:00
Dr. Carsten Leue
1657569f1d Merge branch 'main' of github.com:IBM/fp-go 2026-03-04 10:31:04 +01:00
Dr. Carsten Leue
545876d013 fix: add bool codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-04 10:30:55 +01:00
renovate[bot]
9492c5d994 chore(deps): update actions/setup-node action to v6.3.0 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 09:20:04 +00:00
Dr. Carsten Leue
94b1ea30d1 fix: improved doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:23:59 +01:00
Dr. Carsten Leue
a77d61f632 fix: add Bind for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:01:21 +01:00
renovate[bot]
66b2f57d73 fix(deps): update module github.com/urfave/cli/v3 to v3.7.0 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 08:40:21 +00:00
Dr. Carsten Leue
92eb2a50a2 fix: support MarshalJSON and MarshalText types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-01 22:38:20 +01:00
Dr. Carsten Leue
3df1dca146 fix: better result type for Pipe
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 17:03:12 +01:00
Dr. Carsten Leue
a0132e2e92 fix: parameter order
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 15:08:11 +01:00
Dr. Carsten Leue
c6b342908d doc: better explanation for logger
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:52:08 +01:00
Dr. Carsten Leue
962237492f doc: explain Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:27:20 +01:00
Dr. Carsten Leue
168a6e1072 fix: add Eitherize to Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 12:55:03 +01:00
Dr. Carsten Leue
4d67b1d254 fix: expose Empty for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 10:52:23 +01:00
Dr. Carsten Leue
77a8cc6b09 fix: implement ApSO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:44:27 +01:00
Dr. Carsten Leue
bc8743fdfc fix: build error
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:21:37 +01:00
Dr. Carsten Leue
1837d3f86d fix: add semigroup helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 16:39:05 +01:00
Dr. Carsten Leue
b2d111e8ec fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 14:04:44 +01:00
Dr. Carsten Leue
ae141c85c6 fix: add tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 13:40:12 +01:00
Dr. Carsten Leue
1230b4581b doc: add doc links
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 10:12:20 +01:00
Dr. Carsten Leue
70c831c8f9 fix: simplify type arguments
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 16:27:21 +01:00
Dr. Carsten Leue
cc0c14c7cf fix: do not fail if coverage fails
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 11:35:16 +01:00
Dr. Carsten Leue
19159ad49e fix: WriteFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 10:42:46 +01:00
Dr. Carsten Leue
b9c8fb4ff1 fix: parameter order for Local and TapIOK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 17:55:32 +01:00
Dr. Carsten Leue
4529e720bc fix: add ChainThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:59:51 +01:00
Dr. Carsten Leue
ef8b2ea65d fix: add ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:31:51 +01:00
Dr. Carsten Leue
5bd7caafdd fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 13:42:29 +01:00
Dr. Carsten Leue
47ebcd79b1 fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:15:00 +01:00
Dr. Carsten Leue
dbad94806e fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:14:21 +01:00
Dr. Carsten Leue
c4cac1cb3e fix: add FromReaderResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-21 16:44:51 +01:00
Dr. Carsten Leue
a3fdb03df4 fix: better assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-13 09:27:49 +01:00
Dr. Carsten Leue
47727fd514 fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:51:34 +01:00
Dr. Carsten Leue
ece7d088ea fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:50:30 +01:00
Dr. Carsten Leue
13d25eca32 fix: add composition logic to Iso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:46:41 +01:00
Dr. Carsten Leue
a68e32308d fix: add filterable to Either and Result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 09:28:42 +01:00
Dr. Carsten Leue
61b948425b fix: cleaner use of Kleisli
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-11 16:24:11 +01:00
Dr. Carsten Leue
a276f3acff fix: add llms.txt
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 09:48:19 +01:00
Dr. Carsten Leue
8c656a4297 fix: more Alt tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 08:52:39 +01:00
Dr. Carsten Leue
bd9a642e93 fix: implement Alt for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 18:31:00 +01:00
Dr. Carsten Leue
3b55cae265 fix: implement alternative monoid for codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 09:59:17 +01:00
Dr. Carsten Leue
1472fa5a50 fix: add some more validation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-04 17:58:08 +01:00
Dr. Carsten Leue
49deb57d24 fix: OrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 17:54:43 +01:00
Dr. Carsten Leue
abb55ddbd0 fix: validation logic and ChainLeft
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 14:00:44 +01:00
Dr. Carsten Leue
f6b01dffdc fix: add ModifiyReaderIOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-02 09:09:04 +01:00
Dr. Carsten Leue
43b666edbb fix: add bind to codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-31 11:47:50 +01:00
Dr. Carsten Leue
e42d765852 fix: readeriooption
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 16:59:32 +01:00
Dr. Carsten Leue
d2da8a32b4 fix: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 11:45:45 +01:00
Dr. Carsten Leue
7484af664b fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:12:27 +01:00
Dr. Carsten Leue
ae38e3f8f4 fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:07:12 +01:00
Dr. Carsten Leue
e0f854bda3 fix: executes_all_IO_operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:25:39 +01:00
Dr. Carsten Leue
34786c3cd8 fix: more tests and lens generation fix for prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:11:46 +01:00
Dr. Carsten Leue
a7aa7e3560 fix: better DI example
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 22:45:17 +01:00
Dr. Carsten Leue
ff2a4299b2 fix: add some useful lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 17:39:34 +01:00
Dr. Carsten Leue
edd66d63e6 fix: more codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 14:51:35 +01:00
Dr. Carsten Leue
909aec8eba fix: better sequence iter
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-26 10:41:25 +01:00
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
Dr. Carsten Leue
cd79dd56b9 fix: simplify tests a bit
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 17:56:28 +01:00
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
Dr. Carsten Leue
46bf065e34 fix: migrate CLI to github.com/urfave/v3
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 12:56:23 +01:00
renovate[bot]
b4e303423b chore(deps): update actions/checkout action to v6.0.2 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 12:39:53 +01:00
renovate[bot]
7afc098f58 fix(deps): update go dependencies (major) (#144)
* fix(deps): update go dependencies

* fix: fix renovate config

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:28:56 +01:00
Dr. Carsten Leue
617e43de19 fix: improve codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:12:40 +01:00
Dr. Carsten Leue
0f7a6c0589 fix: prism doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-22 13:57:07 +01:00
Dr. Carsten Leue
e7f78e1a33 fix: more codecs and cleanup of type hints
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-21 09:23:48 +01:00
Dr. Carsten Leue
6505ab1791 fix: improve Retry implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-20 09:50:20 +01:00
Dr. Carsten Leue
cfa48985ec fix: WithLocal
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 18:36:02 +01:00
Dr. Carsten Leue
677523b70f fix: add nested reader transformer
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 16:34:39 +01:00
Dr. Carsten Leue
8243242cf1 doc: explain use of ReaderIOResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 10:10:43 +01:00
Dr. Carsten Leue
9021a8e274 fix: simplify tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 18:33:15 +01:00
446 changed files with 100817 additions and 2451 deletions

View File

@@ -24,24 +24,25 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
- name: Run tests
run: |
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -64,13 +65,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
go-version: ['1.24.x', '1.25.x', '1.26.x']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -78,9 +79,10 @@ jobs:
run: |
cd v2
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -106,6 +108,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -126,17 +129,17 @@ jobs:
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ env.LATEST_GO_VERSION }}
cache: true # Enable Go module caching

4
context7.json Normal file
View File

@@ -0,0 +1,4 @@
{
"url": "https://context7.com/ibm/fp-go",
"public_key": "pk_7wJdJRn8zGHxvIYu7eh9h"
}

16
go.sum
View File

@@ -1,7 +1,3 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,20 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=

View File

@@ -22,7 +22,8 @@
"matchDepTypes": [
"golang"
],
"enabled": false
"enabled": false,
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
},
{
"matchUpdateTypes": [

318
skills/fp-go-http/SKILL.md Normal file
View File

@@ -0,0 +1,318 @@
# fp-go HTTP Requests
## Overview
fp-go wraps `net/http` in the `ReaderIOResult` monad, giving you composable, context-aware HTTP operations with automatic error propagation. The core package is:
```
github.com/IBM/fp-go/v2/context/readerioresult/http
```
All HTTP operations are lazy — they describe what to do but do not execute until you call the resulting function with a `context.Context`.
## Core Types
```go
// Requester builds an *http.Request given a context.
type Requester = ReaderIOResult[*http.Request] // func(context.Context) func() result.Result[*http.Request]
// Client executes a Requester and returns the response wrapped in ReaderIOResult.
type Client interface {
Do(Requester) ReaderIOResult[*http.Response]
}
```
## Basic Usage
### 1. Create a Client
```go
import (
HTTP "net/http"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
)
client := H.MakeClient(HTTP.DefaultClient)
// Or with a custom client:
custom := &HTTP.Client{Timeout: 10 * time.Second}
client := H.MakeClient(custom)
```
### 2. Build a Request
```go
// GET request (most common)
req := H.MakeGetRequest("https://api.example.com/users/1")
// Arbitrary method + body
req := H.MakeRequest("POST", "https://api.example.com/users", bodyReader)
```
### 3. Execute and Parse
```go
import (
"context"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
client := H.MakeClient(HTTP.DefaultClient)
// ReadJSON validates status, Content-Type, then unmarshals JSON
result := H.ReadJSON[User](client)(H.MakeGetRequest("https://api.example.com/users/1"))
// Execute — provide context once
user, err := result(context.Background())()
```
## Response Readers
All accept a `Client` and return a function `Requester → ReaderIOResult[A]`:
| Function | Returns | Notes |
|----------|---------|-------|
| `ReadJSON[A](client)` | `ReaderIOResult[A]` | Validates status + Content-Type, unmarshals JSON |
| `ReadText(client)` | `ReaderIOResult[string]` | Validates status, reads body as UTF-8 string |
| `ReadAll(client)` | `ReaderIOResult[[]byte]` | Validates status, returns raw body bytes |
| `ReadFullResponse(client)` | `ReaderIOResult[FullResponse]` | Returns `Pair[*http.Response, []byte]` |
`FullResponse = Pair[*http.Response, []byte]` — use `pair.First` / `pair.Second` to access components.
## Composing Requests in Pipelines
```go
import (
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
)
client := H.MakeClient(HTTP.DefaultClient)
readPost := H.ReadJSON[Post](client)
pipeline := F.Pipe2(
H.MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1"),
readPost,
RIO.ChainFirstIOK(IO.Logf[Post]("Got post: %v")),
)
post, err := pipeline(context.Background())()
```
## Parallel Requests — Homogeneous Types
Use `RIO.TraverseArray` when all requests return the same type:
```go
import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
)
type PostItem struct {
UserID uint `json:"userId"`
ID uint `json:"id"`
Title string `json:"title"`
}
client := H.MakeClient(HTTP.DefaultClient)
readPost := H.ReadJSON[PostItem](client)
// Fetch 10 posts in parallel
data := F.Pipe3(
A.MakeBy(10, func(i int) string {
return fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", i+1)
}),
RIO.TraverseArray(F.Flow3(
H.MakeGetRequest,
readPost,
RIO.ChainFirstIOK(IO.Logf[PostItem]("Post: %v")),
)),
RIO.ChainFirstIOK(IO.Logf[[]PostItem]("All posts: %v")),
RIO.Map(A.Size[PostItem]),
)
count, err := data(context.Background())()
```
## Parallel Requests — Heterogeneous Types
Use `RIO.TraverseTuple2` (or `Tuple3`, etc.) when requests return different types:
```go
import (
T "github.com/IBM/fp-go/v2/tuple"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
F "github.com/IBM/fp-go/v2/function"
)
type CatFact struct {
Fact string `json:"fact"`
}
client := H.MakeClient(HTTP.DefaultClient)
readPost := H.ReadJSON[PostItem](client)
readCatFact := H.ReadJSON[CatFact](client)
// Execute both requests in parallel with different response types
data := F.Pipe3(
T.MakeTuple2(
"https://jsonplaceholder.typicode.com/posts/1",
"https://catfact.ninja/fact",
),
T.Map2(H.MakeGetRequest, H.MakeGetRequest), // build both requesters
RIO.TraverseTuple2(readPost, readCatFact), // run in parallel, typed
RIO.ChainFirstIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Result: %v")),
)
both, err := data(context.Background())()
// both.F1 is PostItem, both.F2 is CatFact
```
## Building Requests with the Builder API
For complex requests (custom headers, query params, JSON body), use the builder:
```go
import (
B "github.com/IBM/fp-go/v2/http/builder"
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
F "github.com/IBM/fp-go/v2/function"
)
// GET with query parameters
req := F.Pipe2(
B.Default,
B.WithURL("https://api.example.com/items?page=1"),
B.WithQueryArg("limit")("50"),
)
requester := RB.Requester(req)
// POST with JSON body
req := F.Pipe3(
B.Default,
B.WithURL("https://api.example.com/users"),
B.WithMethod("POST"),
B.WithJSON(map[string]string{"name": "Alice"}),
// sets Content-Type: application/json automatically
)
requester := RB.Requester(req)
// With authentication and custom headers
req := F.Pipe3(
B.Default,
B.WithURL("https://api.example.com/protected"),
B.WithBearer("my-token"), // sets Authorization: Bearer my-token
B.WithHeader("X-Request-ID")("123"),
)
requester := RB.Requester(req)
// Execute
result := H.ReadJSON[Response](client)(requester)
data, err := result(ctx)()
```
### Builder Functions
| Function | Effect |
|----------|--------|
| `B.WithURL(url)` | Set the target URL |
| `B.WithMethod(method)` | Set HTTP method (GET, POST, PUT, DELETE, …) |
| `B.WithJSON(v)` | Marshal `v` as JSON body, set `Content-Type: application/json` |
| `B.WithBytes(data)` | Set raw bytes body, set `Content-Length` automatically |
| `B.WithHeader(key)(value)` | Add a request header |
| `B.WithBearer(token)` | Set `Authorization: Bearer <token>` |
| `B.WithQueryArg(key)(value)` | Append a query parameter |
## Error Handling
Errors from request creation, HTTP status codes, Content-Type validation, and JSON parsing all propagate automatically through the `Result` monad. You only handle errors at the call site:
```go
// Pattern 1: direct extraction
value, err := pipeline(ctx)()
if err != nil { /* handle */ }
// Pattern 2: Fold for clean HTTP handler
RIO.Fold(
func(err error) { http.Error(w, err.Error(), http.StatusInternalServerError) },
func(data MyType) { json.NewEncoder(w).Encode(data) },
)(pipeline)(ctx)()
```
## Full HTTP Handler Example
```go
package main
import (
"context"
"encoding/json"
"net/http"
HTTP "net/http"
"fmt"
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
)
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
}
var client = H.MakeClient(HTTP.DefaultClient)
func fetchPost(id int) RIO.ReaderIOResult[Post] {
url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)
return F.Pipe2(
H.MakeGetRequest(url),
H.ReadJSON[Post](client),
RIO.ChainFirstIOK(IO.Logf[Post]("fetched: %v")),
)
}
func handler(w http.ResponseWriter, r *http.Request) {
RIO.Fold(
func(err error) {
http.Error(w, err.Error(), http.StatusBadGateway)
},
func(post Post) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(post)
},
)(fetchPost(1))(r.Context())()
}
```
## Import Reference
```go
import (
HTTP "net/http"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
B "github.com/IBM/fp-go/v2/http/builder"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/array"
T "github.com/IBM/fp-go/v2/tuple"
IO "github.com/IBM/fp-go/v2/io"
)
```
Requires Go 1.24+.

View File

@@ -0,0 +1,410 @@
# fp-go Logging
## Overview
fp-go provides logging utilities that integrate naturally with functional pipelines. Logging is always a **side effect** — it should not change the value being processed. The library achieves this through `ChainFirst`-style combinators that thread the original value through unchanged while performing the log.
## Packages
| Package | Purpose |
|---------|---------|
| `github.com/IBM/fp-go/v2/logging` | Global logger, context-embedded logger, `LoggingCallbacks` |
| `github.com/IBM/fp-go/v2/io` | `Logf`, `Logger`, `LogGo`, `Printf`, `PrintGo` — IO-level logging helpers |
| `github.com/IBM/fp-go/v2/readerio` | `SLog`, `SLogWithCallback` — structured logging for ReaderIO |
| `github.com/IBM/fp-go/v2/context/readerio` | `SLog`, `SLogWithCallback` — structured logging for context ReaderIO |
| `github.com/IBM/fp-go/v2/context/readerresult` | `SLog`, `TapSLog`, `SLogWithCallback` — structured logging for ReaderResult |
| `github.com/IBM/fp-go/v2/context/readerioresult` | `SLog`, `TapSLog`, `SLogWithCallback`, `LogEntryExit`, `LogEntryExitWithCallback` — full suite for ReaderIOResult |
## Logging Inside Pipelines
The idiomatic way to log inside a monadic pipeline is `ChainFirstIOK` (or `ChainFirst` where the monad is already IO). These combinators execute a side-effecting function and pass the **original value** downstream unchanged.
### With `IOResult` / `ReaderIOResult` — printf-style
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
fetchUser(42),
RIO.ChainEitherK(validateUser),
// Log after validation — value flows through unchanged
RIO.ChainFirstIOK(IO.Logf[User]("Validated user: %v")),
RIO.Map(enrichUser),
)
```
`IO.Logf[A](format string) func(A) IO[A]` logs using `log.Printf` and returns the value unchanged. It's a Kleisli arrow suitable for `ChainFirst` and `ChainFirstIOK`.
### With `IOEither` / plain `IO`
```go
import (
IOE "github.com/IBM/fp-go/v2/ioeither"
IO "github.com/IBM/fp-go/v2/io"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
file.ReadFile("config.json"),
IOE.ChainEitherK(J.Unmarshal[Config]),
IOE.ChainFirstIOK(IO.Logf[Config]("Loaded config: %v")),
IOE.Map[error](processConfig),
)
```
### Logging Arrays in TraverseArray
```go
import (
A "github.com/IBM/fp-go/v2/array"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
F "github.com/IBM/fp-go/v2/function"
)
// Log each item individually, then log the final slice
pipeline := F.Pipe2(
A.MakeBy(3, idxToFilename),
RIO.TraverseArray(F.Flow3(
file.ReadFile,
RIO.ChainEitherK(J.Unmarshal[Record]),
RIO.ChainFirstIOK(IO.Logf[Record]("Parsed record: %v")),
)),
RIO.ChainFirstIOK(IO.Logf[[]Record]("All records: %v")),
)
```
## IO Logging Functions
All live in `github.com/IBM/fp-go/v2/io`:
### `Logf` — printf-style
```go
IO.Logf[A any](format string) func(A) IO[A]
```
Uses `log.Printf`. The format string works like `fmt.Sprintf`.
```go
IO.Logf[User]("Processing user: %+v")
IO.Logf[int]("Count: %d")
```
### `Logger` — with custom `*log.Logger`
```go
IO.Logger[A any](loggers ...*log.Logger) func(prefix string) func(A) IO[A]
```
Uses `logger.Printf(prefix+": %v", value)`. Pass your own `*log.Logger` instance.
```go
customLog := log.New(os.Stderr, "APP ", log.LstdFlags)
logUser := IO.Logger[User](customLog)("user")
// logs: "APP user: {ID:42 Name:Alice}"
```
### `LogGo` — Go template syntax
```go
IO.LogGo[A any](tmpl string) func(A) IO[A]
```
Uses Go's `text/template`. The template receives the value as `.`.
```go
type User struct{ Name string; Age int }
IO.LogGo[User]("User {{.Name}} is {{.Age}} years old")
```
### `Printf` / `PrintGo` — stdout instead of log
Same signatures as `Logf` / `LogGo` but use `fmt.Printf`/`fmt.Println` (no log prefix, no timestamp).
```go
IO.Printf[Result]("Result: %v\n")
IO.PrintGo[User]("Name: {{.Name}}")
```
## Structured Logging in the `context` Package
The `context/readerioresult`, `context/readerresult`, and `context/readerio` packages provide structured `slog`-based logging functions that are context-aware: they retrieve the logger from the context (via `logging.GetLoggerFromContext`) rather than using a fixed logger instance.
### `TapSLog` — inline structured logging in a ReaderIOResult pipeline
`TapSLog` is an **Operator** (`func(ReaderIOResult[A]) ReaderIOResult[A]`). It sits directly in a `F.Pipe` call on a `ReaderIOResult`, logs the current value or error using `slog`, and passes the result through unchanged.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe4(
fetchOrder(orderID),
RIO.TapSLog[Order]("Order fetched"), // logs value=<Order> or error=<err>
RIO.Chain(validateOrder),
RIO.TapSLog[Order]("Order validated"),
RIO.Chain(processPayment),
)
result, err := pipeline(ctx)()
```
- Logs **both** success values (`value=<A>`) and errors (`error=<err>`) using `slog` structured attributes.
- Respects the logger level — if the logger is configured to discard Info-level logs, nothing is written.
- Available in both `context/readerioresult` and `context/readerresult`.
### `SLog` — Kleisli-style structured logging
`SLog` is a **Kleisli arrow** (`func(Result[A]) ReaderResult[A]` / `func(Result[A]) ReaderIOResult[A]`). It is used with `Chain` when you want to intercept the raw `Result` directly.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
fetchData(id),
RIO.Chain(RIO.SLog[Data]("Data fetched")), // log raw Result, pass it through
RIO.Chain(validateData),
RIO.Chain(RIO.SLog[Data]("Data validated")),
RIO.Chain(processData),
)
```
**Difference from `TapSLog`:**
- `TapSLog[A](msg)` is an `Operator[A, A]` — used directly in `F.Pipe` on a `ReaderIOResult[A]`.
- `SLog[A](msg)` is a `Kleisli[Result[A], A]` — used with `Chain`, giving access to the raw `Result[A]`.
Both log in the same format. `TapSLog` is more ergonomic in most pipelines.
### `SLogWithCallback` — custom log level and logger source
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
"log/slog"
)
// Log at DEBUG level with a custom logger extracted from context
debugLog := RIO.SLogWithCallback[User](
slog.LevelDebug,
logging.GetLoggerFromContext, // or any func(context.Context) *slog.Logger
"Fetched user",
)
pipeline := F.Pipe2(
fetchUser(123),
RIO.Chain(debugLog),
RIO.Map(func(u User) string { return u.Name }),
)
```
### `LogEntryExit` — automatic entry/exit timing with correlation IDs
`LogEntryExit` wraps a `ReaderIOResult` computation with structured entry and exit log messages. It assigns a unique **correlation ID** (`ID=<n>`) to each invocation so concurrent or nested operations can be correlated in logs.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
fetchUser(123),
RIO.LogEntryExit[User]("fetchUser"), // wraps the operation
RIO.Chain(func(user User) RIO.ReaderIOResult[[]Order] {
return F.Pipe1(
fetchOrders(user.ID),
RIO.LogEntryExit[[]Order]("fetchOrders"),
)
}),
)
result, err := pipeline(ctx)()
// Logs:
// level=INFO msg="[entering]" name=fetchUser ID=1
// level=INFO msg="[exiting ]" name=fetchUser ID=1 duration=42ms
// level=INFO msg="[entering]" name=fetchOrders ID=2
// level=INFO msg="[exiting ]" name=fetchOrders ID=2 duration=18ms
```
On error, the exit log changes to `[throwing]` and includes the error:
```
level=INFO msg="[throwing]" name=fetchUser ID=3 duration=5ms error="user not found"
```
Key properties:
- **Correlation ID** (`ID=`) is unique per operation, monotonically increasing, and stored in the context so nested operations can access the parent's ID.
- **Duration** (`duration=`) is measured from entry to exit.
- **Logger is taken from the context** — embed a request-scoped logger with `logging.WithLogger` before executing the pipeline and `LogEntryExit` picks it up automatically.
- **Level-aware** — if the logger does not have the log level enabled, the entire entry/exit instrumentation is skipped (zero overhead).
- The original `ReaderIOResult[A]` value flows through **unchanged**.
```go
// Use a context logger so all log messages carry request metadata
cancelFn, ctxWithLogger := pair.Unpack(
logging.WithLogger(
slog.Default().With("requestID", r.Header.Get("X-Request-ID")),
)(r.Context()),
)
defer cancelFn()
result, err := pipeline(ctxWithLogger)()
```
### `LogEntryExitWithCallback` — custom log level
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
"log/slog"
)
// Log at DEBUG level instead of INFO
debugPipeline := F.Pipe1(
expensiveComputation(),
RIO.LogEntryExitWithCallback[Result](
slog.LevelDebug,
logging.GetLoggerFromContext,
"expensiveComputation",
),
)
```
### `SLog` / `SLogWithCallback` in `context/readerresult`
The same `SLog` and `TapSLog` functions are also available in `context/readerresult` for use with the synchronous `ReaderResult[A] = func(context.Context) (A, error)` monad:
```go
import RR "github.com/IBM/fp-go/v2/context/readerresult"
pipeline := F.Pipe3(
queryDB(id),
RR.TapSLog[Row]("Row fetched"),
RR.Chain(parseRow),
RR.TapSLog[Record]("Record parsed"),
)
```
## Global Logger (`logging` package)
The `logging` package manages a global `*slog.Logger` (structured logging, Go 1.21+).
```go
import "github.com/IBM/fp-go/v2/logging"
// Get the current global logger (defaults to slog.Default())
logger := logging.GetLogger()
logger.Info("application started", "version", "1.0")
// Replace the global logger; returns the old one for deferred restore
old := logging.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
defer logging.SetLogger(old)
```
## Context-Embedded Logger
Embed a `*slog.Logger` in a `context.Context` to carry request-scoped loggers across the call stack. All context-package logging functions (`TapSLog`, `SLog`, `LogEntryExit`) pick up this logger automatically.
```go
import (
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/pair"
"log/slog"
)
// Create a request-scoped logger
reqLogger := slog.Default().With("requestID", "abc-123")
// Embed it into a context using the Kleisli arrow WithLogger
cancelFn, ctxWithLogger := pair.Unpack(logging.WithLogger(reqLogger)(ctx))
defer cancelFn()
// All downstream logging (TapSLog, LogEntryExit, etc.) uses reqLogger
result, err := pipeline(ctxWithLogger)()
```
`WithLogger` returns a `ContextCancel = Pair[context.CancelFunc, context.Context]`. The cancel function is a no-op — the context is only enriched, not made cancellable.
`GetLoggerFromContext` falls back to the global logger if no logger is found in the context.
## `LoggingCallbacks` — Dual-Logger Pattern
```go
import "github.com/IBM/fp-go/v2/logging"
// Returns (infoCallback, errorCallback) — both are func(string, ...any)
infoLog, errLog := logging.LoggingCallbacks() // use log.Default() for both
infoLog, errLog := logging.LoggingCallbacks(myLogger) // same logger for both
infoLog, errLog := logging.LoggingCallbacks(infoLog, errorLog) // separate loggers
```
Used internally by `io.Logger` and by packages that need separate info/error sinks.
## Choosing the Right Logging Function
| Situation | Use |
|-----------|-----|
| Quick printf logging mid-pipeline | `IO.Logf[A]("fmt")` with `ChainFirstIOK` |
| Go template formatting mid-pipeline | `IO.LogGo[A]("tmpl")` with `ChainFirstIOK` |
| Print to stdout (no log prefix) | `IO.Printf[A]("fmt")` with `ChainFirstIOK` |
| Structured slog — log value or error inline | `RIO.TapSLog[A]("msg")` (Operator, used in Pipe) |
| Structured slog — intercept raw Result | `RIO.Chain(RIO.SLog[A]("msg"))` (Kleisli) |
| Structured slog — custom log level | `RIO.SLogWithCallback[A](level, cb, "msg")` |
| Entry/exit timing + correlation IDs | `RIO.LogEntryExit[A]("name")` |
| Entry/exit at custom log level | `RIO.LogEntryExitWithCallback[A](level, cb, "name")` |
| Structured logging globally | `logging.GetLogger()` / `logging.SetLogger()` |
| Request-scoped logger in context | `logging.WithLogger(logger)` + `logging.GetLoggerFromContext(ctx)` |
| Custom `*log.Logger` in pipeline | `IO.Logger[A](logger)("prefix")` with `ChainFirstIOK` |
## Complete Example
```go
package main
import (
"context"
"log/slog"
"os"
F "github.com/IBM/fp-go/v2/function"
IO "github.com/IBM/fp-go/v2/io"
L "github.com/IBM/fp-go/v2/logging"
P "github.com/IBM/fp-go/v2/pair"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
)
func main() {
// Configure JSON structured logging globally
L.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
// Embed a request-scoped logger into the context
_, ctx := P.Unpack(L.WithLogger(
L.GetLogger().With("requestID", "req-001"),
)(context.Background()))
pipeline := F.Pipe5(
fetchData(42),
RIO.LogEntryExit[Data]("fetchData"), // entry/exit with timing + ID
RIO.TapSLog[Data]("raw data"), // inline structured value log
RIO.ChainEitherK(transformData),
RIO.LogEntryExit[Result]("transformData"),
RIO.ChainFirstIOK(IO.LogGo[Result]("result: {{.Value}}")), // template log
)
value, err := pipeline(ctx)()
if err != nil {
L.GetLogger().Error("pipeline failed", "error", err)
}
_ = value
}
```

View File

@@ -0,0 +1,520 @@
# fp-go Monadic Operations
## Overview
`fp-go` (import path `github.com/IBM/fp-go/v2`) brings type-safe functional programming to Go using generics. Every monad follows a **consistent interface**: once you know the pattern in one monad, it transfers to all others.
All functions use the **data-last** principle: the data being transformed is always the last argument, enabling partial application and pipeline composition.
## Core Types
| Type | Package | Represents |
|------|---------|------------|
| `Option[A]` | `option` | A value that may or may not be present (replaces nil) |
| `Either[E, A]` | `either` | A value that is either a left error `E` or a right success `A` |
| `Result[A]` | `result` | `Either[error, A]` — shorthand for the common case |
| `IO[A]` | `io` | A lazy computation that produces `A` (possibly with side effects) |
| `IOResult[A]` | `ioresult` | `IO[Result[A]]` — lazy computation that can fail |
| `ReaderIOResult[A]` | `context/readerioresult` | `func(context.Context) IOResult[A]` — context-aware IO with errors |
| `Effect[C, A]` | `effect` | `func(C) ReaderIOResult[A]` — typed dependency injection + IO + errors |
Idiomatic (high-performance, tuple-based) equivalents live in `idiomatic/`:
- `idiomatic/option``(A, bool)` tuples
- `idiomatic/result``(A, error)` tuples
- `idiomatic/ioresult``func() (A, error)`
- `idiomatic/context/readerresult``func(context.Context) (A, error)`
## Standard Operations
Every monad exports these operations (PascalCase for exported Go names):
| fp-go | fp-ts / Haskell | Description |
|-------|----------------|-------------|
| `Of` | `of` / `pure` | Lift a pure value into the monad |
| `Map` | `map` / `fmap` | Transform the value inside without changing the context |
| `Chain` | `chain` / `>>=` | Sequence a computation that itself returns a monadic value |
| `Ap` | `ap` / `<*>` | Apply a wrapped function to a wrapped value |
| `Fold` | `fold` / `either` | Eliminate the context — handle every case and extract a plain value |
| `GetOrElse` | `getOrElse` / `fromMaybe` | Extract the value or use a default (Option/Result) |
| `Filter` | `filter` / `mfilter` | Keep only values satisfying a predicate |
| `Flatten` | `flatten` / `join` | Remove one level of nesting (`M[M[A]]``M[A]`) |
| `ChainFirst` | `chainFirst` / `>>` | Sequence for side effects; keeps the original value |
| `Alt` | `alt` / `<\|>` | Provide an alternative when the first computation fails |
| `FromPredicate` | `fromPredicate` / `guard` | Build a monadic value from a predicate |
| `Sequence` | `sequence` | Turn `[]M[A]` into `M[[]A]` |
| `Traverse` | `traverse` | Map and sequence in one step |
Curried (composable) vs. monadic (direct) form:
```go
// Curried — data last, returns a transformer function
option.Map(strings.ToUpper) // func(Option[string]) Option[string]
// Monadic — data first, immediate execution
option.MonadMap(option.Some("hello"), strings.ToUpper)
```
Use curried form for pipelines; use `Monad*` form when you already have all arguments.
## Key Type Aliases (defined per monad)
```go
// A Kleisli arrow: a function from A to a monadic B
type Kleisli[A, B any] = func(A) M[B]
// An operator: transforms one monadic value into another
type Operator[A, B any] = func(M[A]) M[B]
```
`Chain` takes a `Kleisli`, `Map` returns an `Operator`. The naming is consistent across all monads.
## Examples
### Option — nullable values without nil
```go
import (
O "github.com/IBM/fp-go/v2/option"
F "github.com/IBM/fp-go/v2/function"
"strconv"
)
parseAndDouble := F.Flow2(
O.FromPredicate(func(s string) bool { return s != "" }),
O.Chain(func(s string) O.Option[int] {
n, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(n * 2)
}),
)
parseAndDouble("21") // Some(42)
parseAndDouble("") // None
parseAndDouble("abc") // None
```
### Result — error handling without if-err boilerplate
```go
import (
R "github.com/IBM/fp-go/v2/result"
F "github.com/IBM/fp-go/v2/function"
"strconv"
"errors"
)
parse := R.Eitherize1(strconv.Atoi) // lifts (int, error) → Result[int]
validate := func(n int) R.Result[int] {
if n < 0 {
return R.Error[int](errors.New("must be non-negative"))
}
return R.Of(n)
}
pipeline := F.Flow2(parse, R.Chain(validate))
pipeline("42") // Ok(42)
pipeline("-1") // Error("must be non-negative")
pipeline("abc") // Error(strconv parse error)
```
### IOResult — lazy IO with error handling
```go
import (
IOE "github.com/IBM/fp-go/v2/ioresult"
F "github.com/IBM/fp-go/v2/function"
J "github.com/IBM/fp-go/v2/json"
"os"
)
readConfig := F.Flow2(
IOE.Eitherize1(os.ReadFile), // func(string) IOResult[[]byte]
IOE.ChainEitherK(J.Unmarshal[Config]), // parse JSON, propagate errors
)
result := readConfig("config.json")() // execute lazily
```
### ReaderIOResult — context-aware pipelines (recommended for services)
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"context"
)
// type ReaderIOResult[A any] = func(context.Context) func() result.Result[A]
fetchUser := func(id int) RIO.ReaderIOResult[User] {
return func(ctx context.Context) func() result.Result[User] {
return func() result.Result[User] {
// perform IO here
}
}
}
pipeline := F.Pipe3(
fetchUser(42),
RIO.ChainEitherK(validateUser), // lift pure (User, error) function
RIO.Map(enrichUser), // lift pure User → User function
RIO.ChainFirstIOK(IO.Logf[User]("Fetched: %v")), // side-effect logging
)
user, err := pipeline(ctx)() // provide context once, execute
```
### Traversal — process slices monadically
```go
import (
A "github.com/IBM/fp-go/v2/array"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
// Fetch all users, stop on first error
fetchAll := F.Pipe1(
A.MakeBy(10, userID),
RIO.TraverseArray(fetchUser), // []ReaderIOResult[User] → ReaderIOResult[[]User]
)
```
## Function Composition with Flow and Pipe
```go
import F "github.com/IBM/fp-go/v2/function"
// Flow: compose functions left-to-right, returns a new function
transform := F.Flow3(
option.Map(strings.TrimSpace),
option.Filter(func(s string) bool { return s != "" }),
option.GetOrElse(func() string { return "default" }),
)
result := transform(option.Some(" hello ")) // "hello"
// Pipe: apply a value through a pipeline immediately
result := F.Pipe3(
option.Some(" hello "),
option.Map(strings.TrimSpace),
option.Filter(func(s string) bool { return s != "" }),
option.GetOrElse(func() string { return "default" }),
)
```
## Lifting Pure Functions into Monadic Context
fp-go provides helpers to promote non-monadic functions:
| Helper | Lifts |
|--------|-------|
| `ChainEitherK` | `func(A) (B, error)` → works inside the monad |
| `ChainOptionK` | `func(A) Option[B]` → works inside the monad |
| `ChainFirstIOK` | `func(A) IO[B]` for side effects, keeps original value |
| `Eitherize1..N` | `func(A) (B, error)``func(A) Result[B]` |
| `FromPredicate` | `func(A) bool` + error builder → `func(A) Result[A]` |
## Type Parameter Ordering Rule (V2)
Non-inferrable type parameters come **first**, so the compiler can infer the rest:
```go
// B cannot be inferred from the argument — it comes first
result := either.Ap[string](value)(funcInEither)
// All types inferrable — no explicit params needed
result := either.Map(transform)(value)
result := either.Chain(validator)(value)
```
## When to Use Which Monad
| Situation | Use |
|-----------|-----|
| Value that might be absent | `Option[A]` |
| Operation that can fail with custom error type | `Either[E, A]` |
| Operation that can fail with `error` | `Result[A]` |
| Lazy IO, side effects | `IO[A]` |
| IO that can fail | `IOResult[A]` |
| IO + context (cancellation, deadlines) | `ReaderIOResult[A]` from `context/readerioresult` |
| IO + context + typed dependencies | `Effect[C, A]` |
| High-performance services | Idiomatic packages in `idiomatic/` |
## Do-Notation: Accumulating State with `Bind` and `ApS`
When a pipeline needs to carry **multiple intermediate results** forward — not just a single value — the `Chain`/`Map` style becomes unwieldy because each step only threads one value and prior results are lost. Do-notation solves this by accumulating results into a growing struct (the "state") at each step.
Every monad that supports do-notation exports the same family of functions. The examples below use `context/readerioresult` (`RIO`), but the identical API is available in `result`, `option`, `ioresult`, `readerioresult`, and others.
### The Function Family
| Function | Kind | What it does |
|----------|------|-------------|
| `Do(empty S)` | — | Lift an empty struct into the monad; starting point |
| `BindTo(setter)` | monadic | Convert an existing `M[T]` into `M[S]`; alternative start |
| `Bind(setter, f)` | monadic | Add a result; `f` receives the **current state** and returns `M[T]` |
| `ApS(setter, fa)` | applicative | Add a result; `fa` is **independent** of the current state |
| `Let(setter, f)` | pure | Add a value computed by a **pure function** of the state |
| `LetTo(setter, value)` | pure | Add a **constant** value |
Lens variants (`BindL`, `ApSL`, `LetL`, `LetToL`) accept a `Lens[S, T]` instead of a manual setter, integrating naturally with the optics system.
### `Bind` — Sequential, Dependent Steps
`Bind` sequences two monadic computations. The function `f` receives the **full accumulated state** so it can read anything gathered so far. Errors short-circuit automatically.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
"context"
)
type Pipeline struct {
User User
Config Config
Posts []Post
}
// Lenses — focus on individual fields; .Set is already func(T) func(S) S
var (
userLens = L.MakeLens(func(s Pipeline) User { return s.User }, func(s Pipeline, u User) Pipeline { s.User = u; return s })
configLens = L.MakeLens(func(s Pipeline) Config { return s.Config }, func(s Pipeline, c Config) Pipeline { s.Config = c; return s })
postsLens = L.MakeLens(func(s Pipeline) []Post { return s.Posts }, func(s Pipeline, p []Post) Pipeline { s.Posts = p; return s })
)
result := F.Pipe3(
RIO.Do(Pipeline{}), // lift empty struct
RIO.Bind(userLens.Set, func(_ Pipeline) RIO.ReaderIOResult[User] { return fetchUser(42) }),
RIO.Bind(configLens.Set, F.Flow2(userLens.Get, fetchConfigForUser)), // read s.User, pass to fetcher
RIO.Bind(postsLens.Set, F.Flow2(userLens.Get, fetchPostsForUser)), // read s.User, pass to fetcher
)
pipeline, err := result(context.Background())()
// pipeline.User, pipeline.Config, pipeline.Posts are all populated
```
The setter signature is `func(T) func(S1) S2` — it takes the new value and returns a state transformer. `lens.Set` already has this shape, so no manual setter functions are needed. `F.Flow2(lens.Get, f)` composes the field getter with any Kleisli arrow `f` point-free.
### `ApS` — Independent, Applicative Steps
`ApS` uses **applicative** semantics: `fa` is evaluated without any access to the current state. Use it when steps have no dependency on each other — the library can choose to execute them concurrently.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type Summary struct {
User User
Weather Weather
}
var (
userLens = L.MakeLens(func(s Summary) User { return s.User }, func(s Summary, u User) Summary { s.User = u; return s })
weatherLens = L.MakeLens(func(s Summary) Weather { return s.Weather }, func(s Summary, w Weather) Summary { s.Weather = w; return s })
)
// Both are independent — neither needs the other's result
result := F.Pipe2(
RIO.Do(Summary{}),
RIO.ApS(userLens.Set, fetchUser(42)),
RIO.ApS(weatherLens.Set, fetchWeather("NYC")),
)
```
**Key difference from `Bind`:**
| | `Bind(setter, f)` | `ApS(setter, fa)` |
|-|---|---|
| Second argument | `func(S1) M[T]` — a **function** of state | `M[T]` — a **fixed** monadic value |
| Can read prior state? | Yes — receives `S1` | No — no access to state |
| Semantics | Monadic (sequential) | Applicative (independent) |
### `Let` and `LetTo` — Pure Additions
`Let` adds a value computed by a **pure function** of the current state (no monad, cannot fail):
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type Enriched struct {
User User
FullName string
}
var (
userLens = L.MakeLens(func(s Enriched) User { return s.User }, func(s Enriched, u User) Enriched { s.User = u; return s })
fullNameLens = L.MakeLens(func(s Enriched) string { return s.FullName }, func(s Enriched, n string) Enriched { s.FullName = n; return s })
)
fullName := func(u User) string { return u.FirstName + " " + u.LastName }
result := F.Pipe2(
RIO.Do(Enriched{}),
RIO.Bind(userLens.Set, func(_ Enriched) RIO.ReaderIOResult[User] { return fetchUser(42) }),
RIO.Let(fullNameLens.Set, F.Flow2(userLens.Get, fullName)), // read s.User, compute pure string
)
```
`LetTo` adds a **constant** with no computation:
```go
RIO.LetTo(setVersion, "v1.2.3")
```
### `BindTo` — Starting from an Existing Value
When you have an existing `M[T]` and want to project it into a state struct rather than starting from `Do(empty)`:
```go
type State struct{ User User }
result := F.Pipe1(
fetchUser(42), // ReaderIOResult[User]
RIO.BindTo(func(u User) State { return State{User: u} }),// ReaderIOResult[State]
)
```
### Lens Variants (`ApSL`, `BindL`, `LetL`, `LetToL`)
If you have a `Lens[S, T]` (from the optics system or code generation), you can skip writing the setter function entirely:
```go
import (
RO "github.com/IBM/fp-go/v2/readeroption"
F "github.com/IBM/fp-go/v2/function"
)
// Lenses generated by go:generate (see optics/README.md)
// personLenses.Name : Lens[*Person, Name]
// personLenses.Age : Lens[*Person, Age]
makePerson := F.Pipe2(
RO.Do[*PartialPerson](emptyPerson),
RO.ApSL(personLenses.Name, maybeName), // replaces: ApS(personLenses.Name.Set, maybeName)
RO.ApSL(personLenses.Age, maybeAge),
)
```
This exact pattern is used in [`samples/builder`](samples/builder/builder.go) to validate and construct a `Person` from an unvalidated `PartialPerson`.
### Lifted Variants for Mixed Monads
`context/readerioresult` provides `Bind*K` helpers that lift simpler computations directly into the do-chain:
| Helper | Lifts |
|--------|-------|
| `BindResultK` / `BindEitherK` | `func(S1) (T, error)` — pure result |
| `BindIOResultK` / `BindIOEitherK` | `func(S1) func() (T, error)` — lazy IO result |
| `BindIOK` | `func(S1) func() T` — infallible IO |
| `BindReaderK` | `func(S1) func(ctx) T` — context reader |
```go
RIO.BindResultK(setUser, func(s Pipeline) (User, error) {
return validateAndBuild(s) // plain (value, error) function, no wrapping needed
})
```
### Decision Guide
```
Does the new step need to read prior accumulated state?
YES → Bind (monadic, sequential; f receives current S)
NO → ApS (applicative, independent; fa is a fixed M[T])
Is the new value derived purely from state, with no monad?
YES → Let (pure function of S)
Is the new value a compile-time or runtime constant?
YES → LetTo
Starting from an existing M[T] rather than an empty struct?
YES → BindTo
```
### Complete Example — `result` Monad
The same pattern works with simpler monads. Here with `result.Result[A]`:
`Eitherize1` converts any standard `func(A) (B, error)` into `func(A) Result[B]`. Define these lifted functions once as variables. Then use lenses to focus on individual struct fields and compose with `F.Flow2(lens.Get, f)` — no inline lambdas, no manual error handling.
```go
import (
R "github.com/IBM/fp-go/v2/result"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
N "github.com/IBM/fp-go/v2/number"
"strconv"
)
type Parsed struct {
Raw string
Number int
Double int
}
// Lenses — focus on individual fields of Parsed.
var (
rawLens = L.MakeLens(
func(s Parsed) string { return s.Raw },
func(s Parsed, v string) Parsed { s.Raw = v; return s },
)
numberLens = L.MakeLens(
func(s Parsed) int { return s.Number },
func(s Parsed, v int) Parsed { s.Number = v; return s },
)
doubleLens = L.MakeLens(
func(s Parsed) int { return s.Double },
func(s Parsed, v int) Parsed { s.Double = v; return s },
)
)
// Lifted functions — convert standard (value, error) functions into Result-returning ones.
var (
atoi = R.Eitherize1(strconv.Atoi) // func(string) Result[int]
)
parse := func(input string) R.Result[Parsed] {
return F.Pipe3(
R.Do(Parsed{}),
R.LetTo(rawLens.Set, input), // set Raw to constant input
R.Bind(numberLens.Set, F.Flow2(rawLens.Get, atoi)), // get Raw, parse → Result[int]
R.Let(doubleLens.Set, F.Flow2(numberLens.Get, N.Mul(2))), // get Number, multiply → int
)
}
parse("21") // Ok(Parsed{Raw:"21", Number:21, Double:42})
parse("abc") // Error(strconv parse error)
```
`rawLens.Set` is already `func(string) func(Parsed) Parsed`, matching the setter signature `Bind` and `LetTo` expect — no manual setter functions to write. `F.Flow2(rawLens.Get, atoi)` composes the field getter with the eitherized parse function into a `Kleisli[Parsed, int]` without any intermediate lambda.
## Import Paths
```go
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/array"
)
```
Requires Go 1.24+ (generic type aliases).

238
v2/AGENTS.md Normal file
View File

@@ -0,0 +1,238 @@
# Agent Guidelines for fp-go/v2
This document provides guidelines for AI agents working on the fp-go/v2 project.
## Table of Contents
- [Documentation Standards](#documentation-standards)
- [Go Doc Comments](#go-doc-comments)
- [File Headers](#file-headers)
- [Testing Standards](#testing-standards)
- [Test Structure](#test-structure)
- [Test Coverage](#test-coverage)
- [Example Test Pattern](#example-test-pattern)
- [Code Style](#code-style)
- [Functional Patterns](#functional-patterns)
- [Error Handling](#error-handling)
- [Checklist for New Code](#checklist-for-new-code)
## Documentation Standards
### Go Doc Comments
1. **Use Standard Go Doc Format**
- Do NOT use markdown-style links like `[text](url)`
- Do NOT use markdown-style headers like `# Section` or `## Subsection`
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
- Go's documentation system will automatically create links
- Use plain text with blank lines to separate sections
2. **Structure**
```go
// FunctionName does something useful.
//
// Longer description explaining the purpose and behavior.
//
// Type Parameters:
// - T: Description of type parameter
//
// Parameters:
// - param: Description of parameter
//
// Returns:
// - ReturnType: Description of return value
//
// Example:
//
// code example here
//
// See Also:
// - RelatedFunction: Brief description
func FunctionName[T any](param T) ReturnType {
```
3. **Code Examples**
- Use idiomatic Go patterns
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
- Show realistic, runnable examples
- Indent code examples with spaces (not tabs) for proper godoc rendering
### File Headers
Always include the Apache 2.0 license header:
```go
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
```
## Testing Standards
### Test Structure
1. **Organize Tests by Category**
```go
func TestFunctionName_Success(t *testing.T) {
t.Run("specific success case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_Failure(t *testing.T) {
t.Run("specific failure case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_EdgeCases(t *testing.T) {
// edge case tests
}
func TestFunctionName_Integration(t *testing.T) {
// integration tests
}
```
2. **Use Direct Assertions**
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
- Avoid: Verbose `either.MonadFold` patterns unless necessary
- Exception: When you need to verify pointer is not nil or extract specific fields
3. **Use Idiomatic Patterns**
- Use `result.Eitherize1` for converting `(T, error)` functions
- Use `result.Of` for success values
- Use `result.Left` for error values
4. **Folding Either/Result Values in Tests**
- Use `F.Pipe1(result, Fold(onLeft, onRight))` — avoid the `_ = Fold(...)(result)` discard pattern
- Use `slices.Collect[T]` instead of a manual `for n := range seq { collected = append(...) }` loop
- Use `t.Fatal` in the unexpected branch to combine the `IsLeft`/`IsRight` check with value extraction:
```go
// Good: single fold combines assertion and extraction
collected := F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
// Avoid: separate IsRight check + manual loop
assert.True(t, IsRight(result))
var collected []int
_ = MonadFold(result,
func(e error) []int { return nil },
func(seq iter.Seq[int]) []int {
for n := range seq { collected = append(collected, n) }
return collected
},
)
```
- Use `F.Identity[error]` as the Left branch when extracting an error value:
```go
err := F.Pipe1(result, Fold(
F.Identity[error],
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
))
```
- Extract repeated fold patterns as local helper closures within the test function:
```go
collectInts := func(r Result[iter.Seq[int]]) []int {
return F.Pipe1(r, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
}
```
5. **Other Test Style Details**
- Use `for i := range 10` instead of `for i := 0; i < 10; i++`
- Chain curried calls directly: `TraverseSeq(parse)(input)` — no need for an intermediate `traverseFn` variable
- Use direct slice literals (`[]string{"a", "b"}`) rather than `A.From("a", "b")` in tests
### Test Coverage
Include tests for:
- **Success cases**: Normal operation with various input types
- **Failure cases**: Error handling and error preservation
- **Edge cases**: Nil, empty, zero values, boundary conditions
- **Integration**: Composition with other functions
- **Type safety**: Verify type parameters work correctly
- **Benchmarks**: Performance-critical paths
### Example Test Pattern
```go
func TestFromReaderResult_Success(t *testing.T) {
t.Run("converts successful ReaderResult", func(t *testing.T) {
// Arrange
parseIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult[string, int](parseIntRR)
// Act
result := validator("42")(nil)
// Assert
assert.Equal(t, validation.Success(42), result)
})
}
```
## Code Style
### Functional Patterns
1. **Prefer Composition**
```go
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
Chain(validatePositive),
)
```
2. **Use Type-Safe Helpers**
- `result.Eitherize1` for `func(T) (R, error)`
- `result.Of` for wrapping success values
- `result.Left` for wrapping errors
3. **Avoid Verbose Patterns**
- Don't manually handle `(value, error)` tuples when helpers exist
- Don't use `either.MonadFold` in tests unless necessary
4. **Use Void Type for Unit Values**
- Use `function.Void` (or `F.Void`) instead of `struct{}`
- Use `function.VOID` (or `F.VOID`) instead of `struct{}{}`
- Example: `Empty[F.Void, F.Void, any](lazy.Of(pair.MakePair(F.VOID, F.VOID)))`
### Error Handling
1. **In Production Code**
- Use `validation.Success` for successful validations
- Use `validation.FailureWithMessage` for simple failures
- Use `validation.FailureWithError` to preserve error causes
2. **In Tests**
- Verify error messages and causes
- Check error context is preserved
- Test error accumulation when applicable
## Checklist for New Code
- [ ] Apache 2.0 license header included
- [ ] Go doc comments use standard format (no markdown links)
- [ ] Code examples are idiomatic and concise
- [ ] Tests cover success, failure, edge cases, and integration
- [ ] Tests use direct assertions where possible
- [ ] Benchmarks included for performance-critical code
- [ ] All tests pass
- [ ] Code uses functional composition patterns
- [ ] Error handling preserves context and causes

View File

@@ -584,5 +584,7 @@ func process(input string) types.Result[types.Option[int]] {
For more information, see:
- [README.md](./README.md) - Overview and quick start
- [FUNCTIONAL_IO.md](./FUNCTIONAL_IO.md) - Functional I/O patterns with Context and Reader
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison between standard and idiomatic packages
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
- [Samples](./samples/) - Practical examples

1036
v2/FUNCTIONAL_IO.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go/v2.svg)](https://pkg.go.dev/github.com/IBM/fp-go/v2)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main&flag=v2)](https://coveralls.io/github/IBM/fp-go?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/IBM/fp-go/v2)](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
[![Context7](https://img.shields.io/badge/context7-docs-blue)](https://context7.com/ibm/fp-go)
**fp-go** is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by [fp-ts](https://gcanti.github.io/fp-ts/) to the Go ecosystem. Version 2 leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
@@ -447,6 +448,8 @@ func process() IOResult[string] {
## 📚 Documentation
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
- **[Functional I/O in Go](./FUNCTIONAL_IO.md)** - Understanding Context, errors, and the Reader pattern for I/O operations
- **[Idiomatic vs Standard Packages](./IDIOMATIC_COMPARISON.md)** - Performance comparison and when to use each approach
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
@@ -458,12 +461,16 @@ func process() IOResult[string] {
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **IOOption** - Combine IO with Option for optional values with side effects
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither when using standard `error` type)
- **Effect** - Composable effects with dependency injection and error handling
- **Reader** - Dependency injection pattern
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples

File diff suppressed because it is too large Load Diff

View File

@@ -16,308 +16,88 @@
package array
import (
"fmt"
"testing"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestReplicate(t *testing.T) {
result := Replicate(3, "a")
assert.Equal(t, []string{"a", "a", "a"}, result)
empty := Replicate(0, 42)
assert.Equal(t, []int{}, empty)
}
func TestMonadMap(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMap(src, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMonadMapRef(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMapRef(src, func(x *int) int { return *x * 2 })
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
mapper := MapWithIndex(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := mapper(src)
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
}
func TestMapRef(t *testing.T) {
src := []int{1, 2, 3}
mapper := MapRef(func(x *int) int { return *x * 2 })
result := mapper(src)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestFilterWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterWithIndex(func(i, x int) bool {
return i%2 == 0 && x > 2
})
result := filter(src)
assert.Equal(t, []int{3, 5}, result)
}
func TestFilterRef(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterRef(func(x *int) bool { return *x > 2 })
result := filter(src)
assert.Equal(t, []int{3, 4, 5}, result)
}
func TestMonadFilterMap(t *testing.T) {
src := []int{1, 2, 3, 4}
result := MonadFilterMap(src, func(x int) O.Option[string] {
if x%2 == 0 {
return O.Some(fmt.Sprintf("even:%d", x))
}
return O.None[string]()
})
assert.Equal(t, []string{"even:2", "even:4"}, result)
}
func TestMonadFilterMapWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4}
result := MonadFilterMapWithIndex(src, func(i, x int) O.Option[string] {
if i%2 == 0 {
return O.Some(fmt.Sprintf("%d:%d", i, x))
}
return O.None[string]()
})
assert.Equal(t, []string{"0:1", "2:3"}, result)
}
func TestFilterMapWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4}
filter := FilterMapWithIndex(func(i, x int) O.Option[string] {
if i%2 == 0 {
return O.Some(fmt.Sprintf("%d:%d", i, x))
}
return O.None[string]()
})
result := filter(src)
assert.Equal(t, []string{"0:1", "2:3"}, result)
}
func TestFilterMapRef(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterMapRef(
func(x *int) bool { return *x > 2 },
func(x *int) string { return fmt.Sprintf("val:%d", *x) },
)
result := filter(src)
assert.Equal(t, []string{"val:3", "val:4", "val:5"}, result)
}
func TestReduceWithIndex(t *testing.T) {
src := []int{1, 2, 3}
reducer := ReduceWithIndex(func(i, acc, x int) int {
return acc + i + x
// TestMonadReduceWithIndex tests the MonadReduceWithIndex function
func TestMonadReduceWithIndex(t *testing.T) {
// Test with integers - sum with index multiplication
numbers := []int{1, 2, 3, 4, 5}
result := MonadReduceWithIndex(numbers, func(idx, acc, val int) int {
return acc + (val * idx)
}, 0)
result := reducer(src)
assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9
}
// Expected: 0*1 + 1*2 + 2*3 + 3*4 + 4*5 = 0 + 2 + 6 + 12 + 20 = 40
assert.Equal(t, 40, result)
func TestReduceRightWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
reducer := ReduceRightWithIndex(func(i int, x, acc string) string {
return fmt.Sprintf("%s%d:%s", acc, i, x)
// Test with empty array
empty := []int{}
result2 := MonadReduceWithIndex(empty, func(idx, acc, val int) int {
return acc + val
}, 10)
assert.Equal(t, 10, result2)
// Test with strings - concatenate with index
words := []string{"a", "b", "c"}
result3 := MonadReduceWithIndex(words, func(idx int, acc, val string) string {
return acc + val + string(rune('0'+idx))
}, "")
result := reducer(src)
assert.Equal(t, "2:c1:b0:a", result)
assert.Equal(t, "a0b1c2", result3)
}
func TestReduceRef(t *testing.T) {
src := []int{1, 2, 3}
reducer := ReduceRef(func(acc int, x *int) int {
return acc + *x
}, 0)
result := reducer(src)
assert.Equal(t, 6, result)
}
func TestZero(t *testing.T) {
result := Zero[int]()
assert.Equal(t, []int{}, result)
assert.True(t, IsEmpty(result))
}
func TestMonadChain(t *testing.T) {
src := []int{1, 2, 3}
result := MonadChain(src, func(x int) []int {
return []int{x, x * 10}
})
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
}
func TestChain(t *testing.T) {
src := []int{1, 2, 3}
chain := Chain(func(x int) []int {
return []int{x, x * 10}
})
result := chain(src)
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
}
func TestMonadAp(t *testing.T) {
fns := []func(int) int{
N.Mul(2),
N.Add(10),
}
values := []int{1, 2}
result := MonadAp(fns, values)
assert.Equal(t, []int{2, 4, 11, 12}, result)
}
func TestMatchLeft(t *testing.T) {
matcher := MatchLeft(
func() string { return "empty" },
func(head int, tail []int) string {
return fmt.Sprintf("head:%d,tail:%v", head, tail)
},
)
assert.Equal(t, "empty", matcher([]int{}))
assert.Equal(t, "head:1,tail:[2 3]", matcher([]int{1, 2, 3}))
}
func TestTail(t *testing.T) {
assert.Equal(t, O.None[[]int](), Tail([]int{}))
assert.Equal(t, O.Some([]int{2, 3}), Tail([]int{1, 2, 3}))
assert.Equal(t, O.Some([]int{}), Tail([]int{1}))
}
func TestFirst(t *testing.T) {
assert.Equal(t, O.None[int](), First([]int{}))
assert.Equal(t, O.Some(1), First([]int{1, 2, 3}))
}
func TestLast(t *testing.T) {
assert.Equal(t, O.None[int](), Last([]int{}))
assert.Equal(t, O.Some(3), Last([]int{1, 2, 3}))
assert.Equal(t, O.Some(1), Last([]int{1}))
}
func TestUpsertAt(t *testing.T) {
src := []int{1, 2, 3}
upsert := UpsertAt(99)
result1 := upsert(src)
assert.Equal(t, []int{1, 2, 3, 99}, result1)
}
func TestSize(t *testing.T) {
assert.Equal(t, 0, Size([]int{}))
assert.Equal(t, 3, Size([]int{1, 2, 3}))
}
func TestMonadPartition(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
result := MonadPartition(src, func(x int) bool { return x > 2 })
assert.Equal(t, []int{1, 2}, result.F1)
assert.Equal(t, []int{3, 4, 5}, result.F2)
}
func TestIsNil(t *testing.T) {
var nilSlice []int
assert.True(t, IsNil(nilSlice))
assert.False(t, IsNil([]int{}))
assert.False(t, IsNil([]int{1}))
}
func TestIsNonNil(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonNil(nilSlice))
assert.True(t, IsNonNil([]int{}))
assert.True(t, IsNonNil([]int{1}))
}
func TestConstNil(t *testing.T) {
result := ConstNil[int]()
assert.True(t, IsNil(result))
}
func TestSliceRight(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
slicer := SliceRight[int](2)
result := slicer(src)
assert.Equal(t, []int{3, 4, 5}, result)
}
func TestCopy(t *testing.T) {
src := []int{1, 2, 3}
copied := Copy(src)
assert.Equal(t, src, copied)
// Verify it's a different slice
copied[0] = 99
assert.Equal(t, 1, src[0])
assert.Equal(t, 99, copied[0])
}
func TestClone(t *testing.T) {
src := []int{1, 2, 3}
cloner := Clone(N.Mul(2))
result := cloner(src)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestFoldMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := folder(src)
assert.Equal(t, "0:a1:b2:c", result)
}
func TestFold(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
folder := Fold(N.MonoidSum[int]())
result := folder(src)
assert.Equal(t, 15, result)
}
func TestPush(t *testing.T) {
src := []int{1, 2, 3}
pusher := Push(4)
result := pusher(src)
// TestAppend tests the Append function
func TestAppend(t *testing.T) {
// Test appending to non-empty array
arr := []int{1, 2, 3}
result := Append(arr, 4)
assert.Equal(t, []int{1, 2, 3, 4}, result)
// Verify original array is unchanged
assert.Equal(t, []int{1, 2, 3}, arr)
// Test appending to empty array
empty := []int{}
result2 := Append(empty, 1)
assert.Equal(t, []int{1}, result2)
// Test appending strings
words := []string{"hello", "world"}
result3 := Append(words, "!")
assert.Equal(t, []string{"hello", "world", "!"}, result3)
// Test appending to nil array
var nilArr []int
result4 := Append(nilArr, 42)
assert.Equal(t, []int{42}, result4)
}
func TestMonadFlap(t *testing.T) {
fns := []func(int) string{
func(x int) string { return fmt.Sprintf("a%d", x) },
func(x int) string { return fmt.Sprintf("b%d", x) },
}
result := MonadFlap(fns, 5)
assert.Equal(t, []string{"a5", "b5"}, result)
}
// TestStrictEquals tests the StrictEquals function
func TestStrictEquals(t *testing.T) {
eq := StrictEquals[int]()
func TestFlap(t *testing.T) {
fns := []func(int) string{
func(x int) string { return fmt.Sprintf("a%d", x) },
func(x int) string { return fmt.Sprintf("b%d", x) },
}
flapper := Flap[string](5)
result := flapper(fns)
assert.Equal(t, []string{"a5", "b5"}, result)
}
// Test equal arrays
arr1 := []int{1, 2, 3}
arr2 := []int{1, 2, 3}
assert.True(t, eq.Equals(arr1, arr2))
func TestPrepend(t *testing.T) {
src := []int{2, 3, 4}
prepender := Prepend(1)
result := prepender(src)
assert.Equal(t, []int{1, 2, 3, 4}, result)
// Test different arrays
arr3 := []int{1, 2, 4}
assert.False(t, eq.Equals(arr1, arr3))
// Test different lengths
arr4 := []int{1, 2}
assert.False(t, eq.Equals(arr1, arr4))
// Test empty arrays
empty1 := []int{}
empty2 := []int{}
assert.True(t, eq.Equals(empty1, empty2))
// Test with strings
strEq := StrictEquals[string]()
words1 := []string{"hello", "world"}
words2 := []string{"hello", "world"}
words3 := []string{"hello", "there"}
assert.True(t, strEq.Equals(words1, words2))
assert.False(t, strEq.Equals(words1, words3))
}

522
v2/array/array_nil_test.go Normal file
View File

@@ -0,0 +1,522 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
"fmt"
"testing"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestNilSlice_IsEmpty verifies that IsEmpty handles nil slices correctly
func TestNilSlice_IsEmpty(t *testing.T) {
var nilSlice []int
assert.True(t, IsEmpty(nilSlice), "nil slice should be empty")
}
// TestNilSlice_IsNonEmpty verifies that IsNonEmpty handles nil slices correctly
func TestNilSlice_IsNonEmpty(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonEmpty(nilSlice), "nil slice should not be non-empty")
}
// TestNilSlice_MonadMap verifies that MonadMap handles nil slices correctly
func TestNilSlice_MonadMap(t *testing.T) {
var nilSlice []int
result := MonadMap(nilSlice, func(v int) string {
return fmt.Sprintf("%d", v)
})
assert.NotNil(t, result, "MonadMap should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadMap should return empty slice for nil input")
}
// TestNilSlice_MonadMapRef verifies that MonadMapRef handles nil slices correctly
func TestNilSlice_MonadMapRef(t *testing.T) {
var nilSlice []int
result := MonadMapRef(nilSlice, func(v *int) string {
return fmt.Sprintf("%d", *v)
})
assert.NotNil(t, result, "MonadMapRef should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadMapRef should return empty slice for nil input")
}
// TestNilSlice_Map verifies that Map handles nil slices correctly
func TestNilSlice_Map(t *testing.T) {
var nilSlice []int
mapper := Map(func(v int) string {
return fmt.Sprintf("%d", v)
})
result := mapper(nilSlice)
assert.NotNil(t, result, "Map should return non-nil slice")
assert.Equal(t, 0, len(result), "Map should return empty slice for nil input")
}
// TestNilSlice_MapRef verifies that MapRef handles nil slices correctly
func TestNilSlice_MapRef(t *testing.T) {
var nilSlice []int
mapper := MapRef(func(v *int) string {
return fmt.Sprintf("%d", *v)
})
result := mapper(nilSlice)
assert.NotNil(t, result, "MapRef should return non-nil slice")
assert.Equal(t, 0, len(result), "MapRef should return empty slice for nil input")
}
// TestNilSlice_MapWithIndex verifies that MapWithIndex handles nil slices correctly
func TestNilSlice_MapWithIndex(t *testing.T) {
var nilSlice []int
mapper := MapWithIndex(func(i int, v int) string {
return fmt.Sprintf("%d:%d", i, v)
})
result := mapper(nilSlice)
assert.NotNil(t, result, "MapWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "MapWithIndex should return empty slice for nil input")
}
// TestNilSlice_Filter verifies that Filter handles nil slices correctly
func TestNilSlice_Filter(t *testing.T) {
var nilSlice []int
filter := Filter(func(v int) bool {
return v > 0
})
result := filter(nilSlice)
assert.NotNil(t, result, "Filter should return non-nil slice")
assert.Equal(t, 0, len(result), "Filter should return empty slice for nil input")
}
// TestNilSlice_FilterWithIndex verifies that FilterWithIndex handles nil slices correctly
func TestNilSlice_FilterWithIndex(t *testing.T) {
var nilSlice []int
filter := FilterWithIndex(func(i int, v int) bool {
return v > 0
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterWithIndex should return empty slice for nil input")
}
// TestNilSlice_FilterRef verifies that FilterRef handles nil slices correctly
func TestNilSlice_FilterRef(t *testing.T) {
var nilSlice []int
filter := FilterRef(func(v *int) bool {
return *v > 0
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterRef should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterRef should return empty slice for nil input")
}
// TestNilSlice_MonadFilterMap verifies that MonadFilterMap handles nil slices correctly
func TestNilSlice_MonadFilterMap(t *testing.T) {
var nilSlice []int
result := MonadFilterMap(nilSlice, func(v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d", v))
})
assert.NotNil(t, result, "MonadFilterMap should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadFilterMap should return empty slice for nil input")
}
// TestNilSlice_MonadFilterMapWithIndex verifies that MonadFilterMapWithIndex handles nil slices correctly
func TestNilSlice_MonadFilterMapWithIndex(t *testing.T) {
var nilSlice []int
result := MonadFilterMapWithIndex(nilSlice, func(i int, v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d:%d", i, v))
})
assert.NotNil(t, result, "MonadFilterMapWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadFilterMapWithIndex should return empty slice for nil input")
}
// TestNilSlice_FilterMap verifies that FilterMap handles nil slices correctly
func TestNilSlice_FilterMap(t *testing.T) {
var nilSlice []int
filter := FilterMap(func(v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d", v))
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterMap should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterMap should return empty slice for nil input")
}
// TestNilSlice_FilterMapWithIndex verifies that FilterMapWithIndex handles nil slices correctly
func TestNilSlice_FilterMapWithIndex(t *testing.T) {
var nilSlice []int
filter := FilterMapWithIndex(func(i int, v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d:%d", i, v))
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterMapWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterMapWithIndex should return empty slice for nil input")
}
// TestNilSlice_MonadReduce verifies that MonadReduce handles nil slices correctly
func TestNilSlice_MonadReduce(t *testing.T) {
var nilSlice []int
result := MonadReduce(nilSlice, func(acc int, v int) int {
return acc + v
}, 10)
assert.Equal(t, 10, result, "MonadReduce should return initial value for nil slice")
}
// TestNilSlice_MonadReduceWithIndex verifies that MonadReduceWithIndex handles nil slices correctly
func TestNilSlice_MonadReduceWithIndex(t *testing.T) {
var nilSlice []int
result := MonadReduceWithIndex(nilSlice, func(i int, acc int, v int) int {
return acc + v
}, 10)
assert.Equal(t, 10, result, "MonadReduceWithIndex should return initial value for nil slice")
}
// TestNilSlice_Reduce verifies that Reduce handles nil slices correctly
func TestNilSlice_Reduce(t *testing.T) {
var nilSlice []int
reducer := Reduce(func(acc int, v int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "Reduce should return initial value for nil slice")
}
// TestNilSlice_ReduceWithIndex verifies that ReduceWithIndex handles nil slices correctly
func TestNilSlice_ReduceWithIndex(t *testing.T) {
var nilSlice []int
reducer := ReduceWithIndex(func(i int, acc int, v int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceWithIndex should return initial value for nil slice")
}
// TestNilSlice_ReduceRight verifies that ReduceRight handles nil slices correctly
func TestNilSlice_ReduceRight(t *testing.T) {
var nilSlice []int
reducer := ReduceRight(func(v int, acc int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceRight should return initial value for nil slice")
}
// TestNilSlice_ReduceRightWithIndex verifies that ReduceRightWithIndex handles nil slices correctly
func TestNilSlice_ReduceRightWithIndex(t *testing.T) {
var nilSlice []int
reducer := ReduceRightWithIndex(func(i int, v int, acc int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceRightWithIndex should return initial value for nil slice")
}
// TestNilSlice_ReduceRef verifies that ReduceRef handles nil slices correctly
func TestNilSlice_ReduceRef(t *testing.T) {
var nilSlice []int
reducer := ReduceRef(func(acc int, v *int) int {
return acc + *v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceRef should return initial value for nil slice")
}
// TestNilSlice_Append verifies that Append handles nil slices correctly
func TestNilSlice_Append(t *testing.T) {
var nilSlice []int
result := Append(nilSlice, 42)
assert.NotNil(t, result, "Append should return non-nil slice")
assert.Equal(t, 1, len(result), "Append should create slice with one element")
assert.Equal(t, 42, result[0], "Append should add element correctly")
}
// TestNilSlice_MonadChain verifies that MonadChain handles nil slices correctly
func TestNilSlice_MonadChain(t *testing.T) {
var nilSlice []int
result := MonadChain(nilSlice, func(v int) []string {
return []string{fmt.Sprintf("%d", v)}
})
assert.NotNil(t, result, "MonadChain should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadChain should return empty slice for nil input")
}
// TestNilSlice_Chain verifies that Chain handles nil slices correctly
func TestNilSlice_Chain(t *testing.T) {
var nilSlice []int
chain := Chain(func(v int) []string {
return []string{fmt.Sprintf("%d", v)}
})
result := chain(nilSlice)
assert.NotNil(t, result, "Chain should return non-nil slice")
assert.Equal(t, 0, len(result), "Chain should return empty slice for nil input")
}
// TestNilSlice_MonadAp verifies that MonadAp handles nil slices correctly
func TestNilSlice_MonadAp(t *testing.T) {
var nilFuncs []func(int) string
var nilValues []int
// nil functions, nil values
result1 := MonadAp(nilFuncs, nilValues)
assert.NotNil(t, result1, "MonadAp should return non-nil slice")
assert.Equal(t, 0, len(result1), "MonadAp should return empty slice for nil inputs")
// nil functions, non-nil values
nonNilValues := []int{1, 2, 3}
result2 := MonadAp(nilFuncs, nonNilValues)
assert.NotNil(t, result2, "MonadAp should return non-nil slice")
assert.Equal(t, 0, len(result2), "MonadAp should return empty slice when functions are nil")
// non-nil functions, nil values
nonNilFuncs := []func(int) string{func(v int) string { return fmt.Sprintf("%d", v) }}
result3 := MonadAp(nonNilFuncs, nilValues)
assert.NotNil(t, result3, "MonadAp should return non-nil slice")
assert.Equal(t, 0, len(result3), "MonadAp should return empty slice when values are nil")
}
// TestNilSlice_Ap verifies that Ap handles nil slices correctly
func TestNilSlice_Ap(t *testing.T) {
var nilValues []int
ap := Ap[string](nilValues)
var nilFuncs []func(int) string
result := ap(nilFuncs)
assert.NotNil(t, result, "Ap should return non-nil slice")
assert.Equal(t, 0, len(result), "Ap should return empty slice for nil inputs")
}
// TestNilSlice_Head verifies that Head handles nil slices correctly
func TestNilSlice_Head(t *testing.T) {
var nilSlice []int
result := Head(nilSlice)
assert.True(t, O.IsNone(result), "Head should return None for nil slice")
}
// TestNilSlice_First verifies that First handles nil slices correctly
func TestNilSlice_First(t *testing.T) {
var nilSlice []int
result := First(nilSlice)
assert.True(t, O.IsNone(result), "First should return None for nil slice")
}
// TestNilSlice_Last verifies that Last handles nil slices correctly
func TestNilSlice_Last(t *testing.T) {
var nilSlice []int
result := Last(nilSlice)
assert.True(t, O.IsNone(result), "Last should return None for nil slice")
}
// TestNilSlice_Tail verifies that Tail handles nil slices correctly
func TestNilSlice_Tail(t *testing.T) {
var nilSlice []int
result := Tail(nilSlice)
assert.True(t, O.IsNone(result), "Tail should return None for nil slice")
}
// TestNilSlice_Flatten verifies that Flatten handles nil slices correctly
func TestNilSlice_Flatten(t *testing.T) {
var nilSlice [][]int
result := Flatten(nilSlice)
assert.NotNil(t, result, "Flatten should return non-nil slice")
assert.Equal(t, 0, len(result), "Flatten should return empty slice for nil input")
}
// TestNilSlice_Lookup verifies that Lookup handles nil slices correctly
func TestNilSlice_Lookup(t *testing.T) {
var nilSlice []int
lookup := Lookup[int](0)
result := lookup(nilSlice)
assert.True(t, O.IsNone(result), "Lookup should return None for nil slice")
}
// TestNilSlice_Size verifies that Size handles nil slices correctly
func TestNilSlice_Size(t *testing.T) {
var nilSlice []int
result := Size(nilSlice)
assert.Equal(t, 0, result, "Size should return 0 for nil slice")
}
// TestNilSlice_MonadPartition verifies that MonadPartition handles nil slices correctly
func TestNilSlice_MonadPartition(t *testing.T) {
var nilSlice []int
result := MonadPartition(nilSlice, func(v int) bool {
return v > 0
})
left := P.Head(result)
right := P.Tail(result)
assert.NotNil(t, left, "MonadPartition left should return non-nil slice")
assert.NotNil(t, right, "MonadPartition right should return non-nil slice")
assert.Equal(t, 0, len(left), "MonadPartition left should be empty for nil input")
assert.Equal(t, 0, len(right), "MonadPartition right should be empty for nil input")
}
// TestNilSlice_Partition verifies that Partition handles nil slices correctly
func TestNilSlice_Partition(t *testing.T) {
var nilSlice []int
partition := Partition(func(v int) bool {
return v > 0
})
result := partition(nilSlice)
left := P.Head(result)
right := P.Tail(result)
assert.NotNil(t, left, "Partition left should return non-nil slice")
assert.NotNil(t, right, "Partition right should return non-nil slice")
assert.Equal(t, 0, len(left), "Partition left should be empty for nil input")
assert.Equal(t, 0, len(right), "Partition right should be empty for nil input")
}
// TestNilSlice_IsNil verifies that IsNil handles nil slices correctly
func TestNilSlice_IsNil(t *testing.T) {
var nilSlice []int
assert.True(t, IsNil(nilSlice), "IsNil should return true for nil slice")
nonNilSlice := []int{}
assert.False(t, IsNil(nonNilSlice), "IsNil should return false for non-nil empty slice")
}
// TestNilSlice_IsNonNil verifies that IsNonNil handles nil slices correctly
func TestNilSlice_IsNonNil(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonNil(nilSlice), "IsNonNil should return false for nil slice")
nonNilSlice := []int{}
assert.True(t, IsNonNil(nonNilSlice), "IsNonNil should return true for non-nil empty slice")
}
// TestNilSlice_Copy verifies that Copy handles nil slices correctly
func TestNilSlice_Copy(t *testing.T) {
var nilSlice []int
result := Copy(nilSlice)
assert.NotNil(t, result, "Copy should return non-nil slice")
assert.Equal(t, 0, len(result), "Copy should return empty slice for nil input")
}
// TestNilSlice_FoldMap verifies that FoldMap handles nil slices correctly
func TestNilSlice_FoldMap(t *testing.T) {
var nilSlice []int
monoid := S.Monoid
foldMap := FoldMap[int](monoid)(func(v int) string {
return fmt.Sprintf("%d", v)
})
result := foldMap(nilSlice)
assert.Equal(t, "", result, "FoldMap should return empty value for nil slice")
}
// TestNilSlice_FoldMapWithIndex verifies that FoldMapWithIndex handles nil slices correctly
func TestNilSlice_FoldMapWithIndex(t *testing.T) {
var nilSlice []int
monoid := S.Monoid
foldMap := FoldMapWithIndex[int](monoid)(func(i int, v int) string {
return fmt.Sprintf("%d:%d", i, v)
})
result := foldMap(nilSlice)
assert.Equal(t, "", result, "FoldMapWithIndex should return empty value for nil slice")
}
// TestNilSlice_Fold verifies that Fold handles nil slices correctly
func TestNilSlice_Fold(t *testing.T) {
var nilSlice []string
monoid := S.Monoid
fold := Fold[string](monoid)
result := fold(nilSlice)
assert.Equal(t, "", result, "Fold should return empty value for nil slice")
}
// TestNilSlice_Concat verifies that Concat handles nil slices correctly
func TestNilSlice_Concat(t *testing.T) {
var nilSlice []int
nonNilSlice := []int{1, 2, 3}
// nil concat non-nil
concat1 := Concat(nonNilSlice)
result1 := concat1(nilSlice)
assert.Equal(t, nonNilSlice, result1, "nil concat non-nil should return non-nil slice")
// non-nil concat nil
concat2 := Concat(nilSlice)
result2 := concat2(nonNilSlice)
assert.Equal(t, nonNilSlice, result2, "non-nil concat nil should return non-nil slice")
// nil concat nil
concat3 := Concat(nilSlice)
result3 := concat3(nilSlice)
assert.Nil(t, result3, "nil concat nil should return nil")
}
// TestNilSlice_MonadFlap verifies that MonadFlap handles nil slices correctly
func TestNilSlice_MonadFlap(t *testing.T) {
var nilSlice []func(int) string
result := MonadFlap(nilSlice, 42)
assert.NotNil(t, result, "MonadFlap should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadFlap should return empty slice for nil input")
}
// TestNilSlice_Flap verifies that Flap handles nil slices correctly
func TestNilSlice_Flap(t *testing.T) {
var nilSlice []func(int) string
flap := Flap[string, int](42)
result := flap(nilSlice)
assert.NotNil(t, result, "Flap should return non-nil slice")
assert.Equal(t, 0, len(result), "Flap should return empty slice for nil input")
}
// TestNilSlice_Reverse verifies that Reverse handles nil slices correctly
func TestNilSlice_Reverse(t *testing.T) {
var nilSlice []int
result := Reverse(nilSlice)
assert.Nil(t, result, "Reverse should return nil for nil slice")
}
// TestNilSlice_Extend verifies that Extend handles nil slices correctly
func TestNilSlice_Extend(t *testing.T) {
var nilSlice []int
extend := Extend(func(as []int) string {
return fmt.Sprintf("%v", as)
})
result := extend(nilSlice)
assert.NotNil(t, result, "Extend should return non-nil slice")
assert.Equal(t, 0, len(result), "Extend should return empty slice for nil input")
}
// TestNilSlice_Empty verifies that Empty creates an empty non-nil slice
func TestNilSlice_Empty(t *testing.T) {
result := Empty[int]()
assert.NotNil(t, result, "Empty should return non-nil slice")
assert.Equal(t, 0, len(result), "Empty should return empty slice")
assert.False(t, IsNil(result), "Empty should not return nil slice")
}
// TestNilSlice_Zero verifies that Zero creates an empty non-nil slice
func TestNilSlice_Zero(t *testing.T) {
result := Zero[int]()
assert.NotNil(t, result, "Zero should return non-nil slice")
assert.Equal(t, 0, len(result), "Zero should return empty slice")
assert.False(t, IsNil(result), "Zero should not return nil slice")
}
// TestNilSlice_ConstNil verifies that ConstNil returns a nil slice
func TestNilSlice_ConstNil(t *testing.T) {
result := ConstNil[int]()
assert.Nil(t, result, "ConstNil should return nil slice")
assert.True(t, IsNil(result), "ConstNil should return nil slice")
}
// TestNilSlice_Of verifies that Of creates a proper singleton slice
func TestNilSlice_Of(t *testing.T) {
result := Of(42)
assert.NotNil(t, result, "Of should return non-nil slice")
assert.Equal(t, 1, len(result), "Of should create slice with one element")
assert.Equal(t, 42, result[0], "Of should set value correctly")
}

View File

@@ -24,8 +24,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
return n > 2
}
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
}
func TestFilterChain(t *testing.T) {
func TestChainOptionK(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[[]string] {
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
return O.None[[]string]()
}
res := FilterChain(f)(src)
res := ChainOptionK(f)(src)
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
}
@@ -198,11 +198,228 @@ func TestFilterMap(t *testing.T) {
}
func TestFoldMap(t *testing.T) {
src := From("a", "b", "c")
t.Run("FoldMap with 0 items", func(t *testing.T) {
empty := []int{}
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(empty)
assert.Equal(t, 0, result, "FoldMap should return monoid empty for 0 items")
})
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
t.Run("FoldMap with 1 item", func(t *testing.T) {
single := From(5)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(single)
assert.Equal(t, 10, result, "FoldMap should map and return single item")
})
assert.Equal(t, "ABC", fold(src))
t.Run("FoldMap with 2 items", func(t *testing.T) {
two := From(3, 4)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(two)
assert.Equal(t, 14, result, "FoldMap should map and fold 2 items: (3*2) + (4*2) = 14")
})
t.Run("FoldMap with many items", func(t *testing.T) {
many := From(1, 2, 3, 4, 5)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(many)
assert.Equal(t, 30, result, "FoldMap should map and fold many items: (1*2) + (2*2) + (3*2) + (4*2) + (5*2) = 30")
})
t.Run("FoldMap with string concatenation - 0 items", func(t *testing.T) {
empty := []string{}
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(empty)
assert.Equal(t, "", result, "FoldMap should return empty string for 0 items")
})
t.Run("FoldMap with string concatenation - 1 item", func(t *testing.T) {
single := From("a")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(single)
assert.Equal(t, "A", result, "FoldMap should map single string")
})
t.Run("FoldMap with string concatenation - 2 items", func(t *testing.T) {
two := From("a", "b")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(two)
assert.Equal(t, "AB", result, "FoldMap should map and concatenate 2 strings")
})
t.Run("FoldMap with string concatenation - many items", func(t *testing.T) {
many := From("a", "b", "c", "d", "e")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(many)
assert.Equal(t, "ABCDE", result, "FoldMap should map and concatenate many strings")
})
}
func TestFold(t *testing.T) {
t.Run("Fold with 0 items", func(t *testing.T) {
empty := []int{}
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(empty)
assert.Equal(t, 0, result, "Fold should return monoid empty for 0 items")
})
t.Run("Fold with 1 item", func(t *testing.T) {
single := From(42)
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(single)
assert.Equal(t, 42, result, "Fold should return single item")
})
t.Run("Fold with 2 items", func(t *testing.T) {
two := From(10, 20)
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(two)
assert.Equal(t, 30, result, "Fold should combine 2 items: 10 + 20 = 30")
})
t.Run("Fold with many items", func(t *testing.T) {
many := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(many)
assert.Equal(t, 55, result, "Fold should combine many items: 1+2+3+4+5+6+7+8+9+10 = 55")
})
t.Run("Fold with string concatenation - 0 items", func(t *testing.T) {
empty := []string{}
fold := Fold[string](S.Monoid)
result := fold(empty)
assert.Equal(t, "", result, "Fold should return empty string for 0 items")
})
t.Run("Fold with string concatenation - 1 item", func(t *testing.T) {
single := From("hello")
fold := Fold[string](S.Monoid)
result := fold(single)
assert.Equal(t, "hello", result, "Fold should return single string")
})
t.Run("Fold with string concatenation - 2 items", func(t *testing.T) {
two := From("hello", "world")
fold := Fold[string](S.Monoid)
result := fold(two)
assert.Equal(t, "helloworld", result, "Fold should concatenate 2 strings")
})
t.Run("Fold with string concatenation - many items", func(t *testing.T) {
many := From("a", "b", "c", "d", "e", "f")
fold := Fold[string](S.Monoid)
result := fold(many)
assert.Equal(t, "abcdef", result, "Fold should concatenate many strings")
})
t.Run("Fold with product monoid - 0 items", func(t *testing.T) {
empty := []int{}
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(empty)
assert.Equal(t, 1, result, "Fold should return monoid empty (1) for product with 0 items")
})
t.Run("Fold with product monoid - 1 item", func(t *testing.T) {
single := From(7)
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(single)
assert.Equal(t, 7, result, "Fold should return single item for product")
})
t.Run("Fold with product monoid - 2 items", func(t *testing.T) {
two := From(3, 4)
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(two)
assert.Equal(t, 12, result, "Fold should multiply 2 items: 3 * 4 = 12")
})
t.Run("Fold with product monoid - many items", func(t *testing.T) {
many := From(2, 3, 4, 5)
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(many)
assert.Equal(t, 120, result, "Fold should multiply many items: 2*3*4*5 = 120")
})
}
func TestFoldMapWithIndex(t *testing.T) {
t.Run("FoldMapWithIndex with 0 items", func(t *testing.T) {
empty := []int{}
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
result := foldMap(empty)
assert.Equal(t, 0, result, "FoldMapWithIndex should return monoid empty for 0 items")
})
t.Run("FoldMapWithIndex with 1 item", func(t *testing.T) {
single := From(10)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
result := foldMap(single)
assert.Equal(t, 10, result, "FoldMapWithIndex should map with index: 0 + 10 = 10")
})
t.Run("FoldMapWithIndex with 2 items", func(t *testing.T) {
two := From(10, 20)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
result := foldMap(two)
assert.Equal(t, 31, result, "FoldMapWithIndex should map with indices: (0+10) + (1+20) = 31")
})
t.Run("FoldMapWithIndex with many items", func(t *testing.T) {
many := From(5, 10, 15, 20)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i * x })
result := foldMap(many)
assert.Equal(t, 100, result, "FoldMapWithIndex should map with indices: (0*5) + (1*10) + (2*15) + (3*20) = 100")
})
t.Run("FoldMapWithIndex with string concatenation - 0 items", func(t *testing.T) {
empty := []string{}
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := foldMap(empty)
assert.Equal(t, "", result, "FoldMapWithIndex should return empty string for 0 items")
})
t.Run("FoldMapWithIndex with string concatenation - 1 item", func(t *testing.T) {
single := From("a")
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := foldMap(single)
assert.Equal(t, "0:a", result, "FoldMapWithIndex should format single item with index")
})
t.Run("FoldMapWithIndex with string concatenation - 2 items", func(t *testing.T) {
two := From("a", "b")
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s,", i, s)
})
result := foldMap(two)
assert.Equal(t, "0:a,1:b,", result, "FoldMapWithIndex should format 2 items with indices")
})
t.Run("FoldMapWithIndex with string concatenation - many items", func(t *testing.T) {
many := From("a", "b", "c", "d")
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("[%d]%s", i, s)
})
result := foldMap(many)
assert.Equal(t, "[0]a[1]b[2]c[3]d", result, "FoldMapWithIndex should format many items with indices")
})
}
func ExampleFoldMap() {
@@ -767,6 +984,25 @@ func TestExtendUseCases(t *testing.T) {
// TestConcat tests the Concat function
func TestConcat(t *testing.T) {
t.Run("Semantic: Concat(b)(a) produces [a... b...]", func(t *testing.T) {
a := []int{1, 2, 3}
b := []int{4, 5, 6}
// Concat(b)(a) should produce [a... b...]
result := Concat(b)(a)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result, "Concat(b)(a) should produce [a... b...]")
// Verify order: a's elements come first, then b's elements
assert.Equal(t, a[0], result[0], "First element should be from a")
assert.Equal(t, a[1], result[1], "Second element should be from a")
assert.Equal(t, a[2], result[2], "Third element should be from a")
assert.Equal(t, b[0], result[3], "Fourth element should be from b")
assert.Equal(t, b[1], result[4], "Fifth element should be from b")
assert.Equal(t, b[2], result[5], "Sixth element should be from b")
})
t.Run("Concat two non-empty arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
@@ -870,6 +1106,54 @@ func TestConcat(t *testing.T) {
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Explicit append semantic demonstration", func(t *testing.T) {
// Given a base array
base := []string{"A", "B", "C"}
// And a suffix to append
suffix := []string{"D", "E", "F"}
// When we apply Concat(suffix) to base
appendSuffix := Concat(suffix)
result := appendSuffix(base)
// Then the result should be base followed by suffix
expected := []string{"A", "B", "C", "D", "E", "F"}
assert.Equal(t, expected, result)
// And the base should be unchanged
assert.Equal(t, []string{"A", "B", "C"}, base)
// And the suffix should be unchanged
assert.Equal(t, []string{"D", "E", "F"}, suffix)
})
t.Run("Append semantic with different types", func(t *testing.T) {
// Integers
intResult := Concat([]int{4, 5})([]int{1, 2, 3})
assert.Equal(t, []int{1, 2, 3, 4, 5}, intResult)
// Strings
strResult := Concat([]string{"world"})([]string{"hello"})
assert.Equal(t, []string{"hello", "world"}, strResult)
// Floats
floatResult := Concat([]float64{3.3, 4.4})([]float64{1.1, 2.2})
assert.Equal(t, []float64{1.1, 2.2, 3.3, 4.4}, floatResult)
})
t.Run("Append semantic in pipeline", func(t *testing.T) {
// Start with [1, 2, 3]
// Append [4, 5] to get [1, 2, 3, 4, 5]
// Append [6, 7] to get [1, 2, 3, 4, 5, 6, 7]
result := F.Pipe2(
[]int{1, 2, 3},
Concat([]int{4, 5}),
Concat([]int{6, 7}),
)
expected := []int{1, 2, 3, 4, 5, 6, 7}
assert.Equal(t, expected, result)
})
}
// TestConcatComposition tests Concat with other array operations

View File

@@ -63,17 +63,26 @@ func Bind[S1, S2, T any](
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value T rather than []T.
// This is useful when you need to compute a derived value from the current context
// without introducing additional array elements.
//
// Example:
//
// result := array.Let(
// func(sum int) func(s struct{ X int }) struct{ X, Sum int } {
// return func(s struct{ X int }) struct{ X, Sum int } {
// return struct{ X, Sum int }{s.X, sum}
// }
// },
// func(s struct{ X int }) int { return s.X * 2 },
// type State1 struct{ X int }
// type State2 struct{ X, Double int }
//
// result := F.Pipe2(
// []State1{{X: 5}, {X: 10}},
// array.Let(
// func(double int) func(s State1) State2 {
// return func(s State1) State2 {
// return State2{X: s.X, Double: double}
// }
// },
// func(s State1) int { return s.X * 2 },
// ),
// )
// // result: []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
//
//go:inline
func Let[S1, S2, T any](
@@ -84,18 +93,25 @@ func Let[S1, S2, T any](
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
// This is useful for adding constant values to the context.
// This is useful for adding constant values to the context without computation.
//
// Example:
//
// result := array.LetTo(
// func(name string) func(s struct{ X int }) struct{ X int; Name string } {
// return func(s struct{ X int }) struct{ X int; Name string } {
// return struct{ X int; Name string }{s.X, name}
// }
// },
// "constant",
// type State1 struct{ X int }
// type State2 struct{ X int; Name string }
//
// result := F.Pipe2(
// []State1{{X: 1}, {X: 2}},
// array.LetTo(
// func(name string) func(s State1) State2 {
// return func(s State1) State2 {
// return State2{X: s.X, Name: name}
// }
// },
// "constant",
// ),
// )
// // result: []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
//
//go:inline
func LetTo[S1, S2, T any](
@@ -107,15 +123,19 @@ func LetTo[S1, S2, T any](
// BindTo initializes a new state S1 from a value T.
// This is typically the first operation after Do to start building the context.
// It transforms each element of type T into a state of type S1.
//
// Example:
//
// type State struct{ X int }
//
// result := F.Pipe2(
// []int{1, 2, 3},
// array.BindTo(func(x int) struct{ X int } {
// return struct{ X int }{x}
// array.BindTo(func(x int) State {
// return State{X: x}
// }),
// )
// // result: []State{{X: 1}, {X: 2}, {X: 3}}
//
//go:inline
func BindTo[S1, T any](

View File

@@ -22,57 +22,176 @@ import (
"github.com/stretchr/testify/assert"
)
type TestState1 struct {
X int
}
type TestState2 struct {
X int
Y int
}
// TestLet tests the Let function
func TestLet(t *testing.T) {
result := F.Pipe2(
Do(TestState1{}),
type State1 struct {
X int
}
type State2 struct {
X int
Double int
}
// Test Let with pure computation
result := F.Pipe1(
[]State1{{X: 5}, {X: 10}},
Let(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s TestState1) int { return s.X * 2 },
func(s State1) int { return s.X * 2 },
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{0}, result)
expected := []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
assert.Equal(t, expected, result)
// Test Let with empty array
empty := []State1{}
result2 := F.Pipe1(
empty,
Let(
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s State1) int { return s.X * 2 },
),
)
assert.Equal(t, []State2{}, result2)
}
// TestLetTo tests the LetTo function
func TestLetTo(t *testing.T) {
result := F.Pipe2(
Do(TestState1{X: 5}),
type State1 struct {
X int
}
type State2 struct {
X int
Name string
}
// Test LetTo with constant value
result := F.Pipe1(
[]State1{{X: 1}, {X: 2}},
LetTo(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
func(name string) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Name: name}
}
},
42,
"constant",
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{47}, result)
expected := []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
assert.Equal(t, expected, result)
// Test LetTo with different constant
result2 := F.Pipe1(
[]State1{{X: 10}},
LetTo(
func(name string) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Name: name}
}
},
"test",
),
)
expected2 := []State2{{X: 10, Name: "test"}}
assert.Equal(t, expected2, result2)
}
// TestBindTo tests the BindTo function
func TestBindTo(t *testing.T) {
type State struct {
X int
}
// Test BindTo with integers
result := F.Pipe1(
[]int{1, 2, 3},
BindTo(func(x int) TestState1 {
return TestState1{X: x}
BindTo(func(x int) State {
return State{X: x}
}),
)
expected := []TestState1{{X: 1}, {X: 2}, {X: 3}}
expected := []State{{X: 1}, {X: 2}, {X: 3}}
assert.Equal(t, expected, result)
// Test BindTo with strings
type StringState struct {
Value string
}
result2 := F.Pipe1(
[]string{"hello", "world"},
BindTo(func(s string) StringState {
return StringState{Value: s}
}),
)
expected2 := []StringState{{Value: "hello"}, {Value: "world"}}
assert.Equal(t, expected2, result2)
// Test BindTo with empty array
empty := []int{}
result3 := F.Pipe1(
empty,
BindTo(func(x int) State {
return State{X: x}
}),
)
assert.Equal(t, []State{}, result3)
}
// TestDoWithLetAndBindTo tests combining Do, Let, LetTo, and BindTo
func TestDoWithLetAndBindTo(t *testing.T) {
type State1 struct {
X int
}
type State2 struct {
X int
Double int
}
type State3 struct {
X int
Double int
Name string
}
// Test complex pipeline
result := F.Pipe3(
[]int{5, 10},
BindTo(func(x int) State1 {
return State1{X: x}
}),
Let(
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s State1) int { return s.X * 2 },
),
LetTo(
func(name string) func(s State2) State3 {
return func(s State2) State3 {
return State3{X: s.X, Double: s.Double, Name: name}
}
},
"result",
),
)
expected := []State3{
{X: 5, Double: 10, Name: "result"},
{X: 10, Double: 20, Name: "result"},
}
assert.Equal(t, expected, result)
}

137
v2/array/coverage.out Normal file
View File

@@ -0,0 +1,137 @@
mode: set
github.com/IBM/fp-go/v2/array/any.go:34.65,36.2 1 1
github.com/IBM/fp-go/v2/array/any.go:48.51,50.2 1 1
github.com/IBM/fp-go/v2/array/array.go:30.33,32.2 1 1
github.com/IBM/fp-go/v2/array/array.go:37.52,39.2 1 1
github.com/IBM/fp-go/v2/array/array.go:44.39,46.2 1 0
github.com/IBM/fp-go/v2/array/array.go:52.50,54.2 1 0
github.com/IBM/fp-go/v2/array/array.go:58.54,61.23 3 0
github.com/IBM/fp-go/v2/array/array.go:61.23,63.3 1 0
github.com/IBM/fp-go/v2/array/array.go:64.2,64.11 1 0
github.com/IBM/fp-go/v2/array/array.go:70.62,72.2 1 0
github.com/IBM/fp-go/v2/array/array.go:83.48,85.2 1 1
github.com/IBM/fp-go/v2/array/array.go:89.52,91.2 1 0
github.com/IBM/fp-go/v2/array/array.go:93.55,96.23 3 0
github.com/IBM/fp-go/v2/array/array.go:96.23,98.14 2 0
github.com/IBM/fp-go/v2/array/array.go:98.14,100.4 1 0
github.com/IBM/fp-go/v2/array/array.go:102.2,102.15 1 0
github.com/IBM/fp-go/v2/array/array.go:105.75,108.23 3 0
github.com/IBM/fp-go/v2/array/array.go:108.23,110.14 2 0
github.com/IBM/fp-go/v2/array/array.go:110.14,112.4 1 0
github.com/IBM/fp-go/v2/array/array.go:114.2,114.15 1 0
github.com/IBM/fp-go/v2/array/array.go:120.54,122.2 1 1
github.com/IBM/fp-go/v2/array/array.go:127.68,129.2 1 0
github.com/IBM/fp-go/v2/array/array.go:132.58,134.2 1 0
github.com/IBM/fp-go/v2/array/array.go:140.67,142.2 1 0
github.com/IBM/fp-go/v2/array/array.go:148.78,150.2 1 0
github.com/IBM/fp-go/v2/array/array.go:155.65,157.2 1 1
github.com/IBM/fp-go/v2/array/array.go:162.76,164.2 1 0
github.com/IBM/fp-go/v2/array/array.go:169.69,171.2 1 1
github.com/IBM/fp-go/v2/array/array.go:174.80,175.26 1 0
github.com/IBM/fp-go/v2/array/array.go:175.26,177.3 1 0
github.com/IBM/fp-go/v2/array/array.go:180.64,182.25 2 0
github.com/IBM/fp-go/v2/array/array.go:182.25,184.3 1 0
github.com/IBM/fp-go/v2/array/array.go:185.2,185.16 1 0
github.com/IBM/fp-go/v2/array/array.go:189.65,191.2 1 1
github.com/IBM/fp-go/v2/array/array.go:194.79,196.2 1 1
github.com/IBM/fp-go/v2/array/array.go:206.62,208.2 1 1
github.com/IBM/fp-go/v2/array/array.go:214.76,216.2 1 0
github.com/IBM/fp-go/v2/array/array.go:221.67,223.2 1 1
github.com/IBM/fp-go/v2/array/array.go:229.81,231.2 1 0
github.com/IBM/fp-go/v2/array/array.go:235.66,236.24 1 0
github.com/IBM/fp-go/v2/array/array.go:236.24,238.3 1 0
github.com/IBM/fp-go/v2/array/array.go:254.37,256.2 1 1
github.com/IBM/fp-go/v2/array/array.go:261.34,263.2 1 1
github.com/IBM/fp-go/v2/array/array.go:266.37,268.2 1 1
github.com/IBM/fp-go/v2/array/array.go:273.25,275.2 1 1
github.com/IBM/fp-go/v2/array/array.go:280.24,282.2 1 0
github.com/IBM/fp-go/v2/array/array.go:287.25,289.2 1 1
github.com/IBM/fp-go/v2/array/array.go:295.56,297.2 1 0
github.com/IBM/fp-go/v2/array/array.go:308.54,310.2 1 1
github.com/IBM/fp-go/v2/array/array.go:316.53,318.2 1 0
github.com/IBM/fp-go/v2/array/array.go:324.50,326.2 1 1
github.com/IBM/fp-go/v2/array/array.go:331.76,333.2 1 1
github.com/IBM/fp-go/v2/array/array.go:338.83,340.2 1 0
github.com/IBM/fp-go/v2/array/array.go:346.38,348.2 1 0
github.com/IBM/fp-go/v2/array/array.go:354.36,356.2 1 1
github.com/IBM/fp-go/v2/array/array.go:362.37,364.2 1 0
github.com/IBM/fp-go/v2/array/array.go:370.36,372.2 1 0
github.com/IBM/fp-go/v2/array/array.go:375.49,376.26 1 1
github.com/IBM/fp-go/v2/array/array.go:376.26,380.35 4 1
github.com/IBM/fp-go/v2/array/array.go:380.35,385.4 4 1
github.com/IBM/fp-go/v2/array/array.go:386.3,386.16 1 1
github.com/IBM/fp-go/v2/array/array.go:395.50,397.26 2 1
github.com/IBM/fp-go/v2/array/array.go:397.26,398.18 1 1
github.com/IBM/fp-go/v2/array/array.go:398.18,400.4 1 1
github.com/IBM/fp-go/v2/array/array.go:401.3,401.25 1 1
github.com/IBM/fp-go/v2/array/array.go:406.60,407.36 1 1
github.com/IBM/fp-go/v2/array/array.go:407.36,409.3 1 1
github.com/IBM/fp-go/v2/array/array.go:419.36,421.2 1 1
github.com/IBM/fp-go/v2/array/array.go:424.49,426.2 1 1
github.com/IBM/fp-go/v2/array/array.go:432.49,434.2 1 1
github.com/IBM/fp-go/v2/array/array.go:440.42,442.2 1 0
github.com/IBM/fp-go/v2/array/array.go:447.30,449.2 1 1
github.com/IBM/fp-go/v2/array/array.go:456.78,458.2 1 0
github.com/IBM/fp-go/v2/array/array.go:464.75,466.2 1 1
github.com/IBM/fp-go/v2/array/array.go:469.32,471.2 1 0
github.com/IBM/fp-go/v2/array/array.go:474.35,476.2 1 0
github.com/IBM/fp-go/v2/array/array.go:479.28,481.2 1 0
github.com/IBM/fp-go/v2/array/array.go:486.50,488.2 1 0
github.com/IBM/fp-go/v2/array/array.go:493.29,495.2 1 0
github.com/IBM/fp-go/v2/array/array.go:500.47,502.2 1 0
github.com/IBM/fp-go/v2/array/array.go:507.67,509.2 1 1
github.com/IBM/fp-go/v2/array/array.go:514.81,516.2 1 0
github.com/IBM/fp-go/v2/array/array.go:521.45,523.2 1 0
github.com/IBM/fp-go/v2/array/array.go:528.38,530.2 1 0
github.com/IBM/fp-go/v2/array/array.go:605.43,607.2 1 1
github.com/IBM/fp-go/v2/array/array.go:613.52,615.2 1 0
github.com/IBM/fp-go/v2/array/array.go:621.49,623.2 1 0
github.com/IBM/fp-go/v2/array/array.go:628.44,630.2 1 0
github.com/IBM/fp-go/v2/array/array.go:714.33,716.2 1 1
github.com/IBM/fp-go/v2/array/array.go:780.53,781.26 1 1
github.com/IBM/fp-go/v2/array/array.go:781.26,782.47 1 1
github.com/IBM/fp-go/v2/array/array.go:782.47,782.67 1 1
github.com/IBM/fp-go/v2/array/array.go:839.31,841.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:36.7,38.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:60.20,62.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:91.20,93.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:120.20,122.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:143.19,145.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:166.20,168.2 1 1
github.com/IBM/fp-go/v2/array/eq.go:35.37,37.49 2 1
github.com/IBM/fp-go/v2/array/eq.go:37.49,39.3 1 1
github.com/IBM/fp-go/v2/array/eq.go:43.45,45.2 1 1
github.com/IBM/fp-go/v2/array/find.go:33.65,35.2 1 1
github.com/IBM/fp-go/v2/array/find.go:48.79,50.2 1 1
github.com/IBM/fp-go/v2/array/find.go:68.78,70.2 1 1
github.com/IBM/fp-go/v2/array/find.go:76.89,78.2 1 1
github.com/IBM/fp-go/v2/array/find.go:89.64,91.2 1 1
github.com/IBM/fp-go/v2/array/find.go:97.78,99.2 1 1
github.com/IBM/fp-go/v2/array/find.go:105.77,107.2 1 1
github.com/IBM/fp-go/v2/array/find.go:113.88,115.2 1 1
github.com/IBM/fp-go/v2/array/magma.go:38.50,40.2 1 1
github.com/IBM/fp-go/v2/array/monad.go:39.65,41.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:35.36,37.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:48.42,50.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:52.45,54.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:68.45,73.48 3 1
github.com/IBM/fp-go/v2/array/monoid.go:73.48,75.3 1 1
github.com/IBM/fp-go/v2/array/monoid.go:77.2,77.12 1 1
github.com/IBM/fp-go/v2/array/sequence.go:27.19,29.2 1 1
github.com/IBM/fp-go/v2/array/sequence.go:69.22,71.2 1 1
github.com/IBM/fp-go/v2/array/sequence.go:92.53,98.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:35.47,37.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:65.68,67.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:96.51,98.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:66.34,68.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:83.24,86.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:94.39,96.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:105.29,108.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:110.142,117.46 1 1
github.com/IBM/fp-go/v2/array/traverse.go:117.46,118.54 1 1
github.com/IBM/fp-go/v2/array/traverse.go:118.54,125.4 1 1
github.com/IBM/fp-go/v2/array/uniq.go:20.43,22.2 1 1
github.com/IBM/fp-go/v2/array/uniq.go:49.60,51.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:38.73,40.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:58.55,60.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:81.62,83.2 1 1

View File

@@ -21,7 +21,7 @@ import (
FC "github.com/IBM/fp-go/v2/internal/functor"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// Of constructs a single element array
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
}
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
return F.Flow2(
FilterMap[GA, []GB](f),
Flatten[[]GB],
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
}
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
left := Empty[GA]()
right := Empty[GA]()
array.Reduce(as, func(c bool, a A) bool {
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
return c
}, true)
// returns the partition
return tuple.MakeTuple2(left, right)
return pair.MakePair(left, right)
}
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
return F.Bind2nd(MonadPartition[GA, A], pred)
}
@@ -323,34 +323,49 @@ func Clone[AS ~[]A, A any](f func(A) A) func(as AS) AS {
}
func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B {
empty := m.Empty()
concat := m.Concat
return func(f func(A) B) func(AS) B {
return func(as AS) B {
return array.Reduce(as, func(cur B, a A) B {
return concat(cur, f(a))
}, empty)
switch len(as) {
case 0:
return m.Empty()
case 1:
return f(as[0])
case 2:
return concat(f(as[0]), f(as[1]))
default:
return array.Reduce(as[1:], func(cur B, a A) B {
return concat(cur, f(a))
}, f(as[0]))
}
}
}
}
func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B {
empty := m.Empty()
concat := m.Concat
return func(f func(int, A) B) func(AS) B {
return func(as AS) B {
return array.ReduceWithIndex(as, func(idx int, cur B, a A) B {
return concat(cur, f(idx, a))
}, empty)
}, m.Empty())
}
}
}
func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
empty := m.Empty()
concat := m.Concat
return func(as AS) A {
return array.Reduce(as, concat, empty)
switch len(as) {
case 0:
return m.Empty()
case 1:
return as[0]
case 2:
return concat(as[0], as[1])
default:
return array.Reduce(as[1:], concat, as[0])
}
}
}

View File

@@ -18,7 +18,7 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
// longer array are discarded
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
}
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2
as[i] = pair.Head(t)
bs[i] = pair.Tail(t)
}
return T.MakeTuple2(as, bs)
return pair.MakePair(as, bs)
}

View File

@@ -764,14 +764,14 @@ func TestFoldMap(t *testing.T) {
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4)
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
})
t.Run("FoldMap with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From(1, 2, 3)
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
assert.Equal(t, "123", result)
})
}

View File

@@ -25,7 +25,7 @@ func MonadSequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
ma []HKTA) HKTRA {
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
return array.MonadSequence(fof, m.Empty, m.Concat, ma)
}
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
@@ -67,7 +67,7 @@ func Sequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
) func([]HKTA) HKTRA {
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
return array.Sequence[[]HKTA](fof, m.Empty, m.Concat)
}
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestSequenceWithOption tests the generic Sequence function with Option monad
func TestSequenceWithOption(t *testing.T) {
// Test with Option monad - all Some values
opts := From(
O.Some(1),
O.Some(2),
O.Some(3),
)
// Use the Sequence function with Option's applicative monoid
monoid := O.ApplicativeMonoid(Monoid[int]())
seq := Sequence(O.Map(Of[int]), monoid)
result := seq(opts)
assert.Equal(t, O.Of(From(1, 2, 3)), result)
// Test with Option monad - contains None
optsWithNone := From(
O.Some(1),
O.None[int](),
O.Some(3),
)
result2 := seq(optsWithNone)
assert.True(t, O.IsNone(result2))
// Test with empty array
empty := Empty[Option[int]]()
result3 := seq(empty)
assert.Equal(t, O.Some(Empty[int]()), result3)
}
// TestMonadSequence tests the MonadSequence function
func TestMonadSequence(t *testing.T) {
// Test with Option monad
opts := From(
O.Some("hello"),
O.Some("world"),
)
monoid := O.ApplicativeMonoid(Monoid[string]())
result := MonadSequence(O.Map(Of[string]), monoid, opts)
assert.Equal(t, O.Of(From("hello", "world")), result)
// Test with None in the array
optsWithNone := From(
O.Some("hello"),
O.None[string](),
)
result2 := MonadSequence(O.Map(Of[string]), monoid, optsWithNone)
assert.Equal(t, O.None[[]string](), result2)
}

View File

@@ -0,0 +1,164 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
"strconv"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMonadTraverse tests the MonadTraverse function
func TestMonadTraverse(t *testing.T) {
// Test converting integers to strings via Option
numbers := []int{1, 2, 3}
result := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"1", "2", "3"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with a function that can return None
result2 := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(n int) O.Option[string] {
if n == 2 {
return O.None[string]()
}
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsNone(result2))
// Test with empty array
empty := []int{}
result3 := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
empty,
func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsSome(result3))
assert.Equal(t, []string{}, O.GetOrElse(func() []string { return nil })(result3))
}
// TestTraverseWithIndex tests the TraverseWithIndex function
func TestTraverseWithIndex(t *testing.T) {
// Test with index-aware transformation
words := []string{"a", "b", "c"}
traverser := TraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
func(idx int, s string) O.Option[string] {
return O.Some(s + strconv.Itoa(idx))
},
)
result := traverser(words)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"a0", "b1", "c2"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with conditional None based on index
traverser2 := TraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
func(idx int, s string) O.Option[string] {
if idx == 1 {
return O.None[string]()
}
return O.Some(s)
},
)
result2 := traverser2(words)
assert.True(t, O.IsNone(result2))
}
// TestMonadTraverseWithIndex tests the MonadTraverseWithIndex function
func TestMonadTraverseWithIndex(t *testing.T) {
// Test with index-aware transformation
numbers := []int{10, 20, 30}
result := MonadTraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(idx, n int) O.Option[string] {
return O.Some(strconv.Itoa(n * idx))
},
)
assert.True(t, O.IsSome(result))
// Expected: [10*0, 20*1, 30*2] = ["0", "20", "60"]
assert.Equal(t, []string{"0", "20", "60"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with None at specific index
result2 := MonadTraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(idx, n int) O.Option[string] {
if idx == 2 {
return O.None[string]()
}
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsNone(result2))
}
// TestMakeTraverseType tests the MakeTraverseType function
func TestMakeTraverseType(t *testing.T) {
// Create a traverse type for Option
traverseType := MakeTraverseType[int, string, O.Option[string], O.Option[[]string], O.Option[func(string) []string]]()
// Use it to traverse an array
numbers := []int{1, 2, 3}
result := traverseType(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
)(func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n * 2))
})(numbers)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"2", "4", "6"}, O.GetOrElse(func() []string { return []string{} })(result))
}

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays,
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
// // Result: [(a, 1), (b, 2)]
//
//go:inline
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
}
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
// ages := result.Tail // [30, 25, 35]
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
return G.Unzip[[]A, []B](cs)
}

View File

@@ -19,7 +19,7 @@ import (
"fmt"
"testing"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
res := Zip[string](left)(right)
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
}
func TestUnzip(t *testing.T) {
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
unzipped := Unzip(zipped)
assert.Equal(t, right, unzipped.F1)
assert.Equal(t, left, unzipped.F2)
assert.Equal(t, right, pair.Head(unzipped))
assert.Equal(t, left, pair.Tail(unzipped))
}

View File

@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
}
}
// ArrayEmpty checks if an array is empty.
//
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
//
// Example:
//
// func TestArrayEmpty(t *testing.T) {
// empty := []int{}
// assert.ArrayEmpty(empty)(t) // Passes
//
// numbers := []int{1, 2, 3}
// assert.ArrayEmpty(numbers)(t) // Fails
// }
func ArrayEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, arr)
}
}
// RecordNotEmpty checks if a map is not empty.
//
// Example:
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
}
}
// RecordEmpty checks if a map is empty.
//
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
//
// Example:
//
// func TestRecordEmpty(t *testing.T) {
// empty := map[string]int{}
// assert.RecordEmpty(empty)(t) // Passes
//
// config := map[string]int{"timeout": 30}
// assert.RecordEmpty(config)(t) // Fails
// }
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, mp)
}
}
// StringNotEmpty checks if a string is not empty.
//
// Example:
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
//
//go:inline
func RunAll(testcases map[string]Reader) Reader {
return func(t *testing.T) bool {
current := true
for k, r := range testcases {
current = current && t.Run(k, func(t1 *testing.T) {
r(t1)
})
}
return current
}
return SequenceRecord(testcases)
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,

View File

@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
})
}
func TestArrayEmpty(t *testing.T) {
t.Run("should pass for empty array", func(t *testing.T) {
arr := []int{}
result := ArrayEmpty(arr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty array")
}
})
t.Run("should fail for non-empty array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayEmpty(arr)(mockT)
if result {
t.Error("Expected ArrayEmpty to fail for non-empty array")
}
})
t.Run("should work with different types", func(t *testing.T) {
strArr := []string{}
result := ArrayEmpty(strArr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty string array")
}
})
}
func TestRecordNotEmpty(t *testing.T) {
t.Run("should pass for non-empty map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
})
}
func TestRecordEmpty(t *testing.T) {
t.Run("should pass for empty map", func(t *testing.T) {
mp := map[string]int{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map")
}
})
t.Run("should fail for non-empty map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := RecordEmpty(mp)(mockT)
if result {
t.Error("Expected RecordEmpty to fail for non-empty map")
}
})
t.Run("should work with different key-value types", func(t *testing.T) {
mp := map[int]string{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map with int keys")
}
})
}
func TestRecordLength(t *testing.T) {
t.Run("should pass when map length matches", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
})
}
func TestStringNotEmpty(t *testing.T) {
t.Run("should pass for non-empty string", func(t *testing.T) {
str := "Hello, World!"
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for non-empty string")
}
})
t.Run("should fail for empty string", func(t *testing.T) {
mockT := &testing.T{}
str := ""
result := StringNotEmpty(str)(mockT)
if result {
t.Error("Expected StringNotEmpty to fail for empty string")
}
})
t.Run("should pass for string with whitespace", func(t *testing.T) {
str := " "
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for string with whitespace")
}
})
}
func TestStringLength(t *testing.T) {
t.Run("should pass when string length matches", func(t *testing.T) {
str := "hello"

122
v2/assert/from.go Normal file
View File

@@ -0,0 +1,122 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations that may fail
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
// ReaderIOResult computation using the test's context, handles any potential errors by
// converting them to test failures via NoError, and returns the resulting Reader.
//
// The conversion process:
// 1. Executes the ReaderIOResult with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Extracts the Result, converting errors to test failures using NoError
// 4. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations (file access, network calls, etc.)
// - Handle potential errors gracefully in tests
//
// Parameters:
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithContext(t *testing.T) {
// // Create a ReaderIOResult that performs an IO operation
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Simulate database check
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
return func(t *testing.T) bool {
return F.Pipe1(
ri(t.Context())(),
result.GetOrElse(NoError),
)(t)
}
}
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
// and the simpler Reader type used for test assertions. It executes the ReaderIO
// computation using the test's context and returns the resulting Reader.
//
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
//
// The conversion process:
// 1. Executes the ReaderIO with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware testing utilities
//
// Parameters:
// - ri: A ReaderIO that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithIO(t *testing.T) {
// // Create a ReaderIO that performs an IO operation
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log something using context
// logger.InfoContext(ctx, "Running test")
// // Return an assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
func FromReaderIO(ri ReaderIO[Reader]) Reader {
return func(t *testing.T) bool {
return ri(t.Context())()(t)
}
}

383
v2/assert/from_test.go Normal file
View File

@@ -0,0 +1,383 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/result"
)
func TestFromReaderIOResult(t *testing.T) {
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Reader
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Return a Reader that always passes
return result.Of(func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
}
})
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Equal assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of(Equal(42)(42))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns an error
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Left[Reader](errors.New("test error"))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
}
})
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns a failing assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of(Equal(42)(43))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIOResult that checks if context is provided
ri := func(ctx context.Context) func() result.Result[Reader] {
if ctx != nil {
contextUsed = true
}
return func() result.Result[Reader] {
return result.Of(func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIOResult to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns NoError assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of(NoError(nil))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIOResult with multiple composed assertions
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
arr := []int{1, 2, 3}
assertions := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](3)(arr),
ArrayContains(2)(arr),
})
return result.Of(assertions)
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with complex assertions")
}
})
}
func TestFromReaderIO(t *testing.T) {
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
// Create a ReaderIO that returns a Reader that always passes
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
}
})
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
// Create a ReaderIO that returns an Equal assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(42)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIO that returns a failing assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(43)
}
}
reader := FromReaderIO(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIO to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIO that checks if context is provided
ri := func(ctx context.Context) func() Reader {
if ctx != nil {
contextUsed = true
}
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIO to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIO that returns NoError assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return NoError(nil)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with NoError assertion")
}
})
t.Run("should work with Error assertion", func(t *testing.T) {
// Create a ReaderIO that returns Error assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Error(errors.New("expected error"))
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Error assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIO with multiple composed assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
mp := map[string]int{"a": 1, "b": 2}
return AllOf([]Reader{
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
ContainsKey[int]("a")(mp),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with complex assertions")
}
})
t.Run("should work with string assertions", func(t *testing.T) {
// Create a ReaderIO with string assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
str := "hello world"
return AllOf([]Reader{
StringNotEmpty(str),
StringLength[any, any](11)(str),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with string assertions")
}
})
t.Run("should work with Result assertions", func(t *testing.T) {
// Create a ReaderIO with Result assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
successResult := result.Of(42)
return Success(successResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Success assertion")
}
})
t.Run("should work with Failure assertion", func(t *testing.T) {
// Create a ReaderIO with Failure assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
failureResult := result.Left[int](errors.New("test error"))
return Failure(failureResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Failure assertion")
}
})
}
// TestFromReaderIOResultIntegration tests integration scenarios
func TestFromReaderIOResultIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
// Create a ReaderIOResult that uses the context
ri := func(testCtx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Check if context is valid
if testCtx == nil {
return result.Left[Reader](errors.New("context is nil"))
}
// Return a successful assertion
return result.Of(Equal("test")("test"))
}
}
// Use the actual testing.T from the subtest
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
})
}
// TestFromReaderIOIntegration tests integration scenarios
func TestFromReaderIOIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
logCalled := false
// Create a ReaderIO that simulates logging
ri := func(ctx context.Context) func() Reader {
return func() Reader {
// Simulate logging with context
if ctx != nil {
logCalled = true
}
// Return an assertion
return Equal(100)(100)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
if !logCalled {
t.Error("Expected logging to be called")
}
})
}

207
v2/assert/logger.go Normal file
View File

@@ -0,0 +1,207 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/readerio"
)
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
//
// This function provides a functional programming approach to test logging, returning a
// [ReaderIO] that can be composed with other test operations. It's particularly useful
// for debugging tests, tracing execution flow, or documenting test behavior without
// affecting test outcomes.
//
// The function uses a curried design pattern:
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
// 2. This returns a function that takes a value of type T
// 3. That function returns a ReaderIO that performs the logging when executed
//
// # Parameters
//
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
// The format string should contain exactly one format verb that matches type T
//
// # Returns
//
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
// When executed, this ReaderIO logs the formatted message to the test output
//
// # Type Parameters
//
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
//
// # Use Cases
//
// - Debugging test execution by logging intermediate values
// - Tracing the flow of complex test scenarios
// - Documenting test behavior in the test output
// - Logging values in functional pipelines without breaking the chain
// - Creating reusable logging operations for specific types
//
// # Example - Basic Logging
//
// func TestBasicLogging(t *testing.T) {
// // Create a logger for integers
// logInt := assert.Logf[int]("Processing value: %d")
//
// // Use it to log a value
// value := 42
// logInt(value)(t)() // Outputs: "Processing value: 42"
// }
//
// # Example - Logging in Test Pipeline
//
// func TestPipelineWithLogging(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// user := User{Name: "Alice", Age: 30}
//
// // Create a logger for User
// logUser := assert.Logf[User]("Testing user: %+v")
//
// // Log the user being tested
// logUser(user)(t)()
//
// // Continue with assertions
// assert.StringNotEmpty(user.Name)(t)
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
// }
//
// # Example - Multiple Loggers for Different Types
//
// func TestMultipleLoggers(t *testing.T) {
// // Create type-specific loggers
// logString := assert.Logf[string]("String value: %s")
// logInt := assert.Logf[int]("Integer value: %d")
// logFloat := assert.Logf[float64]("Float value: %.2f")
//
// // Use them throughout the test
// logString("hello")(t)() // Outputs: "String value: hello"
// logInt(42)(t)() // Outputs: "Integer value: 42"
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
// }
//
// # Example - Logging Complex Structures
//
// func TestComplexStructureLogging(t *testing.T) {
// type Config struct {
// Host string
// Port int
// Timeout int
// }
//
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
//
// // Use %+v to include field names
// logConfig := assert.Logf[Config]("Configuration: %+v")
// logConfig(config)(t)()
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
//
// // Or use %#v for Go-syntax representation
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
// logConfigGo(config)(t)()
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
// }
//
// # Example - Debugging Test Failures
//
// func TestWithDebugLogging(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// logSlice := assert.Logf[[]int]("Testing slice: %v")
//
// // Log the input data
// logSlice(numbers)(t)()
//
// // Perform assertions
// assert.ArrayNotEmpty(numbers)(t)
// assert.ArrayLength[int](5)(numbers)(t)
//
// // Log intermediate results
// sum := 0
// for _, n := range numbers {
// sum += n
// }
// logInt := assert.Logf[int]("Sum: %d")
// logInt(sum)(t)()
//
// assert.Equal(15)(sum)(t)
// }
//
// # Example - Conditional Logging
//
// func TestConditionalLogging(t *testing.T) {
// logDebug := assert.Logf[string]("DEBUG: %s")
//
// values := []int{1, 2, 3, 4, 5}
// for _, v := range values {
// if v%2 == 0 {
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
// }
// }
// // Outputs:
// // DEBUG: Found even number: 2
// // DEBUG: Found even number: 4
// }
//
// # Format Verbs
//
// Common format verbs you can use in the prefix string:
// - %v: Default format
// - %+v: Default format with field names for structs
// - %#v: Go-syntax representation
// - %T: Type of the value
// - %d: Integer in base 10
// - %s: String
// - %f: Floating point number
// - %t: Boolean (true/false)
// - %p: Pointer address
//
// See the fmt package documentation for a complete list of format verbs.
//
// # Notes
//
// - Logging does not affect test pass/fail status
// - Log output appears in test results when running with -v flag or when tests fail
// - The function returns Void, indicating it's used for side effects only
// - The ReaderIO pattern allows logging to be composed with other operations
//
// # Related Functions
//
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
// - testing.T.Logf: The underlying Go testing log function
//
// # References
//
// - Go testing package: https://pkg.go.dev/testing
// - fmt package format verbs: https://pkg.go.dev/fmt
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
return func(a T) readerio.ReaderIO[*testing.T, Void] {
return func(t *testing.T) IO[Void] {
return io.FromImpure(func() {
t.Logf(prefix, a)
})
}
}
}

406
v2/assert/logger_test.go Normal file
View File

@@ -0,0 +1,406 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"fmt"
"testing"
)
// TestLogf_BasicInteger tests basic integer logging
func TestLogf_BasicInteger(t *testing.T) {
logInt := Logf[int]("Processing value: %d")
// This should not panic and should log the value
logInt(42)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicString tests basic string logging
func TestLogf_BasicString(t *testing.T) {
logString := Logf[string]("String value: %s")
logString("hello world")(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicFloat tests basic float logging
func TestLogf_BasicFloat(t *testing.T) {
logFloat := Logf[float64]("Float value: %.2f")
logFloat(3.14159)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicBoolean tests basic boolean logging
func TestLogf_BasicBoolean(t *testing.T) {
logBool := Logf[bool]("Boolean value: %t")
logBool(true)(t)()
logBool(false)(t)()
// Test passes if no panic occurs
}
// TestLogf_ComplexStruct tests logging of complex structures
func TestLogf_ComplexStruct(t *testing.T) {
type User struct {
Name string
Age int
}
logUser := Logf[User]("User: %+v")
user := User{Name: "Alice", Age: 30}
logUser(user)(t)()
// Test passes if no panic occurs
}
// TestLogf_Slice tests logging of slices
func TestLogf_Slice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
// Test passes if no panic occurs
}
// TestLogf_Map tests logging of maps
func TestLogf_Map(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
data := map[string]int{"a": 1, "b": 2, "c": 3}
logMap(data)(t)()
// Test passes if no panic occurs
}
// TestLogf_Pointer tests logging of pointers
func TestLogf_Pointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %p")
value := 42
logPtr(&value)(t)()
// Test passes if no panic occurs
}
// TestLogf_NilPointer tests logging of nil pointers
func TestLogf_NilPointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %v")
var nilPtr *int
logPtr(nilPtr)(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyString tests logging of empty strings
func TestLogf_EmptyString(t *testing.T) {
logString := Logf[string]("String: '%s'")
logString("")(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptySlice tests logging of empty slices
func TestLogf_EmptySlice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
logSlice([]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyMap tests logging of empty maps
func TestLogf_EmptyMap(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
logMap(map[string]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_MultipleTypes tests using multiple loggers for different types
func TestLogf_MultipleTypes(t *testing.T) {
logString := Logf[string]("String: %s")
logInt := Logf[int]("Integer: %d")
logFloat := Logf[float64]("Float: %.2f")
logString("test")(t)()
logInt(42)(t)()
logFloat(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_WithinTestPipeline tests logging within a test pipeline
func TestLogf_WithinTestPipeline(t *testing.T) {
type Config struct {
Host string
Port int
}
config := Config{Host: "localhost", Port: 8080}
logConfig := Logf[Config]("Testing config: %+v")
logConfig(config)(t)()
// Continue with assertions
StringNotEmpty(config.Host)(t)
That(func(port int) bool { return port > 0 })(config.Port)(t)
// Test passes if no panic occurs and assertions pass
}
// TestLogf_NestedStructures tests logging of nested structures
func TestLogf_NestedStructures(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
logPerson := Logf[Person]("Person: %+v")
person := Person{
Name: "Bob",
Address: Address{
Street: "123 Main St",
City: "Springfield",
},
}
logPerson(person)(t)()
// Test passes if no panic occurs
}
// TestLogf_Interface tests logging of interface values
func TestLogf_Interface(t *testing.T) {
logAny := Logf[any]("Value: %v")
logAny(42)(t)()
logAny("string")(t)()
logAny([]int{1, 2, 3})(t)()
// Test passes if no panic occurs
}
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
func TestLogf_GoSyntaxFormat(t *testing.T) {
type Point struct {
X int
Y int
}
logPoint := Logf[Point]("Point: %#v")
point := Point{X: 10, Y: 20}
logPoint(point)(t)()
// Test passes if no panic occurs
}
// TestLogf_TypeFormat tests logging with type format
func TestLogf_TypeFormat(t *testing.T) {
logType := Logf[any]("Type: %T, Value: %v")
logType(42)(t)()
logType("string")(t)()
logType(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_LargeNumbers tests logging of large numbers
func TestLogf_LargeNumbers(t *testing.T) {
logInt := Logf[int64]("Large number: %d")
logInt(9223372036854775807)(t)() // Max int64
// Test passes if no panic occurs
}
// TestLogf_NegativeNumbers tests logging of negative numbers
func TestLogf_NegativeNumbers(t *testing.T) {
logInt := Logf[int]("Number: %d")
logInt(-42)(t)()
logInt(-100)(t)()
// Test passes if no panic occurs
}
// TestLogf_SpecialFloats tests logging of special float values
func TestLogf_SpecialFloats(t *testing.T) {
logFloat := Logf[float64]("Float: %v")
logFloat(0.0)(t)()
logFloat(-0.0)(t)()
// Test passes if no panic occurs
}
// TestLogf_UnicodeStrings tests logging of unicode strings
func TestLogf_UnicodeStrings(t *testing.T) {
logString := Logf[string]("Unicode: %s")
logString("Hello, 世界")(t)()
logString("Emoji: 🎉🎊")(t)()
// Test passes if no panic occurs
}
// TestLogf_MultilineStrings tests logging of multiline strings
func TestLogf_MultilineStrings(t *testing.T) {
logString := Logf[string]("Multiline:\n%s")
multiline := `Line 1
Line 2
Line 3`
logString(multiline)(t)()
// Test passes if no panic occurs
}
// TestLogf_ReuseLogger tests reusing the same logger multiple times
func TestLogf_ReuseLogger(t *testing.T) {
logInt := Logf[int]("Value: %d")
for i := 0; i < 5; i++ {
logInt(i)(t)()
}
// Test passes if no panic occurs
}
// TestLogf_ConditionalLogging tests conditional logging based on values
func TestLogf_ConditionalLogging(t *testing.T) {
logDebug := Logf[string]("DEBUG: %s")
values := []int{1, 2, 3, 4, 5}
for _, v := range values {
if v%2 == 0 {
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
}
}
// Test passes if no panic occurs
}
// TestLogf_WithAssertions tests combining logging with assertions
func TestLogf_WithAssertions(t *testing.T) {
logInt := Logf[int]("Testing value: %d")
value := 42
logInt(value)(t)()
// Perform assertion after logging
Equal(42)(value)(t)
// Test passes if assertion passes
}
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
func TestLogf_DebuggingFailures(t *testing.T) {
logSlice := Logf[[]int]("Input slice: %v")
logInt := Logf[int]("Computed sum: %d")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
sum := 0
for _, n := range numbers {
sum += n
}
logInt(sum)(t)()
Equal(15)(sum)(t)
// Test passes if assertion passes
}
// TestLogf_ComplexDataStructures tests logging of complex nested data
func TestLogf_ComplexDataStructures(t *testing.T) {
type Metadata struct {
Version string
Tags []string
}
type Document struct {
ID int
Title string
Metadata Metadata
}
logDoc := Logf[Document]("Document: %+v")
doc := Document{
ID: 1,
Title: "Test Document",
Metadata: Metadata{
Version: "1.0",
Tags: []string{"test", "example"},
},
}
logDoc(doc)(t)()
// Test passes if no panic occurs
}
// TestLogf_ArrayTypes tests logging of array types
func TestLogf_ArrayTypes(t *testing.T) {
logArray := Logf[[5]int]("Array: %v")
arr := [5]int{1, 2, 3, 4, 5}
logArray(arr)(t)()
// Test passes if no panic occurs
}
// TestLogf_ChannelTypes tests logging of channel types
func TestLogf_ChannelTypes(t *testing.T) {
logChan := Logf[chan int]("Channel: %v")
ch := make(chan int, 1)
logChan(ch)(t)()
close(ch)
// Test passes if no panic occurs
}
// TestLogf_FunctionTypes tests logging of function types
func TestLogf_FunctionTypes(t *testing.T) {
logFunc := Logf[func() int]("Function: %v")
fn := func() int { return 42 }
logFunc(fn)(t)()
// Test passes if no panic occurs
}

152
v2/assert/monoid.go Normal file
View File

@@ -0,0 +1,152 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
)
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
//
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
// meaning all assertions must pass for the combined assertion to pass. It leverages the
// applicative structure of Reader to execute multiple assertions with the same testing.T
// context and combines their boolean results using boolean.MonoidAll (logical AND).
//
// The monoid provides:
// - Concat: Combines two assertions such that both must pass (logical AND)
// - Empty: Returns an assertion that always passes (identity element)
//
// This is particularly useful for:
// - Composing multiple test assertions into a single assertion
// - Building complex test conditions from simpler ones
// - Creating reusable assertion combinators
// - Implementing test assertion DSLs
//
// # Monoid Laws
//
// The returned monoid satisfies the standard monoid laws:
//
// 1. Associativity:
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
//
// 2. Left Identity:
// Concat(Empty(), a) ≡ a
//
// 3. Right Identity:
// Concat(a, Empty()) ≡ a
//
// # Returns
//
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
//
// # Example - Basic Usage
//
// func TestUserValidation(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
// m := assert.ApplicativeMonoid()
//
// // Combine multiple assertions
// assertion := m.Concat(
// assert.Equal("Alice")(user.Name),
// m.Concat(
// assert.Equal(30)(user.Age),
// assert.StringNotEmpty(user.Email),
// ),
// )
//
// // Execute combined assertion
// assertion(t) // All three assertions must pass
// }
//
// # Example - Building Reusable Validators
//
// func TestWithReusableValidators(t *testing.T) {
// m := assert.ApplicativeMonoid()
//
// // Create a reusable validator
// validateUser := func(u User) assert.Reader {
// return m.Concat(
// assert.StringNotEmpty(u.Name),
// m.Concat(
// assert.True(u.Age > 0),
// assert.StringContains("@")(u.Email),
// ),
// )
// }
//
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
// validateUser(user)(t)
// }
//
// # Example - Using Empty for Identity
//
// func TestEmptyIdentity(t *testing.T) {
// m := assert.ApplicativeMonoid()
// assertion := assert.Equal(42)(42)
//
// // Empty is the identity - these are equivalent
// result1 := m.Concat(m.Empty(), assertion)(t)
// result2 := m.Concat(assertion, m.Empty())(t)
// result3 := assertion(t)
// // All three produce the same result
// }
//
// # Example - Combining with AllOf
//
// func TestCombiningWithAllOf(t *testing.T) {
// // ApplicativeMonoid provides the underlying mechanism for AllOf
// arr := []int{1, 2, 3, 4, 5}
//
// // These are conceptually equivalent:
// m := assert.ApplicativeMonoid()
// manual := m.Concat(
// assert.ArrayNotEmpty(arr),
// m.Concat(
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// ),
// )
//
// // AllOf uses ApplicativeMonoid internally
// convenient := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// })
//
// manual(t)
// convenient(t)
// }
//
// # Related Functions
//
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
//
// # References
//
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
func ApplicativeMonoid() monoid.Monoid[Reader] {
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
}

454
v2/assert/monoid_test.go Normal file
View File

@@ -0,0 +1,454 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
)
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
func TestApplicativeMonoid_Empty(t *testing.T) {
m := ApplicativeMonoid()
empty := m.Empty()
result := empty(t)
if !result {
t.Error("Expected Empty() to return an assertion that always passes")
}
}
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(t)
if !result {
t.Error("Expected Concat to pass when both assertions pass")
}
}
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when first assertion fails")
}
}
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when second assertion fails")
}
}
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when both assertions fail")
}
}
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(Empty(), assertion) should behave the same as assertion
combined := m.Concat(m.Empty(), assertion)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
}
}
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(assertion, Empty()) should behave the same as assertion
combined := m.Concat(assertion, m.Empty())
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
}
}
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
func TestApplicativeMonoid_Associativity(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(t)
result2 := right(t)
if result1 != result2 {
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
}
}
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(3) // This will fail
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(mockT)
result2 := right(mockT)
if result1 != result2 {
t.Error("Associativity law violated even with failures")
}
if result1 || result2 {
t.Error("Expected both to fail when one assertion fails")
}
}
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(t)
if !result {
t.Error("Expected complex combined assertions to pass")
}
}
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
arr := []int{1, 2, 3}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(mockT)
if result {
t.Error("Expected complex combined assertions to fail when one assertion fails")
}
}
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
a4 := Equal(4)(4)
combined := m.Concat(
m.Concat(a1, a2),
m.Concat(a3, a4),
)
result := combined(t)
if !result {
t.Error("Expected multiple Concat operations to pass when all assertions pass")
}
}
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
m := ApplicativeMonoid()
str := "hello world"
combined := m.Concat(
StringNotEmpty(str),
StringLength[any, any](11)(str),
)
result := combined(t)
if !result {
t.Error("Expected string assertions to pass")
}
}
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
Equal(true)(true),
m.Concat(
Equal(false)(false),
Equal(true)(true),
),
)
result := combined(t)
if !result {
t.Error("Expected boolean assertions to pass")
}
}
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
NoError(nil),
Equal("test")("test"),
)
result := combined(t)
if !result {
t.Error("Expected error assertions to pass")
}
}
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Multiple Empty values should still act as identity
combined := m.Concat(
m.Empty(),
m.Concat(
assertion,
m.Empty(),
),
)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Multiple Empty values should still act as identity")
}
}
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
m := ApplicativeMonoid()
// Concat of Empty values should still be Empty (identity)
combined := m.Concat(m.Empty(), m.Empty())
result := combined(t)
if !result {
t.Error("Expected Concat of Empty values to pass")
}
}
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
result := validateUser(validUser)(t)
if !result {
t.Error("Expected valid user to pass all validations")
}
}
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
mockT := &testing.T{}
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
result := validateUser(invalidUser)(mockT)
if result {
t.Error("Expected invalid user to fail validation")
}
}
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
// Using ApplicativeMonoid directly
manualCombination := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
// Using AllOf (which uses ApplicativeMonoid internally)
allOfCombination := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
})
result1 := manualCombination(t)
result2 := allOfCombination(t)
if result1 != result2 {
t.Error("Expected manual combination and AllOf to produce same result")
}
if !result1 || !result2 {
t.Error("Expected both combinations to pass")
}
}

650
v2/assert/traverse.go Normal file
View File

@@ -0,0 +1,650 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
)
// TraverseArray transforms an array of values into a test suite by applying a function
// that generates named test cases for each element.
//
// This function enables data-driven testing where you have a collection of test inputs
// and want to run a named subtest for each one. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each element of the array, applies the provided function to generate
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
// Go's t.Run. All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A function that takes a value of type T and returns a [Pair] containing:
// - Head: The test name (string) for the subtest
// - Tail: The test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
// - Executes each element as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with multiple test cases
// - Parameterized tests where each parameter gets its own subtest
// - Testing collections where each element needs validation
// - Property-based testing with generated test data
//
// # Example - Basic Data-Driven Testing
//
// func TestMathOperations(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// {Input: 4, Expected: 16},
// }
//
// square := func(n int) int { return n * n }
//
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// # Example - String Validation
//
// func TestStringValidation(t *testing.T) {
// inputs := []string{"hello", "world", "test"}
//
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("validate_%s", s),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(s),
// assert.That(func(str string) bool { return len(str) > 0 })(s),
// }),
// )
// })
//
// traverse(inputs)(t)
// }
//
// # Example - Complex Object Testing
//
// func TestUsers(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// users := []User{
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
// }
//
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("user_%s", u.Name),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 })(u.Age),
// assert.That(func(email string) bool {
// return len(email) > 0 && strings.Contains(email, "@")
// })(u.Email),
// }),
// )
// })
//
// traverse(users)(t)
// }
//
// # Comparison with RunAll
//
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
//
// - TraverseArray: Generates test cases from an array of data
//
// - Input: Array of values + function to generate test cases
//
// - Use when: You have test data and need to generate test cases from it
//
// - RunAll: Executes pre-defined named test cases
//
// - Input: Map of test names to assertions
//
// - Use when: You have already defined test cases with names
//
// # Related Functions
//
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
return func(ts []T) Reader {
return func(t *testing.T) bool {
ok := true
for _, src := range ts {
test := f(src)
res := t.Run(pair.Head(test), func(t *testing.T) {
pair.Tail(test)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
//
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
// requiring an array.
//
// The function iterates through all test cases, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - s: A [Seq2] iterator that yields pairs of:
// - Key: Test name (string) for the subtest
// - Value: Test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each test case as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Working with iterator-based test data
// - Lazy evaluation of test cases
// - Integration with Go 1.23+ iterator patterns
// - Memory-efficient testing of large test suites
//
// # Example - Basic Usage with Iterator
//
// func TestWithIterator(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// assert.SequenceSeq2(testCases)(t)
// }
//
// # Example - Generated Test Cases
//
// func TestGeneratedCases(t *testing.T) {
// // Generate test cases on the fly
// generateTests := func(yield func(string, assert.Reader) bool) {
// for i := 1; i <= 5; i++ {
// name := fmt.Sprintf("test_%d", i)
// assertion := assert.Equal(i*i)(i * i)
// if !yield(name, assertion) {
// return
// }
// }
// }
//
// assert.SequenceSeq2(generateTests)(t)
// }
//
// # Example - Filtering Test Cases
//
// func TestFilteredCases(t *testing.T) {
// type TestCase struct {
// Name string
// Input int
// Expected int
// Skip bool
// }
//
// allCases := []TestCase{
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
// }
//
// // Create iterator that filters out skipped tests
// activeTests := func(yield func(string, assert.Reader) bool) {
// for _, tc := range allCases {
// if !tc.Skip {
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// if !yield(tc.Name, assertion) {
// return
// }
// }
// }
// }
//
// assert.SequenceSeq2(activeTests)(t)
// }
//
// # Comparison with TraverseArray
//
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
//
// - SequenceSeq2: Works with iterators (Seq2)
//
// - Input: Iterator yielding (name, assertion) pairs
//
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
//
// - Memory: More efficient for large test suites (lazy evaluation)
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + transformation function
//
// - Use when: You have an array of test data
//
// - Memory: All test data must be in memory
//
// # Comparison with RunAll
//
// SequenceSeq2 and [RunAll] are very similar:
//
// - SequenceSeq2: Takes an iterator (Seq2)
// - RunAll: Takes a map[string]Reader
//
// Both execute named test cases as subtests. Choose based on your data structure:
// use SequenceSeq2 for iterators, RunAll for maps.
//
// # Related Functions
//
// - [TraverseArray]: Similar but works with arrays instead of iterators
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Go iterators: https://go.dev/blog/range-functions
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
return func(t *testing.T) bool {
ok := true
for name, test := range s {
res := t.Run(name, func(t *testing.T) {
test(t)
})
ok = ok && res
}
return ok
}
}
// TraverseRecord transforms a map of values into a test suite by applying a function
// that generates test assertions for each map entry.
//
// This function enables data-driven testing where you have a map of test data and want
// to run a named subtest for each entry. The map keys become test names, and the function
// transforms each value into a test assertion. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each key-value pair from the map, applies the provided function to
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
// All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
//
// # Returns
//
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
// - Executes each map entry as a named subtest (using the key as the test name)
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with named test cases in a map
// - Testing configuration maps where keys are meaningful names
// - Validating collections where natural keys exist
// - Property-based testing with named scenarios
//
// # Example - Basic Configuration Testing
//
// func TestConfigurations(t *testing.T) {
// configs := map[string]int{
// "timeout": 30,
// "maxRetries": 3,
// "bufferSize": 1024,
// }
//
// validatePositive := assert.That(func(n int) bool { return n > 0 })
//
// traverse := assert.TraverseRecord(validatePositive)
// traverse(configs)(t)
// }
//
// # Example - User Validation
//
// func TestUserMap(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// users := map[string]User{
// "alice": {Name: "Alice", Age: 30},
// "bob": {Name: "Bob", Age: 25},
// "carol": {Name: "Carol", Age: 35},
// }
//
// validateUser := func(u User) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
// })
// }
//
// traverse := assert.TraverseRecord(validateUser)
// traverse(users)(t)
// }
//
// # Example - API Endpoint Testing
//
// func TestEndpoints(t *testing.T) {
// type Endpoint struct {
// Path string
// Method string
// }
//
// endpoints := map[string]Endpoint{
// "get_users": {Path: "/api/users", Method: "GET"},
// "create_user": {Path: "/api/users", Method: "POST"},
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
// }
//
// validateEndpoint := func(e Endpoint) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(e.Path),
// assert.That(func(path string) bool {
// return strings.HasPrefix(path, "/api/")
// })(e.Path),
// assert.That(func(method string) bool {
// return method == "GET" || method == "POST" ||
// method == "PUT" || method == "DELETE"
// })(e.Method),
// })
// }
//
// traverse := assert.TraverseRecord(validateEndpoint)
// traverse(endpoints)(t)
// }
//
// # Comparison with TraverseArray
//
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
//
// - TraverseRecord: Works with maps (records)
//
// - Input: Map with string keys + transformation function
//
// - Use when: You have named test data in a map
//
// - Test names: Derived from map keys
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + function that generates names and assertions
//
// - Use when: You have sequential test data
//
// - Test names: Generated by the transformation function
//
// # Comparison with SequenceRecord
//
// TraverseRecord and [SequenceRecord] are closely related:
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader
//
// - Use when: Assertions are already defined
//
// # Related Functions
//
// - [SequenceRecord]: Similar but takes pre-defined assertions
// - [TraverseArray]: Similar but works with arrays
// - [RunAll]: Alias for SequenceRecord
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
return func(m map[string]T) Reader {
return func(t *testing.T) bool {
ok := true
for name, src := range m {
res := t.Run(name, func(t *testing.T) {
f(src)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceRecord executes a map of named test cases as subtests.
//
// This function takes a map where keys are test names and values are test assertions
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
// convenience.
//
// The function iterates through all map entries, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - m: A map[string]Reader where:
// - Keys: Test names (strings) for the subtests
// - Values: Test assertions ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each map entry as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Executing a collection of pre-defined named test cases
// - Organizing related tests in a map structure
// - Running multiple assertions with descriptive names
// - Building test suites programmatically
//
// # Example - Basic Named Tests
//
// func TestMathOperations(t *testing.T) {
// tests := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "subtraction": assert.Equal(1)(3 - 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "division": assert.Equal(2)(6 / 3),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - String Validation Suite
//
// func TestStringValidations(t *testing.T) {
// testString := "hello world"
//
// tests := map[string]assert.Reader{
// "not_empty": assert.StringNotEmpty(testString),
// "correct_length": assert.StringLength[any, any](11)(testString),
// "has_space": assert.That(func(s string) bool {
// return strings.Contains(s, " ")
// })(testString),
// "lowercase": assert.That(func(s string) bool {
// return s == strings.ToLower(s)
// })(testString),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Complex Object Validation
//
// func TestUserValidation(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
//
// tests := map[string]assert.Reader{
// "name_not_empty": assert.StringNotEmpty(user.Name),
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
// "email_valid": assert.That(func(email string) bool {
// return strings.Contains(email, "@") && strings.Contains(email, ".")
// })(user.Email),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Array Validation Suite
//
// func TestArrayValidations(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
//
// tests := map[string]assert.Reader{
// "not_empty": assert.ArrayNotEmpty(numbers),
// "correct_length": assert.ArrayLength[int](5)(numbers),
// "contains_three": assert.ArrayContains(3)(numbers),
// "all_positive": assert.That(func(arr []int) bool {
// for _, n := range arr {
// if n <= 0 {
// return false
// }
// }
// return true
// })(numbers),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Comparison with TraverseRecord
//
// SequenceRecord and [TraverseRecord] are closely related:
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader (assertions already created)
//
// - Use when: You have already defined test cases with assertions
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// # Comparison with SequenceSeq2
//
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
//
// - SequenceRecord: Works with maps
//
// - Input: map[string]Reader
//
// - Use when: You have named test cases in a map
//
// - Iteration order: Non-deterministic (map iteration)
//
// - SequenceSeq2: Works with iterators
//
// - Input: Seq2[string, Reader]
//
// - Use when: You have test cases in an iterator
//
// - Iteration order: Deterministic (iterator order)
//
// # Note on Map Iteration Order
//
// Go maps have non-deterministic iteration order. If test execution order matters,
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
// or use [TraverseArray] with a slice of test cases.
//
// # Related Functions
//
// - [RunAll]: Alias for SequenceRecord
// - [TraverseRecord]: Similar but transforms values into assertions
// - [SequenceSeq2]: Similar but works with iterators
// - [TraverseArray]: Similar but works with arrays
//
// # References
//
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceRecord(m map[string]Reader) Reader {
return TraverseRecord(reader.Ask[Reader]())(m)
}

960
v2/assert/traverse_test.go Normal file
View File

@@ -0,0 +1,960 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/pair"
)
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
func TestTraverseArray_EmptyArray(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n)(n),
)
})
result := traverse([]int{})(t)
if !result {
t.Error("Expected TraverseArray to pass with empty array")
}
}
// TestTraverseArray_SingleElement tests TraverseArray with a single element
func TestTraverseArray_SingleElement(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n*2)(n*2),
)
})
result := traverse([]int{5})(t)
if !result {
t.Error("Expected TraverseArray to pass with single element")
}
}
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
func TestTraverseArray_MultipleElements(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square_%d", n),
Equal(n*n)(n*n),
)
})
result := traverse([]int{1, 2, 3, 4, 5})(t)
if !result {
t.Error("Expected TraverseArray to pass with all passing elements")
}
}
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
func TestTraverseArray_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(10)(n), // Will fail for all except 10
)
})
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
result := traverse([]int{1, 2, 3})(t)
// The traverse should return false because assertions fail
if result {
t.Error("Expected traverse to return false when elements don't match")
}
}
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
func TestTraverseArray_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("is_even_%d", n),
Equal(0)(n%2), // Only passes for even numbers
)
})
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
// The traverse should return false because one assertion fails
if result {
t.Error("Expected traverse to return false when some elements fail")
}
}
// TestTraverseArray_StringData tests TraverseArray with string data
func TestTraverseArray_StringData(t *testing.T) {
words := []string{"hello", "world", "test"}
traverse := TraverseArray(func(s string) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("validate_%s", s),
AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
}),
)
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid strings")
}
}
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
func TestTraverseArray_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
}),
)
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid users")
}
}
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "", Age: 25}, // Invalid: empty name
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
}),
)
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testCases := []TestCase{
{Input: 2, Expected: 4},
{Input: 3, Expected: 9},
{Input: 4, Expected: 16},
{Input: 5, Expected: 25},
}
square := func(n int) int { return n * n }
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
Equal(tc.Expected)(square(tc.Input)),
)
})
result := traverse(testCases)(t)
if !result {
t.Error("Expected all test cases to pass")
}
}
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
func TestSequenceSeq2_EmptySequence(t *testing.T) {
emptySeq := func(yield func(string, Reader) bool) {
// Empty - yields nothing
}
result := SequenceSeq2[Reader](emptySeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with empty sequence")
}
}
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
func TestSequenceSeq2_SingleTest(t *testing.T) {
singleSeq := func(yield func(string, Reader) bool) {
yield("test_one", Equal(42)(42))
}
result := SequenceSeq2[Reader](singleSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with single test")
}
}
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
func TestSequenceSeq2_MultipleTests(t *testing.T) {
multiSeq := func(yield func(string, Reader) bool) {
if !yield("test_addition", Equal(4)(2+2)) {
return
}
if !yield("test_subtraction", Equal(1)(3-2)) {
return
}
if !yield("test_multiplication", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](multiSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with all passing tests")
}
}
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
func TestSequenceSeq2_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
failSeq := func(yield func(string, Reader) bool) {
if !yield("test_pass", Equal(4)(2+2)) {
return
}
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
return
}
if !yield("test_pass2", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](failSeq)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
generateTests := func(yield func(string, Reader) bool) {
for i := 1; i <= 5; i++ {
name := fmt.Sprintf("test_%d", i)
assertion := Equal(i * i)(i * i)
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](generateTests)(t)
if !result {
t.Error("Expected all generated tests to pass")
}
}
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
func TestSequenceSeq2_StringTests(t *testing.T) {
stringSeq := func(yield func(string, Reader) bool) {
if !yield("test_hello", StringNotEmpty("hello")) {
return
}
if !yield("test_world", StringNotEmpty("world")) {
return
}
if !yield("test_length", StringLength[any, any](5)("hello")) {
return
}
}
result := SequenceSeq2[Reader](stringSeq)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
func TestSequenceSeq2_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
arraySeq := func(yield func(string, Reader) bool) {
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
return
}
if !yield("test_length", ArrayLength[int](5)(arr)) {
return
}
if !yield("test_contains", ArrayContains(3)(arr)) {
return
}
}
result := SequenceSeq2[Reader](arraySeq)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
userSeq := func(yield func(string, Reader) bool) {
if !yield("test_name", StringNotEmpty(user.Name)) {
return
}
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
return
}
if !yield("test_email", That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(user.Email)) {
return
}
}
result := SequenceSeq2[Reader](userSeq)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
executionCount := 0
earlyTermSeq := func(yield func(string, Reader) bool) {
executionCount++
if !yield("test_1", Equal(1)(1)) {
return
}
executionCount++
if !yield("test_2", Equal(2)(2)) {
return
}
executionCount++
// This should execute even though we don't check the return
yield("test_3", Equal(3)(3))
executionCount++
}
SequenceSeq2[Reader](earlyTermSeq)(t)
// All iterations should execute since we're not terminating early
if executionCount != 4 {
t.Errorf("Expected 4 executions, got %d", executionCount)
}
}
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
testMap := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_multiplication": Equal(6)(2 * 3),
"test_subtraction": Equal(1)(3 - 2),
}
// Convert map to Seq2
mapSeq := func(yield func(string, Reader) bool) {
for name, assertion := range testMap {
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](mapSeq)(t)
if !result {
t.Error("Expected all map-based tests to pass")
}
}
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
type TestCase struct {
Name string
Input int
Expected int
}
testCases := []TestCase{
{Name: "test_1", Input: 2, Expected: 4},
{Name: "test_2", Input: 3, Expected: 9},
{Name: "test_3", Input: 4, Expected: 16},
}
// Using TraverseArray
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
})(testCases)(t)
// Using SequenceSeq2
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
for _, tc := range testCases {
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
return
}
}
})(t)
if traverseResult != seqResult {
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
func TestTraverseRecord_EmptyMap(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n)(n)
})
result := traverse(map[string]int{})(t)
if !result {
t.Error("Expected TraverseRecord to pass with empty map")
}
}
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
func TestTraverseRecord_SingleEntry(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * 2)(n * 2)
})
result := traverse(map[string]int{"test_5": 5})(t)
if !result {
t.Error("Expected TraverseRecord to pass with single entry")
}
}
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
func TestTraverseRecord_MultipleEntries(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * n)(n * n)
})
result := traverse(map[string]int{
"square_1": 1,
"square_2": 2,
"square_3": 3,
"square_4": 4,
"square_5": 5,
})(t)
if !result {
t.Error("Expected TraverseRecord to pass with all passing entries")
}
}
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
func TestTraverseRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(10)(n) // Will fail for all except 10
})
result := traverse(map[string]int{
"test_1": 1,
"test_2": 2,
"test_3": 3,
})(t)
// The traverse should return false because entries don't match
if result {
t.Error("Expected traverse to return false when entries don't match")
}
}
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
func TestTraverseRecord_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(0)(n % 2) // Only passes for even numbers
})
result := traverse(map[string]int{
"even_2": 2,
"odd_3": 3,
"even_4": 4,
})(t)
// The traverse should return false because some entries fail
if result {
t.Error("Expected traverse to return false when some entries fail")
}
}
// TestTraverseRecord_StringData tests TraverseRecord with string data
func TestTraverseRecord_StringData(t *testing.T) {
words := map[string]string{
"greeting": "hello",
"world": "world",
"test": "test",
}
traverse := TraverseRecord(func(s string) Reader {
return AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
})
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid strings")
}
}
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
func TestTraverseRecord_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"bob": {Name: "Bob", Age: 25},
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
})
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid users")
}
}
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"invalid": {Name: "", Age: 25}, // Invalid: empty name
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
})
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
configs := map[string]int{
"timeout": 30,
"maxRetries": 3,
"bufferSize": 1024,
}
validatePositive := That(func(n int) bool { return n > 0 })
traverse := TraverseRecord(validatePositive)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configuration values to be positive")
}
}
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
type Endpoint struct {
Path string
Method string
}
endpoints := map[string]Endpoint{
"get_users": {Path: "/api/users", Method: "GET"},
"create_user": {Path: "/api/users", Method: "POST"},
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
}
validateEndpoint := func(e Endpoint) Reader {
return AllOf([]Reader{
StringNotEmpty(e.Path),
That(func(path string) bool {
return len(path) > 0 && path[0] == '/'
})(e.Path),
That(func(method string) bool {
return method == "GET" || method == "POST" ||
method == "PUT" || method == "DELETE"
})(e.Method),
})
}
traverse := TraverseRecord(validateEndpoint)
result := traverse(endpoints)(t)
if !result {
t.Error("Expected all endpoints to be valid")
}
}
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
func TestSequenceRecord_EmptyMap(t *testing.T) {
result := SequenceRecord(map[string]Reader{})(t)
if !result {
t.Error("Expected SequenceRecord to pass with empty map")
}
}
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
func TestSequenceRecord_SingleTest(t *testing.T) {
tests := map[string]Reader{
"test_one": Equal(42)(42),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with single test")
}
}
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
func TestSequenceRecord_MultipleTests(t *testing.T) {
tests := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_subtraction": Equal(1)(3 - 2),
"test_multiplication": Equal(6)(2 * 3),
"test_division": Equal(2)(6 / 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with all passing tests")
}
}
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
func TestSequenceRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
tests := map[string]Reader{
"test_pass": Equal(4)(2 + 2),
"test_fail": Equal(5)(2 + 2), // This will fail
"test_pass2": Equal(6)(2 * 3),
}
result := SequenceRecord(tests)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
func TestSequenceRecord_StringTests(t *testing.T) {
testString := "hello world"
tests := map[string]Reader{
"not_empty": StringNotEmpty(testString),
"correct_length": StringLength[any, any](11)(testString),
"has_space": That(func(s string) bool {
for _, ch := range s {
if ch == ' ' {
return true
}
}
return false
})(testString),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
func TestSequenceRecord_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"not_empty": ArrayNotEmpty(arr),
"correct_length": ArrayLength[int](5)(arr),
"contains_three": ArrayContains(3)(arr),
"all_positive": That(func(arr []int) bool {
for _, n := range arr {
if n <= 0 {
return false
}
}
return true
})(arr),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
tests := map[string]Reader{
"name_not_empty": StringNotEmpty(user.Name),
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
"email_valid": That(func(email string) bool {
hasAt := false
hasDot := false
for _, ch := range email {
if ch == '@' {
hasAt = true
}
if ch == '.' {
hasDot = true
}
}
return hasAt && hasDot
})(user.Email),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
func TestSequenceRecord_MathOperations(t *testing.T) {
tests := map[string]Reader{
"addition": Equal(4)(2 + 2),
"subtraction": Equal(1)(3 - 2),
"multiplication": Equal(6)(2 * 3),
"division": Equal(2)(6 / 3),
"modulo": Equal(1)(7 % 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all math operations to pass")
}
}
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
func TestSequenceRecord_BooleanTests(t *testing.T) {
tests := map[string]Reader{
"true_is_true": Equal(true)(true),
"false_is_false": Equal(false)(false),
"not_true": Equal(false)(!true),
"not_false": Equal(true)(!false),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all boolean tests to pass")
}
}
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
func TestSequenceRecord_ErrorTests(t *testing.T) {
tests := map[string]Reader{
"no_error": NoError(nil),
"equal_value": Equal("test")("test"),
"not_empty": StringNotEmpty("hello"),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all error tests to pass")
}
}
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testData := map[string]TestCase{
"test_1": {Input: 2, Expected: 4},
"test_2": {Input: 3, Expected: 9},
"test_3": {Input: 4, Expected: 16},
}
// Using TraverseRecord
traverseResult := TraverseRecord(func(tc TestCase) Reader {
return Equal(tc.Expected)(tc.Input * tc.Input)
})(testData)(t)
// Using SequenceRecord (manually creating the map)
tests := make(map[string]Reader)
for name, tc := range testData {
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
}
seqResult := SequenceRecord(tests)(t)
if traverseResult != seqResult {
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
func TestSequenceRecord_WithAllOf(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"array_validations": AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
}),
"element_checks": AllOf([]Reader{
That(func(a []int) bool { return a[0] == 1 })(arr),
That(func(a []int) bool { return a[4] == 5 })(arr),
}),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected combined assertions to pass")
}
}
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
func TestTraverseRecord_ConfigValidation(t *testing.T) {
type Config struct {
Value int
Min int
Max int
}
configs := map[string]Config{
"timeout": {Value: 30, Min: 1, Max: 60},
"maxRetries": {Value: 3, Min: 1, Max: 10},
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
}
validateConfig := func(c Config) Reader {
return AllOf([]Reader{
That(func(val int) bool { return val >= c.Min })(c.Value),
That(func(val int) bool { return val <= c.Max })(c.Value),
})
}
traverse := TraverseRecord(validateConfig)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configurations to be within valid ranges")
}
}
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
func TestSequenceRecord_RealWorldExample(t *testing.T) {
type Response struct {
StatusCode int
Body string
}
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
tests := map[string]Reader{
"status_ok": Equal(200)(response.StatusCode),
"body_not_empty": StringNotEmpty(response.Body),
"body_is_json": That(func(s string) bool {
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
})(response.Body),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected response validation to pass")
}
}

View File

@@ -1,11 +1,32 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"iter"
"testing"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -13,23 +34,506 @@ import (
type (
// Result represents a computation that may fail with an error.
//
// This is an alias for [result.Result][T], which encapsulates either a successful
// value of type T or an error. It's commonly used in test assertions to represent
// operations that might fail, allowing for functional error handling without exceptions.
//
// A Result can be in one of two states:
// - Success: Contains a value of type T
// - Failure: Contains an error
//
// This type is particularly useful in testing scenarios where you need to:
// - Test functions that return results
// - Chain operations that might fail
// - Handle errors functionally
//
// Example:
//
// func TestResultHandling(t *testing.T) {
// successResult := result.Of[int](42)
// assert.Success(successResult)(t) // Passes
//
// failureResult := result.Error[int](errors.New("failed"))
// assert.Failure(failureResult)(t) // Passes
// }
//
// See also:
// - [Success]: Asserts a Result is successful
// - [Failure]: Asserts a Result contains an error
// - [result.Result]: The underlying Result type
Result[T any] = result.Result[T]
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
//
// This is the core type for all assertions in this package. It's an alias for
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
// and produces a boolean result indicating whether the assertion passed.
//
// The Reader pattern enables:
// - Composable assertions that can be combined using functional operators
// - Deferred execution - assertions are defined but not executed until applied to a test
// - Reusable assertion logic that can be applied to multiple tests
// - Functional composition of complex test conditions
//
// All assertion functions in this package return a Reader, which must be applied
// to a *testing.T to execute the assertion:
//
// assertion := assert.Equal(42)(result) // Creates a Reader
// assertion(t) // Executes the assertion
//
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
// functional operators from the reader package.
//
// Example:
//
// func TestReaderComposition(t *testing.T) {
// // Create individual assertions
// assertion1 := assert.Equal(42)(42)
// assertion2 := assert.StringNotEmpty("hello")
//
// // Combine them
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
//
// // Execute the combined assertion
// combined(t)
// }
//
// See also:
// - [Kleisli]: Function that produces a Reader from a value
// - [AllOf]: Combines multiple Readers
// - [ApplicativeMonoid]: Monoid for combining Readers
Reader = reader.Reader[*testing.T, bool]
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
//
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
// "data last" principle used throughout this package.
//
// Kleisli functions enable:
// - Partial application of assertions - configure the expected value first, apply actual value later
// - Reusable assertion builders that can be applied to different values
// - Functional composition of assertion pipelines
// - Point-free style programming with assertions
//
// Most assertion functions in this package return a Kleisli, which must be applied
// to the actual value being tested, and then to a *testing.T:
//
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
// reader := kleisli(result) // Reader - assertion ready to execute
// reader(t) // Execute the assertion
//
// Or more concisely:
//
// assert.Equal(42)(result)(t)
//
// Example:
//
// func TestKleisliPattern(t *testing.T) {
// // Create a reusable assertion for positive numbers
// isPositive := assert.That(func(n int) bool { return n > 0 })
//
// // Apply it to different values
// isPositive(42)(t) // Passes
// isPositive(100)(t) // Passes
// // isPositive(-5)(t) would fail
//
// // Can be used with Local for property testing
// type User struct { Age int }
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
// checkAge(User{Age: 25})(t) // Passes
// }
//
// See also:
// - [Reader]: The assertion type produced by Kleisli
// - [Local]: Focuses a Kleisli on a property of a larger structure
Kleisli[T any] = reader.Reader[T, Reader]
// Predicate represents a function that tests a value of type T and returns a boolean.
//
// This is an alias for [predicate.Predicate][T], which is a simple function that
// takes a value and returns true or false based on some condition. Predicates are
// used with the [That] function to create custom assertions.
//
// Predicates enable:
// - Custom validation logic for any type
// - Reusable test conditions
// - Composition of complex validation rules
// - Integration with functional programming patterns
//
// Example:
//
// func TestPredicates(t *testing.T) {
// // Simple predicate
// isEven := func(n int) bool { return n%2 == 0 }
// assert.That(isEven)(42)(t) // Passes
//
// // String predicate
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
// assert.That(hasPrefix)("test_file.go")(t) // Passes
//
// // Complex predicate
// isValidEmail := func(s string) bool {
// return strings.Contains(s, "@") && strings.Contains(s, ".")
// }
// assert.That(isValidEmail)("user@example.com")(t) // Passes
// }
//
// See also:
// - [That]: Creates an assertion from a Predicate
// - [predicate.Predicate]: The underlying predicate type
Predicate[T any] = predicate.Predicate[T]
// Lens is a functional reference to a subpart of a data structure.
//
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
// on a specific field within a larger structure. Lenses enable getting and setting
// values in nested data structures in a functional, immutable way.
//
// In the context of testing, lenses are used with [LocalL] to focus assertions
// on specific properties of complex objects without manually extracting those properties.
//
// A Lens[S, T] focuses on a value of type T within a structure of type S.
//
// Example:
//
// func TestLensUsage(t *testing.T) {
// type Address struct { City string }
// type User struct { Name string; Address Address }
//
// // Define lenses (typically generated)
// addressLens := lens.Lens[User, Address]{...}
// cityLens := lens.Lens[Address, string]{...}
//
// // Compose lenses to focus on nested field
// userCityLens := lens.Compose(addressLens, cityLens)
//
// // Use with LocalL to assert on nested property
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
// }
//
// See also:
// - [LocalL]: Uses a Lens to focus assertions on a property
// - [lens.Lens]: The underlying lens type
// - [Optional]: Similar but for values that may not exist
Lens[S, T any] = lens.Lens[S, T]
// Optional is an optic that focuses on a value that may or may not be present.
//
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
// handles cases where the focused value might not exist. Optionals are useful for
// working with nullable fields, optional properties, or values that might be absent.
//
// In testing, Optionals are used with [FromOptional] to create assertions that
// verify whether an optional value is present and, if so, whether it satisfies
// certain conditions.
//
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
//
// Example:
//
// func TestOptionalUsage(t *testing.T) {
// type Config struct { Timeout *int }
//
// // Define optional (typically generated)
// timeoutOptional := optional.Optional[Config, int]{...}
//
// // Test when value is present
// config1 := Config{Timeout: ptr(30)}
// assert.FromOptional(timeoutOptional)(
// assert.Equal(30),
// )(config1)(t) // Passes
//
// // Test when value is absent
// config2 := Config{Timeout: nil}
// // FromOptional would fail because value is not present
// }
//
// See also:
// - [FromOptional]: Creates assertions for optional values
// - [optional.Optional]: The underlying optional type
// - [Lens]: Similar but for values that always exist
Optional[S, T any] = optional.Optional[S, T]
// Prism is an optic that focuses on a case of a sum type.
//
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
// matching and extraction of values from sum types in a functional way.
//
// In testing, Prisms are used with [FromPrism] to create assertions that verify
// whether a value matches a specific case and, if so, whether the contained value
// satisfies certain conditions.
//
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
//
// Example:
//
// func TestPrismUsage(t *testing.T) {
// // Prism for extracting success value from Result
// successPrism := prism.Success[int]()
//
// // Test successful result
// successResult := result.Of[int](42)
// assert.FromPrism(successPrism)(
// assert.Equal(42),
// )(successResult)(t) // Passes
//
// // Prism for extracting error from Result
// failurePrism := prism.Failure[int]()
//
// // Test failed result
// failureResult := result.Error[int](errors.New("failed"))
// assert.FromPrism(failurePrism)(
// assert.Error,
// )(failureResult)(t) // Passes
// }
//
// See also:
// - [FromPrism]: Creates assertions for prism-focused values
// - [prism.Prism]: The underlying prism type
// - [Optional]: Similar but for optional values
Prism[S, T any] = prism.Prism[S, T]
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
//
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
// computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like file I/O, network calls)
// - Result: May fail with an error
//
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
// context-aware, effectful computations into test assertions. This is useful
// when your test assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations (database queries, API calls, file access)
// - Handle potential errors gracefully
//
// Example:
//
// func TestReaderIOResult(t *testing.T) {
// // Create a ReaderIOResult that performs IO and may fail
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Perform database check with context
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
//
// See also:
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
// - [ReaderIO]: Similar but without error handling
// - [readerioresult.ReaderIOResult]: The underlying type
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// ReaderIO represents a context-aware, IO-based computation.
//
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like logging, metrics)
//
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
// effectful computations into test assertions. This is useful when your test
// assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware utilities
//
// Example:
//
// func TestReaderIO(t *testing.T) {
// // Create a ReaderIO that performs IO
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log with context
// logger.InfoContext(ctx, "Running test")
// // Return assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
//
// See also:
// - [FromReaderIO]: Converts ReaderIO to Reader
// - [ReaderIOResult]: Similar but with error handling
// - [readerio.ReaderIO]: The underlying type
ReaderIO[A any] = readerio.ReaderIO[A]
// Seq2 represents a Go iterator that yields key-value pairs.
//
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
// iterated over using a for-range loop.
//
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
// test cases provided as an iterator. This enables:
// - Lazy evaluation of test cases
// - Memory-efficient testing of large test suites
// - Integration with Go's iterator patterns
// - Dynamic generation of test cases
//
// Example:
//
// func TestSeq2Usage(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// // Execute all test cases
// assert.SequenceSeq2[assert.Reader](testCases)(t)
// }
//
// See also:
// - [SequenceSeq2]: Executes a Seq2 of test cases
// - [TraverseArray]: Similar but for arrays
// - [iter.Seq2]: The underlying iterator type
Seq2[K, A any] = iter.Seq2[K, A]
// Pair represents a tuple of two values with potentially different types.
//
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
// related values together without defining a custom struct.
//
// In testing, Pairs are used with [TraverseArray] to associate test names with
// their corresponding assertions. Each element in the array is transformed into
// a Pair[string, Reader] where the string is the test name and the Reader is
// the assertion to execute.
//
// Example:
//
// func TestPairUsage(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// }
//
// // Transform each test case into a named assertion
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// See also:
// - [TraverseArray]: Uses Pairs to create named test cases
// - [pair.Pair]: The underlying pair type
// - [pair.MakePair]: Creates a Pair
// - [pair.Head]: Extracts the first value
// - [pair.Tail]: Extracts the second value
Pair[L, R any] = pair.Pair[L, R]
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
//
// This is an alias for [function.Void], which is used to represent operations that don't
// return a meaningful value but may perform side effects. In the context of testing, Void
// is used with IO operations that perform actions without producing a result.
//
// Void is conceptually similar to:
// - Unit type in functional languages (Haskell's (), Scala's Unit)
// - void in languages like C/Java (but as a value, not just a type)
// - Empty struct{} in Go (but with clearer semantic meaning)
//
// Example:
//
// func TestWithSideEffect(t *testing.T) {
// // An IO operation that logs but returns Void
// logOperation := func() function.Void {
// log.Println("Test executed")
// return function.Void{}
// }
//
// // Execute the operation
// logOperation()
// }
//
// See also:
// - [IO]: Wraps side-effecting operations
// - [function.Void]: The underlying void type
Void = function.Void
// IO represents a side-effecting computation that produces a value of type A.
//
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
// computation - it describes an effect but doesn't execute it until explicitly run.
//
// In testing, IO is used to:
// - Defer execution of side effects until needed
// - Compose multiple side-effecting operations
// - Maintain referential transparency in test setup
// - Separate effect description from effect execution
//
// An IO[A] is essentially a function `func() A` that:
// - Encapsulates a side effect
// - Returns a value of type A when executed
// - Can be composed with other IO operations
//
// Example:
//
// func TestIOOperation(t *testing.T) {
// // Define an IO operation that reads a file
// readConfig := func() io.IO[string] {
// return func() string {
// data, _ := os.ReadFile("config.txt")
// return string(data)
// }
// }
//
// // The IO is not executed yet - it's just a description
// configIO := readConfig()
//
// // Execute the IO to get the result
// config := configIO()
// assert.StringNotEmpty(config)(t)
// }
//
// Example with composition:
//
// func TestIOComposition(t *testing.T) {
// // Chain multiple IO operations
// pipeline := io.Map(
// func(s string) int { return len(s) },
// )(readFileIO)
//
// // Execute the composed operation
// length := pipeline()
// assert.That(func(n int) bool { return n > 0 })(length)(t)
// }
//
// See also:
// - [ReaderIO]: Combines Reader and IO effects
// - [ReaderIOResult]: Adds error handling to ReaderIO
// - [io.IO]: The underlying IO type
// - [Void]: Represents operations without meaningful return values
IO[A any] = io.IO[A]
)

273
v2/cli/README.md Normal file
View File

@@ -0,0 +1,273 @@
# CLI Package - Functional Wrappers for urfave/cli/v3
This package provides functional programming wrappers for the `github.com/urfave/cli/v3` library, enabling Effect-based command actions and type-safe flag handling through Prisms.
## Features
### 1. Effect-Based Command Actions
Transform CLI command actions into composable Effects that follow functional programming principles.
#### Key Functions
- **`ToAction(effect CommandEffect) func(context.Context, *C.Command) error`**
- Converts a CommandEffect into a standard urfave/cli Action function
- Enables Effect-based command handlers to work with cli/v3 framework
- **`FromAction(action func(context.Context, *C.Command) error) CommandEffect`**
- Lifts existing cli/v3 action handlers into the Effect type
- Allows gradual migration to functional style
- **`MakeCommand(name, usage string, flags []C.Flag, effect CommandEffect) *C.Command`**
- Creates a new Command with an Effect-based action
- Convenience function combining command creation with Effect conversion
- **`MakeCommandWithSubcommands(...) *C.Command`**
- Creates a Command with subcommands and an Effect-based action
#### Example Usage
```go
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Define an Effect-based command action
processEffect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process input...
return R.Of(F.Void{})
}
}
}
// Create command with Effect
command := cli.MakeCommand(
"process",
"Process input files",
[]C.Flag{
&C.StringFlag{Name: "input", Usage: "Input file path"},
},
processEffect,
)
// Or convert existing action to Effect
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Existing logic...
return nil
}
effect := cli.FromAction(existingAction)
```
### 2. Flag Type Prisms
Type-safe extraction and manipulation of CLI flags using Prisms from the optics package.
#### Available Prisms
- `StringFlagPrism()` - Extract `*C.StringFlag` from `C.Flag`
- `IntFlagPrism()` - Extract `*C.IntFlag` from `C.Flag`
- `BoolFlagPrism()` - Extract `*C.BoolFlag` from `C.Flag`
- `Float64FlagPrism()` - Extract `*C.Float64Flag` from `C.Flag`
- `DurationFlagPrism()` - Extract `*C.DurationFlag` from `C.Flag`
- `TimestampFlagPrism()` - Extract `*C.TimestampFlag` from `C.Flag`
- `StringSliceFlagPrism()` - Extract `*C.StringSliceFlag` from `C.Flag`
- `IntSliceFlagPrism()` - Extract `*C.IntSliceFlag` from `C.Flag`
- `Float64SliceFlagPrism()` - Extract `*C.Float64SliceFlag` from `C.Flag`
- `UintFlagPrism()` - Extract `*C.UintFlag` from `C.Flag`
- `Uint64FlagPrism()` - Extract `*C.Uint64Flag` from `C.Flag`
- `Int64FlagPrism()` - Extract `*C.Int64Flag` from `C.Flag`
#### Example Usage
```go
import (
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Extract a StringFlag from a Flag interface
var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
prism := cli.StringFlagPrism()
// Safe extraction returns Option
result := prism.GetOption(flag)
if O.IsSome(result) {
strFlag := O.MonadFold(result,
func() *C.StringFlag { return nil },
func(f *C.StringFlag) *C.StringFlag { return f },
)
// Use strFlag...
}
// Type mismatch returns None
var intFlag C.Flag = &C.IntFlag{Name: "count"}
result = prism.GetOption(intFlag) // Returns None
// Convert back to Flag
strFlag := &C.StringFlag{Name: "output"}
flag = prism.ReverseGet(strFlag)
```
## Type Definitions
### CommandEffect
```go
type CommandEffect = E.Effect[*C.Command, F.Void]
```
A CommandEffect represents a CLI command action as an Effect. It takes a `*C.Command` as context and produces a result wrapped in the Effect monad.
The Effect structure is:
```
func(*C.Command) -> func(context.Context) -> func() -> Result[Void]
```
This allows for:
- **Composability**: Effects can be composed using standard functional combinators
- **Testability**: Pure functions are easier to test
- **Error Handling**: Errors are explicitly represented in the Result type
- **Context Management**: Context flows naturally through the Effect
## Benefits
### 1. Functional Composition
Effects can be composed using standard functional programming patterns:
```go
import (
F "github.com/IBM/fp-go/v2/function"
RRIOE "github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Compose multiple effects
validateInput := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
processData := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
saveResults := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
// Chain effects together
pipeline := F.Pipe3(
validateInput,
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return processData }),
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return saveResults }),
)
```
### 2. Type Safety
Prisms provide compile-time type safety when working with flags:
```go
// Type-safe flag extraction
flags := []C.Flag{
&C.StringFlag{Name: "input"},
&C.IntFlag{Name: "count"},
}
for _, flag := range flags {
// Safe extraction with pattern matching
O.MonadFold(
cli.StringFlagPrism().GetOption(flag),
func() { /* Not a string flag */ },
func(sf *C.StringFlag) { /* Handle string flag */ },
)
}
```
### 3. Error Handling
Errors are explicitly represented in the Result type:
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
if err := validateInput(cmd); err != nil {
return R.Left[F.Void](err) // Explicit error
}
return R.Of(F.Void{}) // Success
}
}
}
```
### 4. Testability
Pure functions are easier to test:
```go
func TestCommandEffect(t *testing.T) {
cmd := &C.Command{Name: "test"}
effect := myCommandEffect(cmd)
// Execute effect
result := effect(context.Background())()
// Assert on result
assert.True(t, R.IsRight(result))
}
```
## Migration Guide
### From Standard Actions to Effects
**Before:**
```go
command := &C.Command{
Name: "process",
Action: func(ctx context.Context, cmd *C.Command) error {
input := cmd.String("input")
// Process...
return nil
},
}
```
**After:**
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process...
return R.Of(F.Void{})
}
}
}
command := cli.MakeCommand("process", "Process files", flags, effect)
```
### Gradual Migration
You can mix both styles during migration:
```go
// Wrap existing action
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Legacy code...
return nil
}
// Use as Effect
effect := cli.FromAction(existingAction)
command := cli.MakeCommand("legacy", "Legacy command", flags, effect)
```
## See Also
- [Effect Package](../effect/) - Core Effect type definitions
- [Optics Package](../optics/) - Prism and other optics
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateTraverseTuple(f *os.File, i int) {
@@ -387,8 +387,8 @@ func generateApplyHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -422,10 +422,10 @@ func ApplyCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateApplyHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func createCombinations(n int, all, prev []int) [][]int {
@@ -265,8 +265,8 @@ func generateBindHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)
@@ -284,10 +284,10 @@ func BindCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateBindHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func Commands() []*C.Command {

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// Deprecated:
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateContextReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateMakeProvider(f *os.File, i int) {
@@ -188,8 +188,8 @@ func generateDIHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -221,10 +221,10 @@ func DICommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateDIHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

199
v2/cli/effect.go Normal file
View File

@@ -0,0 +1,199 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
R "github.com/IBM/fp-go/v2/result"
C "github.com/urfave/cli/v3"
)
// CommandEffect represents a CLI command action as an Effect.
// The Effect takes a *C.Command as context and produces a result.
type CommandEffect = E.Effect[*C.Command, F.Void]
// ToAction converts a CommandEffect into a standard urfave/cli Action function.
// This allows Effect-based command handlers to be used with the cli/v3 framework.
//
// The conversion process:
// 1. Takes the Effect which expects a *C.Command context
// 2. Executes it with the provided command
// 3. Runs the resulting IO operation
// 4. Converts the Result to either nil (success) or error (failure)
//
// # Parameters
//
// - effect: The CommandEffect to convert
//
// # Returns
//
// - A function compatible with C.Command.Action signature
//
// # Example Usage
//
// effect := func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// // Command logic here
// return R.Of(F.Void{})
// }
// }
// }
// action := ToAction(effect)
// command := &C.Command{
// Name: "example",
// Action: action,
// }
func ToAction(effect CommandEffect) func(context.Context, *C.Command) error {
return func(ctx context.Context, cmd *C.Command) error {
// Execute the effect: cmd -> ctx -> IO -> Result
return F.Pipe3(
ctx,
effect(cmd),
io.Run,
// Convert Result[Void] to error
ET.Fold(F.Identity[error], F.Constant1[F.Void, error](nil)),
)
}
}
// FromAction converts a standard urfave/cli Action function into a CommandEffect.
// This allows existing cli/v3 action handlers to be lifted into the Effect type.
//
// The conversion process:
// 1. Takes a standard action function (context.Context, *C.Command) -> error
// 2. Wraps it in the Effect structure
// 3. Converts the error result to a Result type
//
// # Parameters
//
// - action: The standard cli/v3 action function to convert
//
// # Returns
//
// - A CommandEffect that wraps the original action
//
// # Example Usage
//
// standardAction := func(ctx context.Context, cmd *C.Command) error {
// // Existing command logic
// return nil
// }
// effect := FromAction(standardAction)
// // Now can be composed with other Effects
func FromAction(action func(context.Context, *C.Command) error) CommandEffect {
return func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
err := action(ctx, cmd)
if err != nil {
return R.Left[F.Void](err)
}
return R.Of(F.Void{})
}
}
}
}
// MakeCommand creates a new Command with an Effect-based action.
// This is a convenience function that combines command creation with Effect conversion.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommand(
// "process",
// "Process data files",
// []C.Flag{
// &C.StringFlag{Name: "input", Usage: "Input file"},
// },
// func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// input := cmd.String("input")
// // Process input...
// return R.Of(F.Void{})
// }
// }
// },
// )
func MakeCommand(
name string,
usage string,
flags []C.Flag,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Action: ToAction(effect),
}
}
// MakeCommandWithSubcommands creates a new Command with subcommands and an Effect-based action.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - commands: The subcommands
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with subcommands and the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommandWithSubcommands(
// "app",
// "Application commands",
// []C.Flag{},
// []*C.Command{subCmd1, subCmd2},
// defaultEffect,
// )
func MakeCommandWithSubcommands(
name string,
usage string,
flags []C.Flag,
commands []*C.Command,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Commands: commands,
Action: ToAction(effect),
}
}

204
v2/cli/effect_test.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"errors"
"testing"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestToAction_Success(t *testing.T) {
t.Run("converts successful Effect to action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
})
}
func TestToAction_Failure(t *testing.T) {
t.Run("converts failed Effect to error", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Left[F.Void](expectedErr)
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestFromAction_Success(t *testing.T) {
t.Run("converts successful action to Effect", func(t *testing.T) {
// Arrange
action := func(ctx context.Context, cmd *C.Command) error {
return nil
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsRight(result))
})
}
func TestFromAction_Failure(t *testing.T) {
t.Run("converts failed action to Effect", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
action := func(ctx context.Context, cmd *C.Command) error {
return expectedErr
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsLeft(result))
err := R.MonadFold(result, F.Identity[error], func(F.Void) error { return nil })
assert.Equal(t, expectedErr, err)
})
}
func TestMakeCommand(t *testing.T) {
t.Run("creates command with Effect-based action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommand(
"test",
"Test command",
[]C.Flag{},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "test", cmd.Name)
assert.Equal(t, "Test command", cmd.Usage)
assert.NotNil(t, cmd.Action)
// Test the action
err := cmd.Action(context.Background(), cmd)
assert.NoError(t, err)
})
}
func TestMakeCommandWithSubcommands(t *testing.T) {
t.Run("creates command with subcommands and Effect-based action", func(t *testing.T) {
// Arrange
subCmd := &C.Command{Name: "sub"}
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommandWithSubcommands(
"parent",
"Parent command",
[]C.Flag{},
[]*C.Command{subCmd},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "parent", cmd.Name)
assert.Equal(t, "Parent command", cmd.Usage)
assert.Len(t, cmd.Commands, 1)
assert.Equal(t, "sub", cmd.Commands[0].Name)
assert.NotNil(t, cmd.Action)
})
}
func TestToAction_Integration(t *testing.T) {
t.Run("Effect can access command flags", func(t *testing.T) {
// Arrange
var capturedValue string
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
capturedValue = cmd.String("input")
return R.Of(F.Void{})
}
}
}
cmd := &C.Command{
Name: "test",
Flags: []C.Flag{
&C.StringFlag{
Name: "input",
Value: "default-value",
},
},
Action: ToAction(effect),
}
// Act
err := cmd.Action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
assert.Equal(t, "default-value", capturedValue)
})
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func eitherHKT(typeE string) func(typeA string) string {
@@ -147,8 +147,8 @@ func generateEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -190,10 +190,10 @@ func EitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

359
v2/cli/flags.go Normal file
View File

@@ -0,0 +1,359 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
C "github.com/urfave/cli/v3"
)
// StringFlagPrism creates a Prism for extracting a StringFlag from a Flag.
// This provides a type-safe way to work with string flags, handling type
// mismatches gracefully through the Option type.
//
// The prism's GetOption attempts to cast a Flag to *C.StringFlag.
// If the cast succeeds, it returns Some(*C.StringFlag); if it fails, it returns None.
//
// The prism's ReverseGet converts a *C.StringFlag back to a Flag.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringFlag] for safe StringFlag extraction
//
// # Example Usage
//
// prism := StringFlagPrism()
//
// // Extract StringFlag from Flag
// var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
// result := prism.GetOption(flag) // Some(*C.StringFlag{...})
//
// // Type mismatch returns None
// var intFlag C.Flag = &C.IntFlag{Name: "count"}
// result = prism.GetOption(intFlag) // None[*C.StringFlag]()
//
// // Convert back to Flag
// strFlag := &C.StringFlag{Name: "output"}
// flag = prism.ReverseGet(strFlag)
func StringFlagPrism() P.Prism[C.Flag, *C.StringFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringFlag] {
if sf, ok := flag.(*C.StringFlag); ok {
return O.Some(sf)
}
return O.None[*C.StringFlag]()
},
func(f *C.StringFlag) C.Flag { return f },
)
}
// IntFlagPrism creates a Prism for extracting an IntFlag from a Flag.
// This provides a type-safe way to work with integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntFlag] for safe IntFlag extraction
//
// # Example Usage
//
// prism := IntFlagPrism()
//
// // Extract IntFlag from Flag
// var flag C.Flag = &C.IntFlag{Name: "count", Value: 10}
// result := prism.GetOption(flag) // Some(*C.IntFlag{...})
func IntFlagPrism() P.Prism[C.Flag, *C.IntFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntFlag] {
if f, ok := flag.(*C.IntFlag); ok {
return O.Some(f)
}
return O.None[*C.IntFlag]()
},
func(f *C.IntFlag) C.Flag { return f },
)
}
// BoolFlagPrism creates a Prism for extracting a BoolFlag from a Flag.
// This provides a type-safe way to work with boolean flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.BoolFlag] for safe BoolFlag extraction
//
// # Example Usage
//
// prism := BoolFlagPrism()
//
// // Extract BoolFlag from Flag
// var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// result := prism.GetOption(flag) // Some(*C.BoolFlag{...})
func BoolFlagPrism() P.Prism[C.Flag, *C.BoolFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.BoolFlag] {
if f, ok := flag.(*C.BoolFlag); ok {
return O.Some(f)
}
return O.None[*C.BoolFlag]()
},
func(f *C.BoolFlag) C.Flag { return f },
)
}
// Float64FlagPrism creates a Prism for extracting a Float64Flag from a Flag.
// This provides a type-safe way to work with float64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64Flag] for safe Float64Flag extraction
//
// # Example Usage
//
// prism := Float64FlagPrism()
//
// // Extract Float64Flag from Flag
// var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// result := prism.GetOption(flag) // Some(*C.Float64Flag{...})
func Float64FlagPrism() P.Prism[C.Flag, *C.Float64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64Flag] {
if f, ok := flag.(*C.Float64Flag); ok {
return O.Some(f)
}
return O.None[*C.Float64Flag]()
},
func(f *C.Float64Flag) C.Flag { return f },
)
}
// DurationFlagPrism creates a Prism for extracting a DurationFlag from a Flag.
// This provides a type-safe way to work with duration flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.DurationFlag] for safe DurationFlag extraction
//
// # Example Usage
//
// prism := DurationFlagPrism()
//
// // Extract DurationFlag from Flag
// var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: 30 * time.Second}
// result := prism.GetOption(flag) // Some(*C.DurationFlag{...})
func DurationFlagPrism() P.Prism[C.Flag, *C.DurationFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.DurationFlag] {
if f, ok := flag.(*C.DurationFlag); ok {
return O.Some(f)
}
return O.None[*C.DurationFlag]()
},
func(f *C.DurationFlag) C.Flag { return f },
)
}
// TimestampFlagPrism creates a Prism for extracting a TimestampFlag from a Flag.
// This provides a type-safe way to work with timestamp flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.TimestampFlag] for safe TimestampFlag extraction
//
// # Example Usage
//
// prism := TimestampFlagPrism()
//
// // Extract TimestampFlag from Flag
// var flag C.Flag = &C.TimestampFlag{Name: "created"}
// result := prism.GetOption(flag) // Some(*C.TimestampFlag{...})
func TimestampFlagPrism() P.Prism[C.Flag, *C.TimestampFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.TimestampFlag] {
if f, ok := flag.(*C.TimestampFlag); ok {
return O.Some(f)
}
return O.None[*C.TimestampFlag]()
},
func(f *C.TimestampFlag) C.Flag { return f },
)
}
// StringSliceFlagPrism creates a Prism for extracting a StringSliceFlag from a Flag.
// This provides a type-safe way to work with string slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringSliceFlag] for safe StringSliceFlag extraction
//
// # Example Usage
//
// prism := StringSliceFlagPrism()
//
// // Extract StringSliceFlag from Flag
// var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// result := prism.GetOption(flag) // Some(*C.StringSliceFlag{...})
func StringSliceFlagPrism() P.Prism[C.Flag, *C.StringSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringSliceFlag] {
if f, ok := flag.(*C.StringSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.StringSliceFlag]()
},
func(f *C.StringSliceFlag) C.Flag { return f },
)
}
// IntSliceFlagPrism creates a Prism for extracting an IntSliceFlag from a Flag.
// This provides a type-safe way to work with int slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntSliceFlag] for safe IntSliceFlag extraction
//
// # Example Usage
//
// prism := IntSliceFlagPrism()
//
// // Extract IntSliceFlag from Flag
// var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// result := prism.GetOption(flag) // Some(*C.IntSliceFlag{...})
func IntSliceFlagPrism() P.Prism[C.Flag, *C.IntSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntSliceFlag] {
if f, ok := flag.(*C.IntSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.IntSliceFlag]()
},
func(f *C.IntSliceFlag) C.Flag { return f },
)
}
// Float64SliceFlagPrism creates a Prism for extracting a Float64SliceFlag from a Flag.
// This provides a type-safe way to work with float64 slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64SliceFlag] for safe Float64SliceFlag extraction
//
// # Example Usage
//
// prism := Float64SliceFlagPrism()
//
// // Extract Float64SliceFlag from Flag
// var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// result := prism.GetOption(flag) // Some(*C.Float64SliceFlag{...})
func Float64SliceFlagPrism() P.Prism[C.Flag, *C.Float64SliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64SliceFlag] {
if f, ok := flag.(*C.Float64SliceFlag); ok {
return O.Some(f)
}
return O.None[*C.Float64SliceFlag]()
},
func(f *C.Float64SliceFlag) C.Flag { return f },
)
}
// UintFlagPrism creates a Prism for extracting a UintFlag from a Flag.
// This provides a type-safe way to work with unsigned integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.UintFlag] for safe UintFlag extraction
//
// # Example Usage
//
// prism := UintFlagPrism()
//
// // Extract UintFlag from Flag
// var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// result := prism.GetOption(flag) // Some(*C.UintFlag{...})
func UintFlagPrism() P.Prism[C.Flag, *C.UintFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.UintFlag] {
if f, ok := flag.(*C.UintFlag); ok {
return O.Some(f)
}
return O.None[*C.UintFlag]()
},
func(f *C.UintFlag) C.Flag { return f },
)
}
// Uint64FlagPrism creates a Prism for extracting a Uint64Flag from a Flag.
// This provides a type-safe way to work with uint64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Uint64Flag] for safe Uint64Flag extraction
//
// # Example Usage
//
// prism := Uint64FlagPrism()
//
// // Extract Uint64Flag from Flag
// var flag C.Flag = &C.Uint64Flag{Name: "size"}
// result := prism.GetOption(flag) // Some(*C.Uint64Flag{...})
func Uint64FlagPrism() P.Prism[C.Flag, *C.Uint64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Uint64Flag] {
if f, ok := flag.(*C.Uint64Flag); ok {
return O.Some(f)
}
return O.None[*C.Uint64Flag]()
},
func(f *C.Uint64Flag) C.Flag { return f },
)
}
// Int64FlagPrism creates a Prism for extracting an Int64Flag from a Flag.
// This provides a type-safe way to work with int64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Int64Flag] for safe Int64Flag extraction
//
// # Example Usage
//
// prism := Int64FlagPrism()
//
// // Extract Int64Flag from Flag
// var flag C.Flag = &C.Int64Flag{Name: "offset"}
// result := prism.GetOption(flag) // Some(*C.Int64Flag{...})
func Int64FlagPrism() P.Prism[C.Flag, *C.Int64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Int64Flag] {
if f, ok := flag.(*C.Int64Flag); ok {
return O.Some(f)
}
return O.None[*C.Int64Flag]()
},
func(f *C.Int64Flag) C.Flag { return f },
)
}

287
v2/cli/flags_test.go Normal file
View File

@@ -0,0 +1,287 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestStringFlagPrism_Success(t *testing.T) {
t.Run("extracts StringFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.StringFlag{Name: "input", Value: "test"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringFlag { return nil }, func(f *C.StringFlag) *C.StringFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "input", extracted.Name)
assert.Equal(t, "test", extracted.Value)
})
}
func TestStringFlagPrism_Failure(t *testing.T) {
t.Run("returns None for non-StringFlag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsNone(result))
})
}
func TestStringFlagPrism_ReverseGet(t *testing.T) {
t.Run("converts StringFlag back to Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
strFlag := &C.StringFlag{Name: "output", Value: "result"}
// Act
flag := prism.ReverseGet(strFlag)
// Assert
assert.NotNil(t, flag)
assert.IsType(t, &C.StringFlag{}, flag)
})
}
func TestIntFlagPrism_Success(t *testing.T) {
t.Run("extracts IntFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count", Value: 42}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntFlag { return nil }, func(f *C.IntFlag) *C.IntFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "count", extracted.Name)
assert.Equal(t, 42, extracted.Value)
})
}
func TestBoolFlagPrism_Success(t *testing.T) {
t.Run("extracts BoolFlag from Flag", func(t *testing.T) {
// Arrange
prism := BoolFlagPrism()
var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.BoolFlag { return nil }, func(f *C.BoolFlag) *C.BoolFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "verbose", extracted.Name)
assert.Equal(t, true, extracted.Value)
})
}
func TestFloat64FlagPrism_Success(t *testing.T) {
t.Run("extracts Float64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Float64FlagPrism()
var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64Flag { return nil }, func(f *C.Float64Flag) *C.Float64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratio", extracted.Name)
assert.Equal(t, 0.5, extracted.Value)
})
}
func TestDurationFlagPrism_Success(t *testing.T) {
t.Run("extracts DurationFlag from Flag", func(t *testing.T) {
// Arrange
prism := DurationFlagPrism()
duration := 30 * time.Second
var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: duration}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.DurationFlag { return nil }, func(f *C.DurationFlag) *C.DurationFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "timeout", extracted.Name)
assert.Equal(t, duration, extracted.Value)
})
}
func TestTimestampFlagPrism_Success(t *testing.T) {
t.Run("extracts TimestampFlag from Flag", func(t *testing.T) {
// Arrange
prism := TimestampFlagPrism()
var flag C.Flag = &C.TimestampFlag{Name: "created"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.TimestampFlag { return nil }, func(f *C.TimestampFlag) *C.TimestampFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "created", extracted.Name)
})
}
func TestStringSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts StringSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringSliceFlagPrism()
var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringSliceFlag { return nil }, func(f *C.StringSliceFlag) *C.StringSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "tags", extracted.Name)
})
}
func TestIntSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts IntSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntSliceFlagPrism()
var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntSliceFlag { return nil }, func(f *C.IntSliceFlag) *C.IntSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ports", extracted.Name)
})
}
func TestFloat64SliceFlagPrism_Success(t *testing.T) {
t.Run("extracts Float64SliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := Float64SliceFlagPrism()
var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64SliceFlag { return nil }, func(f *C.Float64SliceFlag) *C.Float64SliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratios", extracted.Name)
})
}
func TestUintFlagPrism_Success(t *testing.T) {
t.Run("extracts UintFlag from Flag", func(t *testing.T) {
// Arrange
prism := UintFlagPrism()
var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.UintFlag { return nil }, func(f *C.UintFlag) *C.UintFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "workers", extracted.Name)
assert.Equal(t, uint(4), extracted.Value)
})
}
func TestUint64FlagPrism_Success(t *testing.T) {
t.Run("extracts Uint64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Uint64FlagPrism()
var flag C.Flag = &C.Uint64Flag{Name: "size", Value: 1024}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Uint64Flag { return nil }, func(f *C.Uint64Flag) *C.Uint64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "size", extracted.Name)
assert.Equal(t, uint64(1024), extracted.Value)
})
}
func TestInt64FlagPrism_Success(t *testing.T) {
t.Run("extracts Int64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Int64FlagPrism()
var flag C.Flag = &C.Int64Flag{Name: "offset", Value: -100}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Int64Flag { return nil }, func(f *C.Int64Flag) *C.Int64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "offset", extracted.Name)
assert.Equal(t, int64(-100), extracted.Value)
})
}
func TestPrisms_EdgeCases(t *testing.T) {
t.Run("all prisms return None for wrong type", func(t *testing.T) {
// Arrange
var flag C.Flag = &C.StringFlag{Name: "test"}
// Act & Assert
assert.True(t, O.IsNone(IntFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(BoolFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(DurationFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(TimestampFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(StringSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(IntSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64SliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(UintFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Uint64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Int64FlagPrism().GetOption(flag)))
})
}

View File

@@ -18,7 +18,6 @@ package cli
import (
"fmt"
"os"
"time"
)
func writePackage(f *os.File, pkg string) {
@@ -26,6 +25,6 @@ func writePackage(f *os.File, pkg string) {
fmt.Fprintf(f, "package %s\n\n", pkg)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func identityHKT(typeA string) string {
@@ -61,8 +61,8 @@ func generateIdentityHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -93,10 +93,10 @@ func IdentityCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIdentityHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,14 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIO(param string) string {
@@ -70,8 +70,8 @@ func generateIOHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -102,10 +102,10 @@ func IOCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,14 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
@@ -218,8 +218,8 @@ func generateIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -233,8 +233,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")
@@ -273,10 +272,10 @@ func IOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,14 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIOOption(param string) string {
@@ -75,8 +75,8 @@ func generateIOOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -107,10 +107,10 @@ func IOOptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -17,6 +17,7 @@ package cli
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
@@ -28,7 +29,7 @@ import (
"text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (
@@ -86,7 +87,9 @@ type templateData struct {
}
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
// {{.Name}}Lenses provides [lenses] for accessing fields of [{{.Name}}]
//
// [lenses]: __lens.Lens
type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
@@ -100,7 +103,10 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
// {{.Name}}RefLenses provides [lenses] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
//
//
// [lenses]: __lens.Lens
type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
@@ -111,23 +117,32 @@ type {{.Name}}RefLenses{{.TypeParams}} struct {
{{- if .IsComparable}}
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
// prisms
{{- range .Fields}}
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
// {{.Name}}Prisms provides [prisms] for accessing fields of [{{.Name}}]
//
// [prisms]: __prism.Prism
type {{.Name}}Prisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}RefPrisms provides [prisms] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
//
// [prisms]: __prism.Prism
type {{.Name}}RefPrisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
// Make{{.Name}}Lenses creates a new [{{.Name}}Lenses] with [lenses] for all fields
//
// [lenses]:__lens.Lens
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
@@ -157,7 +172,9 @@ func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
// Make{{.Name}}RefLenses creates a new [{{.Name}}RefLenses] with [lenses] for all fields
//
// [lenses]:__lens.Lens
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
@@ -195,7 +212,9 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
}
}
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
// Make{{.Name}}Prisms creates a new [{{.Name}}Prisms] with [prisms] for all fields
//
// [prisms]:__prism.Prism
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
@@ -235,6 +254,49 @@ func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- end}}
}
}
// Make{{.Name}}RefPrisms creates a new [{{.Name}}RefPrisms] with [prisms] for all fields
//
// [prisms]:__prism.Prism
func Make{{.Name}}RefPrisms{{.TypeParams}}() {{.Name}}RefPrisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
_prism{{.Name}} := __prism.MakePrismWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return &result
{{- else}}
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- else}}
_prism{{.Name}} := __prism.MakePrismWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return &result
{{- else}}
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- end}}
{{- end}}
return {{.Name}}RefPrisms{{.TypeParamNames}} {
{{- range .Fields}}
{{.Name}}: _prism{{.Name}},
{{- end}}
}
}
`
var (
@@ -535,9 +597,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
fieldTypeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := fieldTypeName
@@ -697,9 +759,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
typeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := typeName
isComparable := false
@@ -934,12 +996,12 @@ func LensCommand() *C.Command {
flagVerbose,
flagIncludeTestFiles,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
ctx.Bool(keyIncludeTestFile),
cmd.String(keyLensDir),
cmd.String(keyFilename),
cmd.Bool(keyVerbose),
cmd.Bool(keyIncludeTestFile),
)
},
}

View File

@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
}
func TestParseFileWithUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
PublicName string
privateName string
PublicValue int
privateValue *int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
// Check exported field
assert.Equal(t, "PublicName", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Check unexported field
assert.Equal(t, "privateName", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.False(t, config.Fields[1].IsOptional)
// Check exported int field
assert.Equal(t, "PublicValue", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.False(t, config.Fields[2].IsOptional)
// Check unexported pointer field
assert.Equal(t, "privateValue", config.Fields[3].Name)
assert.Equal(t, "*int", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
}
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type MixedStruct struct {
PublicField string
privateField int
OptionalPrivate *string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "MixedStructLenses")
assert.Contains(t, contentStr, "MakeMixedStructLenses")
// Check that lenses are generated for all fields (exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
// Check lens constructors
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
// Check setters
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
}
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type PrivateConfig struct {
name string
value int
enabled bool
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check PrivateConfig struct
config := structs[0]
assert.Equal(t, "PrivateConfig", config.Name)
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
// Check all fields are unexported
assert.Equal(t, "name", config.Fields[0].Name)
assert.Equal(t, "value", config.Fields[1].Name)
assert.Equal(t, "enabled", config.Fields[2].Name)
}
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
type BaseConfig struct {
publicBase string
privateBase int
}
// fp-go:Lens
type ExtendedConfig struct {
BaseConfig
PublicField string
privateField bool
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "ExtendedConfigLenses")
// Check that lenses are generated for embedded unexported fields
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
// Check that lenses are generated for direct fields (both exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
}
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
// Create a temporary test file with various field visibility patterns
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type ComplexStruct struct {
// Exported fields
Name string
Age int
Email *string
// Unexported fields
password string
secretKey []byte
internalID *int
// Mixed with tags
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check ComplexStruct
complex := structs[0]
assert.Equal(t, "ComplexStruct", complex.Name)
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
// Verify field names and types
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
for i, expectedName := range fieldNames {
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
}
// Check optional fields
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func optionHKT(typeA string) string {
@@ -147,8 +147,8 @@ func generateOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -200,10 +200,10 @@ func OptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateUnsliced(f *os.File, i int) {
@@ -377,8 +377,8 @@ func generatePipeHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)
@@ -423,10 +423,10 @@ func PipeCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generatePipeHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderFrom(f, fg *os.File, i int) {
@@ -117,8 +117,8 @@ func generateReaderHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -130,8 +130,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")
@@ -154,10 +153,10 @@ func ReaderCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,13 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
@@ -232,8 +232,8 @@ func generateReaderIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -245,8 +245,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")
@@ -284,10 +283,10 @@ func ReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,14 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func writeTupleType(f *os.File, symbol string, i int) {
@@ -398,8 +398,8 @@ func generateTupleHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -615,10 +615,10 @@ func TupleCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateTupleHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -13,6 +13,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package constant provides the Const functor, a phantom type that ignores its second type parameter.
//
// The Const functor is a fundamental building block in functional programming that wraps a value
// of type E while having a phantom type parameter A. This makes it useful for:
// - Accumulating values during traversals (e.g., collecting metadata)
// - Implementing optics (lenses, prisms) where you need to track information
// - Building applicative functors that combine values using a semigroup
//
// # The Const Functor
//
// Const[E, A] wraps a value of type E and has a phantom type parameter A that doesn't affect
// the runtime value. This allows it to participate in functor and applicative operations while
// maintaining the wrapped value unchanged.
//
// # Key Properties
//
// - Map operations ignore the function and preserve the wrapped value
// - Ap operations combine wrapped values using a semigroup
// - The phantom type A allows type-safe composition with other functors
//
// # Example Usage
//
// // Accumulate string values
// c1 := Make[string, int]("hello")
// c2 := Make[string, int]("world")
//
// // Map doesn't change the wrapped value
// mapped := Map[string, int, string](strconv.Itoa)(c1) // Still contains "hello"
//
// // Ap combines values using a semigroup
// combined := Ap[string, int, int](S.Monoid)(c1)(c2) // Contains "helloworld"
package constant
import (
@@ -21,36 +52,209 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Const is a functor that wraps a value of type E with a phantom type parameter A.
//
// The Const functor is useful for accumulating values during traversals or implementing
// optics. The type parameter A is phantom - it doesn't affect the runtime value but allows
// the type to participate in functor and applicative operations.
//
// Type Parameters:
// - E: The type of the wrapped value (the actual data)
// - A: The phantom type parameter (not stored, only used for type-level operations)
//
// Example:
//
// // Create a Const that wraps a string
// c := Make[string, int]("metadata")
//
// // The int type parameter is phantom - no int value is stored
// value := Unwrap(c) // "metadata"
type Const[E, A any] struct {
value E
}
// Make creates a Const value wrapping the given value.
//
// This is the primary constructor for Const values. The second type parameter A
// is phantom and must be specified explicitly when needed for type inference.
//
// Type Parameters:
// - E: The type of the value to wrap
// - A: The phantom type parameter
//
// Parameters:
// - e: The value to wrap
//
// Returns:
// - A Const[E, A] wrapping the value
//
// Example:
//
// c := Make[string, int]("hello")
// value := Unwrap(c) // "hello"
func Make[E, A any](e E) Const[E, A] {
return Const[E, A]{value: e}
}
// Unwrap extracts the wrapped value from a Const.
//
// This is the inverse of Make, retrieving the actual value stored in the Const.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The phantom type parameter
//
// Parameters:
// - c: The Const to unwrap
//
// Returns:
// - The wrapped value of type E
//
// Example:
//
// c := Make[string, int]("world")
// value := Unwrap(c) // "world"
func Unwrap[E, A any](c Const[E, A]) E {
return c.value
}
// Of creates a Const containing the monoid's empty value, ignoring the input.
//
// This implements the Applicative's "pure" operation for Const. It creates a Const
// wrapping the monoid's identity element, regardless of the input value.
//
// Type Parameters:
// - E: The type of the wrapped value (must have a monoid)
// - A: The input type (ignored)
//
// Parameters:
// - m: The monoid providing the empty value
//
// Returns:
// - A function that ignores its input and returns Const[E, A] with the empty value
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// of := Of[string, int](S.Monoid)
// c := of(42) // Const[string, int] containing ""
// value := Unwrap(c) // ""
func Of[E, A any](m M.Monoid[E]) func(A) Const[E, A] {
return F.Constant1[A](Make[E, A](m.Empty()))
}
// MonadMap applies a function to the phantom type parameter without changing the wrapped value.
//
// This implements the Functor's map operation for Const. Since the type parameter A is phantom,
// the function is never actually called - the wrapped value E remains unchanged.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - fa: The Const to map over
// - _: The function to apply (ignored)
//
// Returns:
// - A Const[E, B] with the same wrapped value
//
// Example:
//
// c := Make[string, int]("hello")
// mapped := MonadMap(c, func(i int) string { return strconv.Itoa(i) })
// // mapped still contains "hello", function was never called
func MonadMap[E, A, B any](fa Const[E, A], _ func(A) B) Const[E, B] {
return Make[E, B](fa.value)
}
// MonadAp combines two Const values using a semigroup.
//
// This implements the Applicative's ap operation for Const. It combines the wrapped
// values from both Const instances using the provided semigroup, ignoring the function
// type in the first argument.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A function that takes two Const values and combines their wrapped values
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// ap := MonadAp[string, int, int](S.Monoid)
// c1 := Make[string, func(int) int]("hello")
// c2 := Make[string, int]("world")
// result := ap(c1, c2) // Const containing "helloworld"
func MonadAp[E, A, B any](s S.Semigroup[E]) func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return Make[E, B](s.Concat(fab.value, fa.value))
}
}
// Map applies a function to the phantom type parameter without changing the wrapped value.
//
// This is the curried version of MonadMap, providing a more functional programming style.
// The function is never actually called since A is a phantom type.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - f: The function to apply (ignored)
//
// Returns:
// - A function that transforms Const[E, A] to Const[E, B]
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// c := Make[string, int]("data")
// mapped := F.Pipe1(c, Map[string, int, string](strconv.Itoa))
// // mapped still contains "data"
func Map[E, A, B any](f func(A) B) func(fa Const[E, A]) Const[E, B] {
return F.Bind2nd(MonadMap[E, A, B], f)
}
// Ap combines Const values using a semigroup in a curried style.
//
// This is the curried version of MonadAp, providing data-last style for better composition.
// It combines the wrapped values from both Const instances using the provided semigroup.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A curried function for combining Const values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// c1 := Make[string, int]("hello")
// c2 := Make[string, func(int) int]("world")
// result := F.Pipe1(c1, Ap[string, int, int](S.Monoid)(c2))
// // result contains "helloworld"
func Ap[E, A, B any](s S.Semigroup[E]) func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {
monadap := MonadAp[E, A, B](s)
return func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {

View File

@@ -16,25 +16,340 @@
package constant
import (
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
fa := Make[string, int]("foo")
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
// TestMake tests the Make constructor
func TestMake(t *testing.T) {
t.Run("creates Const with string value", func(t *testing.T) {
c := Make[string, int]("hello")
assert.Equal(t, "hello", Unwrap(c))
})
t.Run("creates Const with int value", func(t *testing.T) {
c := Make[int, string](42)
assert.Equal(t, 42, Unwrap(c))
})
t.Run("creates Const with struct value", func(t *testing.T) {
type Config struct {
Name string
Port int
}
cfg := Config{Name: "server", Port: 8080}
c := Make[Config, bool](cfg)
assert.Equal(t, cfg, Unwrap(c))
})
}
// TestUnwrap tests extracting values from Const
func TestUnwrap(t *testing.T) {
t.Run("unwraps string value", func(t *testing.T) {
c := Make[string, int]("world")
value := Unwrap(c)
assert.Equal(t, "world", value)
})
t.Run("unwraps empty string", func(t *testing.T) {
c := Make[string, int]("")
value := Unwrap(c)
assert.Equal(t, "", value)
})
t.Run("unwraps zero value", func(t *testing.T) {
c := Make[int, string](0)
value := Unwrap(c)
assert.Equal(t, 0, value)
})
}
// TestOf tests the Of function
func TestOf(t *testing.T) {
assert.Equal(t, Make[string, int](""), Of[string, int](S.Monoid)(1))
t.Run("creates Const with monoid empty value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c := of(42)
assert.Equal(t, "", Unwrap(c))
})
t.Run("ignores input value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c1 := of(1)
c2 := of(100)
assert.Equal(t, Unwrap(c1), Unwrap(c2))
})
t.Run("works with int monoid", func(t *testing.T) {
of := Of[int, string](N.MonoidSum[int]())
c := of("ignored")
assert.Equal(t, 0, Unwrap(c))
})
}
func TestAp(t *testing.T) {
fab := Make[string, int]("bar")
assert.Equal(t, Make[string, int]("foobar"), Ap[string, int, int](S.Monoid)(fab)(Make[string, func(int) int]("foo")))
// TestMap tests the Map function
func TestMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("foo")
result := F.Pipe1(fa, Map[string](utils.Double))
assert.Equal(t, "foo", Unwrap(result))
})
t.Run("changes phantom type", func(t *testing.T) {
fa := Make[string, int]("data")
fb := Map[string, int, string](strconv.Itoa)(fa)
// Value unchanged, but type changed from Const[string, int] to Const[string, string]
assert.Equal(t, "data", Unwrap(fb))
})
t.Run("function is never called", func(t *testing.T) {
called := false
fa := Make[string, int]("test")
fb := Map[string, int, string](func(i int) string {
called = true
return strconv.Itoa(i)
})(fa)
assert.False(t, called, "Map function should not be called")
assert.Equal(t, "test", Unwrap(fb))
})
}
// TestMonadMap tests the MonadMap function
func TestMonadMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("original")
fb := MonadMap(fa, func(i int) string { return strconv.Itoa(i) })
assert.Equal(t, "original", Unwrap(fb))
})
t.Run("works with different types", func(t *testing.T) {
fa := Make[int, string](42)
fb := MonadMap(fa, func(s string) bool { return len(s) > 0 })
assert.Equal(t, 42, Unwrap(fb))
})
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
t.Run("combines string values", func(t *testing.T) {
fab := Make[string, int]("bar")
fa := Make[string, func(int) int]("foo")
result := Ap[string, int, int](S.Monoid)(fab)(fa)
assert.Equal(t, "foobar", Unwrap(result))
})
t.Run("combines int values with sum", func(t *testing.T) {
fab := Make[int, string](10)
fa := Make[int, func(string) string](5)
result := Ap[int, string, string](N.SemigroupSum[int]())(fab)(fa)
assert.Equal(t, 15, Unwrap(result))
})
t.Run("combines int values with product", func(t *testing.T) {
fab := Make[int, bool](3)
fa := Make[int, func(bool) bool](4)
result := Ap[int, bool, bool](N.SemigroupProduct[int]())(fab)(fa)
assert.Equal(t, 12, Unwrap(result))
})
}
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
t.Run("combines values using semigroup", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("hello")
fa := Make[string, int]("world")
result := ap(fab, fa)
assert.Equal(t, "helloworld", Unwrap(result))
})
t.Run("works with empty strings", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("")
fa := Make[string, int]("test")
result := ap(fab, fa)
assert.Equal(t, "test", Unwrap(result))
})
}
// TestMonoid tests the Monoid function
func TestMonoid(t *testing.T) {
t.Run("always returns constant value", func(t *testing.T) {
m := Monoid(42)
assert.Equal(t, 42, m.Concat(1, 2))
assert.Equal(t, 42, m.Concat(100, 200))
assert.Equal(t, 42, m.Empty())
})
t.Run("works with strings", func(t *testing.T) {
m := Monoid("constant")
assert.Equal(t, "constant", m.Concat("a", "b"))
assert.Equal(t, "constant", m.Empty())
})
t.Run("works with structs", func(t *testing.T) {
type Point struct{ X, Y int }
p := Point{X: 1, Y: 2}
m := Monoid(p)
assert.Equal(t, p, m.Concat(Point{X: 3, Y: 4}, Point{X: 5, Y: 6}))
assert.Equal(t, p, m.Empty())
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := Monoid(10)
// Left identity: Concat(Empty(), x) = x (both return constant)
assert.Equal(t, 10, m.Concat(m.Empty(), 5))
// Right identity: Concat(x, Empty()) = x (both return constant)
assert.Equal(t, 10, m.Concat(5, m.Empty()))
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
left := m.Concat(m.Concat(1, 2), 3)
right := m.Concat(1, m.Concat(2, 3))
assert.Equal(t, left, right)
assert.Equal(t, 10, left)
})
}
// TestConstFunctorLaws tests functor laws for Const
func TestConstFunctorLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// map id = id
fa := Make[string, int]("test")
mapped := Map[string, int, int](F.Identity[int])(fa)
assert.Equal(t, Unwrap(fa), Unwrap(mapped))
})
t.Run("composition law", func(t *testing.T) {
// map (g . f) = map g . map f
fa := Make[string, int]("data")
f := func(i int) string { return strconv.Itoa(i) }
g := func(s string) bool { return len(s) > 0 }
// map (g . f)
composed := Map[string, int, bool](func(i int) bool { return g(f(i)) })(fa)
// map g . map f
intermediate := F.Pipe1(fa, Map[string, int, string](f))
chained := Map[string, string, bool](g)(intermediate)
assert.Equal(t, Unwrap(composed), Unwrap(chained))
})
}
// TestConstApplicativeLaws tests applicative laws for Const
func TestConstApplicativeLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// For Const, ap combines the wrapped values using the semigroup
// ap (of id) v combines empty (from of) with v's value
v := Make[string, int]("value")
ofId := Of[string, func(int) int](S.Monoid)(F.Identity[int])
result := Ap[string, int, int](S.Monoid)(v)(ofId)
// Result combines "" (from Of) with "value" using string monoid
assert.Equal(t, "value", Unwrap(result))
})
t.Run("homomorphism law", func(t *testing.T) {
// ap (of f) (of x) = of (f x)
f := func(i int) string { return strconv.Itoa(i) }
x := 42
ofF := Of[string, func(int) string](S.Monoid)(f)
ofX := Of[string, int](S.Monoid)(x)
left := Ap[string, int, string](S.Monoid)(ofX)(ofF)
right := Of[string, string](S.Monoid)(f(x))
assert.Equal(t, Unwrap(left), Unwrap(right))
})
}
// TestConstEdgeCases tests edge cases
func TestConstEdgeCases(t *testing.T) {
t.Run("empty string values", func(t *testing.T) {
c := Make[string, int]("")
assert.Equal(t, "", Unwrap(c))
mapped := Map[string, int, string](strconv.Itoa)(c)
assert.Equal(t, "", Unwrap(mapped))
})
t.Run("zero values", func(t *testing.T) {
c := Make[int, string](0)
assert.Equal(t, 0, Unwrap(c))
})
t.Run("nil pointer", func(t *testing.T) {
var ptr *int
c := Make[*int, string](ptr)
assert.Nil(t, Unwrap(c))
})
t.Run("multiple map operations", func(t *testing.T) {
c := Make[string, int]("original")
// Chain multiple map operations
step1 := Map[string, int, string](strconv.Itoa)(c)
step2 := Map[string, string, bool](func(s string) bool { return len(s) > 0 })(step1)
result := Map[string, bool, int](func(b bool) int {
if b {
return 1
}
return 0
})(step2)
assert.Equal(t, "original", Unwrap(result))
})
}
// BenchmarkMake benchmarks the Make constructor
func BenchmarkMake(b *testing.B) {
b.ResetTimer()
for b.Loop() {
_ = Make[string, int]("test")
}
}
// BenchmarkUnwrap benchmarks the Unwrap function
func BenchmarkUnwrap(b *testing.B) {
c := Make[string, int]("test")
b.ResetTimer()
for b.Loop() {
_ = Unwrap(c)
}
}
// BenchmarkMap benchmarks the Map function
func BenchmarkMap(b *testing.B) {
c := Make[string, int]("test")
mapFn := Map[string, int, string](strconv.Itoa)
b.ResetTimer()
for b.Loop() {
_ = mapFn(c)
}
}
// BenchmarkAp benchmarks the Ap function
func BenchmarkAp(b *testing.B) {
fab := Make[string, int]("hello")
fa := Make[string, func(int) int]("world")
apFn := Ap[string, int, int](S.Monoid)
b.ResetTimer()
for b.Loop() {
_ = apFn(fab)(fa)
}
}
// BenchmarkMonoid benchmarks the Monoid function
func BenchmarkMonoid(b *testing.B) {
m := Monoid(42)
b.ResetTimer()
for b.Loop() {
_ = m.Concat(1, 2)
}
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package constant
import (
@@ -5,7 +20,47 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
// Monoid creates a monoid that always returns a constant value.
//
// This creates a trivial monoid where both the Concat operation and Empty
// always return the same constant value, regardless of inputs. This is useful
// for testing, placeholder implementations, or when you need a monoid instance
// but the actual combining behavior doesn't matter.
//
// # Monoid Laws
//
// The constant monoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always returns 'a'
// - Left Identity: Concat(Empty(), x) = x - both return 'a'
// - Right Identity: Concat(x, Empty()) = x - both return 'a'
//
// Type Parameters:
// - A: The type of the constant value
//
// Parameters:
// - a: The constant value to return in all operations
//
// Returns:
// - A Monoid[A] that always returns the constant value
//
// Example:
//
// // Create a monoid that always returns 42
// m := Monoid(42)
// result := m.Concat(1, 2) // 42
// empty := m.Empty() // 42
//
// // Useful for testing or placeholder implementations
// type Config struct {
// Timeout int
// }
// defaultConfig := Monoid(Config{Timeout: 30})
// config := defaultConfig.Concat(Config{Timeout: 10}, Config{Timeout: 20})
// // config is Config{Timeout: 30}
//
// See also:
// - function.Constant2: The underlying constant function
// - M.MakeMonoid: The monoid constructor
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

View File

@@ -13,28 +13,218 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package constraints defines a set of useful type constraints for generic programming in Go.
# Overview
This package provides type constraints that can be used with Go generics to restrict
type parameters to specific categories of types. These constraints are similar to those
in Go's standard constraints package but are defined here for consistency within the
fp-go project.
# Type Constraints
Ordered - Types that support comparison operators:
type Ordered interface {
Integer | Float | ~string
}
Used for types that can be compared using <, <=, >, >= operators.
Integer - All integer types (signed and unsigned):
type Integer interface {
Signed | Unsigned
}
Signed - Signed integer types:
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
Unsigned - Unsigned integer types:
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
Float - Floating-point types:
type Float interface {
~float32 | ~float64
}
Complex - Complex number types:
type Complex interface {
~complex64 | ~complex128
}
# Usage Examples
Using Ordered constraint for comparison:
import C "github.com/IBM/fp-go/v2/constraints"
func Min[T C.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
result := Min(5, 3) // 3
result := Min(3.14, 2.71) // 2.71
result := Min("apple", "banana") // "apple"
Using Integer constraint:
func Abs[T C.Integer](n T) T {
if n < 0 {
return -n
}
return n
}
result := Abs(-42) // 42
result := Abs(uint(10)) // 10
Using Float constraint:
func Average[T C.Float](a, b T) T {
return (a + b) / 2
}
result := Average(3.14, 2.86) // 3.0
Using Complex constraint:
func Magnitude[T C.Complex](c T) float64 {
r, i := real(c), imag(c)
return math.Sqrt(r*r + i*i)
}
c := complex(3, 4)
result := Magnitude(c) // 5.0
# Combining Constraints
Constraints can be combined to create more specific type restrictions:
type Number interface {
C.Integer | C.Float | C.Complex
}
func Add[T Number](a, b T) T {
return a + b
}
# Tilde Operator
The ~ operator in type constraints means "underlying type". For example, ~int
matches not only int but also any type whose underlying type is int:
type MyInt int
func Double[T C.Integer](n T) T {
return n * 2
}
var x MyInt = 5
result := Double(x) // Works because MyInt's underlying type is int
# Related Packages
- number: Provides algebraic structures and utilities for numeric types
- ord: Provides ordering operations using these constraints
- eq: Provides equality operations for comparable types
*/
package constraints
// Ordered is a constraint that permits any ordered type: any type that supports
// the operators < <= >= >. Ordered types include integers, floats, and strings.
//
// This constraint is commonly used for comparison operations, sorting, and
// finding minimum/maximum values.
//
// Example:
//
// func Max[T Ordered](a, b T) T {
// if a > b {
// return a
// }
// return b
// }
type Ordered interface {
Integer | Float | ~string
}
// Signed is a constraint that permits any signed integer type.
// This includes int, int8, int16, int32, and int64, as well as any
// types whose underlying type is one of these.
//
// Example:
//
// func Negate[T Signed](n T) T {
// return -n
// }
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned is a constraint that permits any unsigned integer type.
// This includes uint, uint8, uint16, uint32, uint64, and uintptr, as well
// as any types whose underlying type is one of these.
//
// Example:
//
// func IsEven[T Unsigned](n T) bool {
// return n%2 == 0
// }
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// Integer is a constraint that permits any integer type, both signed and unsigned.
// This is a union of the Signed and Unsigned constraints.
//
// Example:
//
// func Abs[T Integer](n T) T {
// if n < 0 {
// return -n
// }
// return n
// }
type Integer interface {
Signed | Unsigned
}
// Float is a constraint that permits any floating-point type.
// This includes float32 and float64, as well as any types whose
// underlying type is one of these.
//
// Example:
//
// func Round[T Float](f T) T {
// return T(math.Round(float64(f)))
// }
type Float interface {
~float32 | ~float64
}
// Complex is a constraint that permits any complex numeric type.
// This includes complex64 and complex128, as well as any types whose
// underlying type is one of these.
//
// Example:
//
// func Conjugate[T Complex](c T) T {
// return complex(real(c), -imag(c))
// }
type Complex interface {
~complex64 | ~complex128
}

View File

@@ -177,3 +177,255 @@ func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
}
}
}
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
// It composes a preprocessing function with a consumer, creating a new consumer that applies
// the function before consuming the value.
//
// This function is semantically identical to Local but uses terminology that may be more familiar
// to developers coming from functional programming backgrounds where "compose" is a common operation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// The name "Compose" highlights that we're composing two operations:
// 1. The transformation function f: R2 -> R1
// 2. The consumer c: R1 -> ()
//
// Result: A composed consumer: R2 -> ()
//
// Type Parameters:
// - R1: The input type of the original Consumer (what it expects)
// - R2: The input type of the new Consumer (what you have)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic composition:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Compose with a string-to-int parser
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Compose(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Composing multiple transformations:
//
// type Data struct {
// Value string
// }
//
// type Wrapper struct {
// Data Data
// }
//
// // Consumer that logs strings
// logString := func(s string) {
// fmt.Println(s)
// }
//
// // Compose transformations step by step
// extractData := func(w Wrapper) Data { return w.Data }
// extractValue := func(d Data) string { return d.Value }
//
// logData := consumer.Compose(extractValue)(logString)
// logWrapper := consumer.Compose(extractData)(logData)
//
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
//
// Example - Function composition style:
//
// // Compose is particularly useful when thinking in terms of function composition
// type Request struct {
// Body []byte
// }
//
// // Consumer that processes strings
// processString := func(s string) {
// fmt.Printf("Processing: %s\n", s)
// }
//
// // Compose byte-to-string conversion with processing
// bytesToString := func(b []byte) string {
// return string(b)
// }
// extractBody := func(r Request) []byte {
// return r.Body
// }
//
// // Chain compositions
// processBytes := consumer.Compose(bytesToString)(processString)
// processRequest := consumer.Compose(extractBody)(processBytes)
//
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
//
// Relationship to Local:
// - Compose and Local are identical in implementation
// - Compose emphasizes the functional composition aspect
// - Local emphasizes the environment/context transformation aspect
// - Use Compose when thinking about function composition
// - Use Local when thinking about adapting to different contexts
//
// Use Cases:
// - Building processing pipelines with clear composition semantics
// - Adapting consumers in a functional programming style
// - Creating reusable consumer transformations
// - Chaining multiple preprocessing steps
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return Local(f)
}
// Contramap is the categorical name for the contravariant functor operation on Consumers.
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
// functor's map operation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
//
// In category theory, a contravariant functor reverses the direction of morphisms.
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
//
// For Consumers:
// - Consumer[A] is contravariant in A
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
//
// This is semantically identical to Local and Compose, but uses the standard
// categorical terminology that emphasizes the contravariant nature of the transformation.
//
// Type Parameters:
// - R1: The input type of the original Consumer (what it expects)
// - R2: The input type of the new Consumer (what you have)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic contravariant mapping:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Contramap with a string-to-int parser
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Contramap(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Demonstrating contravariance:
//
// // In covariant functors (like Option, Array), map goes "forward":
// // map: (A -> B) -> F[A] -> F[B]
// //
// // In contravariant functors (like Consumer), contramap goes "backward":
// // contramap: (B -> A) -> F[A] -> F[B]
//
// type Animal struct{ Name string }
// type Dog struct{ Animal Animal; Breed string }
//
// // Consumer for animals
// consumeAnimal := func(a Animal) {
// fmt.Printf("Animal: %s\n", a.Name)
// }
//
// // Function from Dog to Animal (B -> A)
// dogToAnimal := func(d Dog) Animal {
// return d.Animal
// }
//
// // Contramap creates Consumer[Dog] from Consumer[Animal]
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
//
// consumeDog(Dog{
// Animal: Animal{Name: "Buddy"},
// Breed: "Golden Retriever",
// }) // Logs: "Animal: Buddy"
//
// Example - Contravariant functor laws:
//
// // Law 1: Identity
// // contramap(identity) = identity
// identity := func(x int) int { return x }
// consumer1 := consumer.Contramap(identity)(consumeInt)
// // consumer1 behaves identically to consumeInt
//
// // Law 2: Composition
// // contramap(f . g) = contramap(g) . contramap(f)
// // Note: composition order is reversed compared to covariant map
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
// g := func(b bool) string { if b { return "1" } else { return "0" } }
//
// // These two are equivalent:
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
//
// Example - Practical use with type hierarchies:
//
// type Logger interface {
// Log(string)
// }
//
// type Message struct {
// Text string
// Timestamp time.Time
// }
//
// // Consumer that logs strings
// logString := func(s string) {
// fmt.Println(s)
// }
//
// // Contramap to handle Message types
// extractText := func(m Message) string {
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
// }
//
// logMessage := consumer.Contramap(extractText)(logString)
// logMessage(Message{
// Text: "Hello",
// Timestamp: time.Now(),
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
//
// Relationship to Local and Compose:
// - Contramap, Local, and Compose are identical in implementation
// - Contramap emphasizes the categorical/theoretical aspect
// - Local emphasizes the context transformation aspect
// - Compose emphasizes the function composition aspect
// - Use Contramap when working with category theory concepts
// - Use Local when adapting to different contexts
// - Use Compose when building functional pipelines
//
// Category Theory Background:
// - Consumer[A] forms a contravariant functor
// - The contravariant functor laws must hold:
// 1. contramap(id) = id
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
// - This is dual to the covariant functor (map) operation
// - Consumers are contravariant because they consume rather than produce values
//
// Use Cases:
// - Working with contravariant functors in a categorical style
// - Adapting consumers to work with more specific types
// - Building type-safe consumer transformations
// - Implementing profunctor patterns (Consumer is a profunctor)
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return Local(f)
}

View File

@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
assert.Equal(t, 42, captured)
})
}
func TestContramap(t *testing.T) {
t.Run("basic contravariant mapping", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
parseToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Contramap(parseToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("contravariant identity law", func(t *testing.T) {
// contramap(identity) = identity
var captured int
consumeInt := func(x int) {
captured = x
}
identity := function.Identity[int]
consumeIdentity := Contramap(identity)(consumeInt)
consumeIdentity(42)
assert.Equal(t, 42, captured)
// Should behave identically to original consumer
consumeInt(100)
capturedDirect := captured
consumeIdentity(100)
capturedMapped := captured
assert.Equal(t, capturedDirect, capturedMapped)
})
t.Run("contravariant composition law", func(t *testing.T) {
// contramap(f . g) = contramap(g) . contramap(f)
var captured int
consumeInt := func(x int) {
captured = x
}
f := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
g := func(b bool) string {
if b {
return "1"
}
return "0"
}
// Compose f and g manually
fg := func(b bool) int {
return f(g(b))
}
// Method 1: contramap(f . g)
consumer1 := Contramap(fg)(consumeInt)
consumer1(true)
result1 := captured
// Method 2: contramap(g) . contramap(f)
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
consumer2(true)
result2 := captured
assert.Equal(t, result1, result2)
assert.Equal(t, 1, result1)
})
t.Run("type hierarchy adaptation", func(t *testing.T) {
type Animal struct {
Name string
}
type Dog struct {
Animal Animal
Breed string
}
var capturedName string
consumeAnimal := func(a Animal) {
capturedName = a.Name
}
dogToAnimal := func(d Dog) Animal {
return d.Animal
}
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
consumeDog(Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
})
assert.Equal(t, "Buddy", capturedName)
})
t.Run("field extraction with contramap", func(t *testing.T) {
type Message struct {
Text string
Timestamp time.Time
}
var capturedText string
consumeString := func(s string) {
capturedText = s
}
extractText := func(m Message) string {
return m.Text
}
consumeMessage := Contramap(extractText)(consumeString)
consumeMessage(Message{
Text: "Hello",
Timestamp: time.Now(),
})
assert.Equal(t, "Hello", capturedText)
})
t.Run("multiple contramap applications", func(t *testing.T) {
type Level3 struct{ Value int }
type Level2 struct{ L3 Level3 }
type Level1 struct{ L2 Level2 }
var captured int
consumeInt := func(x int) {
captured = x
}
extract3 := func(l3 Level3) int { return l3.Value }
extract2 := func(l2 Level2) Level3 { return l2.L3 }
extract1 := func(l1 Level1) Level2 { return l1.L2 }
// Chain contramap operations
consumeLevel3 := Contramap(extract3)(consumeInt)
consumeLevel2 := Contramap(extract2)(consumeLevel3)
consumeLevel1 := Contramap(extract1)(consumeLevel2)
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
assert.Equal(t, 42, captured)
})
t.Run("contramap with calculation", func(t *testing.T) {
type Rectangle struct {
Width int
Height int
}
var capturedArea int
consumeArea := func(area int) {
capturedArea = area
}
calculateArea := func(r Rectangle) int {
return r.Width * r.Height
}
consumeRectangle := Contramap(calculateArea)(consumeArea)
consumeRectangle(Rectangle{Width: 5, Height: 10})
assert.Equal(t, 50, capturedArea)
})
t.Run("contramap preserves side effects", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
contramappedConsumer := Contramap(transform)(consumer)
contramappedConsumer("1")
contramappedConsumer("2")
contramappedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("contramap with pointer types", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
dereference := func(p *int) int {
if p == nil {
return 0
}
return *p
}
consumePointer := Contramap(dereference)(consumeInt)
value := 42
consumePointer(&value)
assert.Equal(t, 42, captured)
consumePointer(nil)
assert.Equal(t, 0, captured)
})
t.Run("contramap equivalence with Local", func(t *testing.T) {
var capturedLocal, capturedContramap int
consumeIntLocal := func(x int) {
capturedLocal = x
}
consumeIntContramap := func(x int) {
capturedContramap = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// Both should produce identical results
consumerLocal := Local(transform)(consumeIntLocal)
consumerContramap := Contramap(transform)(consumeIntContramap)
consumerLocal("42")
consumerContramap("42")
assert.Equal(t, capturedLocal, capturedContramap)
assert.Equal(t, 42, capturedLocal)
})
}
func TestCompose(t *testing.T) {
t.Run("basic composition", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
parseToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Compose(parseToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("composing multiple transformations", func(t *testing.T) {
type Data struct {
Value string
}
type Wrapper struct {
Data Data
}
var captured string
consumeString := func(s string) {
captured = s
}
extractData := func(w Wrapper) Data { return w.Data }
extractValue := func(d Data) string { return d.Value }
// Compose step by step
consumeData := Compose(extractValue)(consumeString)
consumeWrapper := Compose(extractData)(consumeData)
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
assert.Equal(t, "Hello", captured)
})
t.Run("function composition style", func(t *testing.T) {
type Request struct {
Body []byte
}
var captured string
processString := func(s string) {
captured = s
}
bytesToString := func(b []byte) string {
return string(b)
}
extractBody := func(r Request) []byte {
return r.Body
}
// Chain compositions
processBytes := Compose(bytesToString)(processString)
processRequest := Compose(extractBody)(processBytes)
processRequest(Request{Body: []byte("test")})
assert.Equal(t, "test", captured)
})
t.Run("compose with identity", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
identity := function.Identity[int]
composedConsumer := Compose(identity)(consumeInt)
composedConsumer(42)
assert.Equal(t, 42, captured)
})
t.Run("compose with field extraction", func(t *testing.T) {
type User struct {
Name string
Email string
Age int
}
var capturedName string
consumeName := func(name string) {
capturedName = name
}
extractName := func(u User) string {
return u.Name
}
consumeUser := Compose(extractName)(consumeName)
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
assert.Equal(t, "Alice", capturedName)
})
t.Run("compose with calculation", func(t *testing.T) {
type Circle struct {
Radius float64
}
var capturedArea float64
consumeArea := func(area float64) {
capturedArea = area
}
calculateArea := func(c Circle) float64 {
return 3.14159 * c.Radius * c.Radius
}
consumeCircle := Compose(calculateArea)(consumeArea)
consumeCircle(Circle{Radius: 5.0})
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
})
t.Run("compose with slice operations", func(t *testing.T) {
var captured int
consumeLength := func(n int) {
captured = n
}
getLength := func(s []string) int {
return len(s)
}
consumeSlice := Compose(getLength)(consumeLength)
consumeSlice([]string{"a", "b", "c", "d"})
assert.Equal(t, 4, captured)
})
t.Run("compose with map operations", func(t *testing.T) {
var captured bool
consumeHasKey := func(has bool) {
captured = has
}
hasKey := func(m map[string]int) bool {
_, exists := m["key"]
return exists
}
consumeMap := Compose(hasKey)(consumeHasKey)
consumeMap(map[string]int{"key": 42})
assert.True(t, captured)
consumeMap(map[string]int{"other": 42})
assert.False(t, captured)
})
t.Run("compose preserves consumer behavior", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
composedConsumer := Compose(transform)(consumer)
composedConsumer("1")
composedConsumer("2")
composedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("compose with error handling", func(t *testing.T) {
type Result struct {
Value int
Error error
}
var captured int
consumeInt := func(x int) {
captured = x
}
extractValue := func(r Result) int {
if r.Error != nil {
return -1
}
return r.Value
}
consumeResult := Compose(extractValue)(consumeInt)
consumeResult(Result{Value: 42, Error: nil})
assert.Equal(t, 42, captured)
consumeResult(Result{Value: 100, Error: assert.AnError})
assert.Equal(t, -1, captured)
})
t.Run("compose equivalence with Local", func(t *testing.T) {
var capturedLocal, capturedCompose int
consumeIntLocal := func(x int) {
capturedLocal = x
}
consumeIntCompose := func(x int) {
capturedCompose = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// Both should produce identical results
consumerLocal := Local(transform)(consumeIntLocal)
consumerCompose := Compose(transform)(consumeIntCompose)
consumerLocal("42")
consumerCompose("42")
assert.Equal(t, capturedLocal, capturedCompose)
assert.Equal(t, 42, capturedLocal)
})
t.Run("compose equivalence with Contramap", func(t *testing.T) {
var capturedCompose, capturedContramap int
consumeIntCompose := func(x int) {
capturedCompose = x
}
consumeIntContramap := func(x int) {
capturedContramap = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// All three should produce identical results
consumerCompose := Compose(transform)(consumeIntCompose)
consumerContramap := Contramap(transform)(consumeIntContramap)
consumerCompose("42")
consumerContramap("42")
assert.Equal(t, capturedCompose, capturedContramap)
assert.Equal(t, 42, capturedCompose)
})
}

View File

@@ -44,7 +44,7 @@ import (
// return result.Of("done")
// }
//
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
//
// wrapped := WithContext(ctx, computation)

130
v2/context/reader/reader.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package reader provides a specialization of the Reader monad for [context.Context].
//
// This package offers a context-aware Reader monad that simplifies working with
// Go's [context.Context] in a functional programming style. It eliminates the need
// to explicitly thread context through function calls while maintaining type safety
// and composability.
//
// # Core Concept
//
// The Reader monad represents computations that depend on a shared environment.
// In this package, that environment is fixed to [context.Context], making it
// particularly useful for:
//
// - Request-scoped data propagation
// - Cancellation and timeout handling
// - Dependency injection via context values
// - Avoiding explicit context parameter threading
//
// # Type Definitions
//
// - Reader[A]: A computation that depends on context.Context and produces A
// - Kleisli[A, B]: A function from A to Reader[B] for composing computations
// - Operator[A, B]: A transformation from Reader[A] to Reader[B]
//
// # Usage Pattern
//
// Instead of passing context explicitly through every function:
//
// func processUser(ctx context.Context, userID string) (User, error) {
// user := fetchUser(ctx, userID)
// profile := fetchProfile(ctx, user.ProfileID)
// return enrichUser(ctx, user, profile), nil
// }
//
// You can use Reader to compose context-dependent operations:
//
// fetchUser := func(userID string) Reader[User] {
// return func(ctx context.Context) User {
// // Use ctx for database access, cancellation, etc.
// return queryDatabase(ctx, userID)
// }
// }
//
// processUser := func(userID string) Reader[User] {
// return F.Pipe2(
// fetchUser(userID),
// reader.Chain(func(user User) Reader[Profile] {
// return fetchProfile(user.ProfileID)
// }),
// reader.Map(func(profile Profile) User {
// return enrichUser(user, profile)
// }),
// )
// }
//
// // Execute with context
// ctx := context.Background()
// user := processUser("user123")(ctx)
//
// # Integration with Standard Library
//
// This package works seamlessly with Go's standard [context] package:
//
// - Context cancellation and deadlines are preserved
// - Context values can be accessed within Reader computations
// - Readers can be composed with context-aware libraries
//
// # Relationship to Other Packages
//
// This package is a specialization of [github.com/IBM/fp-go/v2/reader] where
// the environment type R is fixed to [context.Context]. For more general
// Reader operations, see the base reader package.
//
// For combining Reader with other monads:
// - [github.com/IBM/fp-go/v2/context/readerio]: Reader + IO effects
// - [github.com/IBM/fp-go/v2/readeroption]: Reader + Option
// - [github.com/IBM/fp-go/v2/readerresult]: Reader + Result (Either)
//
// # Example: HTTP Request Handler
//
// type RequestContext struct {
// UserID string
// RequestID string
// }
//
// // Extract request context from context.Context
// getRequestContext := func(ctx context.Context) RequestContext {
// return RequestContext{
// UserID: ctx.Value("userID").(string),
// RequestID: ctx.Value("requestID").(string),
// }
// }
//
// // A Reader that logs with request context
// logInfo := func(message string) Reader[function.Void] {
// return func(ctx context.Context) function.Void {
// reqCtx := getRequestContext(ctx)
// log.Printf("[%s] User %s: %s", reqCtx.RequestID, reqCtx.UserID, message)
// return function.VOID
// }
// }
//
// // Compose operations
// handleRequest := func(data string) Reader[Response] {
// return F.Pipe2(
// logInfo("Processing request"),
// reader.Chain(func(_ function.Void) Reader[Result] {
// return processData(data)
// }),
// reader.Map(func(result Result) Response {
// return Response{Data: result}
// }),
// )
// }
package reader

142
v2/context/reader/types.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reader
import (
"context"
R "github.com/IBM/fp-go/v2/reader"
)
type (
// Reader represents a computation that depends on a [context.Context] and produces a value of type A.
//
// This is a specialization of the generic Reader monad where the environment type is fixed
// to [context.Context]. This is particularly useful for Go applications that need to thread
// context through computations for cancellation, deadlines, and request-scoped values.
//
// Type Parameters:
// - A: The result type produced by the computation
//
// Reader[A] is equivalent to func(context.Context) A
//
// The Reader monad enables:
// - Dependency injection using context values
// - Cancellation and timeout handling
// - Request-scoped data propagation
// - Avoiding explicit context parameter threading
//
// Example:
//
// // A Reader that extracts a user ID from context
// getUserID := func(ctx context.Context) string {
// if userID, ok := ctx.Value("userID").(string); ok {
// return userID
// }
// return "anonymous"
// }
//
// // A Reader that checks if context is cancelled
// isCancelled := func(ctx context.Context) bool {
// select {
// case <-ctx.Done():
// return true
// default:
// return false
// }
// }
//
// // Use the readers with a context
// ctx := context.WithValue(context.Background(), "userID", "user123")
// userID := getUserID(ctx) // "user123"
// cancelled := isCancelled(ctx) // false
Reader[A any] = R.Reader[context.Context, A]
// Kleisli represents a Kleisli arrow for the context-based Reader monad.
//
// It's a function from A to Reader[B], used for composing Reader computations
// that all depend on the same [context.Context].
//
// Type Parameters:
// - A: The input type
// - B: The output type wrapped in Reader
//
// Kleisli[A, B] is equivalent to func(A) func(context.Context) B
//
// Kleisli arrows are fundamental for monadic composition, allowing you to chain
// operations that depend on context without explicitly passing the context through
// each function call.
//
// Example:
//
// // A Kleisli arrow that creates a greeting Reader from a name
// greet := func(name string) Reader[string] {
// return func(ctx context.Context) string {
// if deadline, ok := ctx.Deadline(); ok {
// return fmt.Sprintf("Hello %s (deadline: %v)", name, deadline)
// }
// return fmt.Sprintf("Hello %s", name)
// }
// }
//
// // Use the Kleisli arrow
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// greeting := greet("Alice")(ctx) // "Hello Alice (deadline: ...)"
Kleisli[A, B any] = R.Reader[A, Reader[B]]
// Operator represents a transformation from one Reader to another.
//
// It takes a Reader[A] and produces a Reader[B], where both readers depend on
// the same [context.Context]. This type is commonly used for operations like
// Map, Chain, and other transformations that convert readers while preserving
// the context dependency.
//
// Type Parameters:
// - A: The input Reader's result type
// - B: The output Reader's result type
//
// Operator[A, B] is equivalent to func(Reader[A]) func(context.Context) B
//
// Operators enable building pipelines of context-dependent computations where
// each step can transform the result of the previous computation while maintaining
// access to the shared context.
//
// Example:
//
// // An operator that transforms int readers to string readers
// intToString := func(r Reader[int]) Reader[string] {
// return func(ctx context.Context) string {
// value := r(ctx)
// return strconv.Itoa(value)
// }
// }
//
// // A Reader that extracts a timeout value from context
// getTimeout := func(ctx context.Context) int {
// if deadline, ok := ctx.Deadline(); ok {
// return int(time.Until(deadline).Seconds())
// }
// return 0
// }
//
// // Transform the Reader
// getTimeoutStr := intToString(getTimeout)
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// defer cancel()
// result := getTimeoutStr(ctx) // "30" (approximately)
Operator[A, B any] = Kleisli[Reader[A], B]
)

View File

@@ -1,6 +1,7 @@
package readerio
import (
"github.com/IBM/fp-go/v2/function"
RIO "github.com/IBM/fp-go/v2/readerio"
)
@@ -61,7 +62,7 @@ import (
//
// // Safely read file with automatic cleanup
// safeRead := Bracket(acquireFile, readFile, closeFile)
// result := safeRead(context.Background())()
// result := safeRead(t.Context())()
//
//go:inline
func Bracket[
@@ -73,3 +74,117 @@ func Bracket[
) ReaderIO[B] {
return RIO.Bracket(acquire, use, release)
}
// WithResource creates a higher-order function that manages a resource lifecycle for any operation.
// It returns a Kleisli arrow that takes a use function and automatically handles resource
// acquisition and cleanup using the bracket pattern.
//
// This is a more composable alternative to Bracket, allowing you to define resource management
// once and reuse it with different use functions. The resource is acquired when the returned
// Kleisli arrow is invoked, used by the provided function, and then released regardless of
// success or failure.
//
// Type Parameters:
// - A: The type of the resource to be managed
// - B: The type of the result produced by the use function
// - ANY: The type returned by the release function (typically ignored)
//
// Parameters:
// - onCreate: A ReaderIO that acquires/creates the resource
// - onRelease: A Kleisli arrow that releases/cleans up the resource
//
// Returns:
// - A Kleisli arrow that takes a use function and returns a ReaderIO managing the full lifecycle
//
// Example with database connection:
//
// // Define resource management once
// withDB := WithResource(
// // Acquire connection
// func(ctx context.Context) IO[*sql.DB] {
// return func() *sql.DB {
// db, _ := sql.Open("postgres", "connection-string")
// return db
// }
// },
// // Release connection
// func(db *sql.DB) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// db.Close()
// return nil
// }
// }
// },
// )
//
// // Reuse with different operations
// queryUsers := withDB(func(db *sql.DB) ReaderIO[[]User] {
// return func(ctx context.Context) IO[[]User] {
// return func() []User {
// // Query users from db
// return users
// }
// }
// })
//
// insertUser := withDB(func(db *sql.DB) ReaderIO[int64] {
// return func(ctx context.Context) IO[int64] {
// return func() int64 {
// // Insert user into db
// return userID
// }
// }
// })
//
// Example with file handling:
//
// withFile := WithResource(
// func(ctx context.Context) IO[*os.File] {
// return func() *os.File {
// f, _ := os.Open("data.txt")
// return f
// }
// },
// func(f *os.File) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// f.Close()
// return nil
// }
// }
// },
// )
//
// // Use for reading
// readContent := withFile(func(f *os.File) ReaderIO[string] {
// return func(ctx context.Context) IO[string] {
// return func() string {
// data, _ := io.ReadAll(f)
// return string(data)
// }
// }
// })
//
// // Use for getting file info
// getSize := withFile(func(f *os.File) ReaderIO[int64] {
// return func(ctx context.Context) IO[int64] {
// return func() int64 {
// info, _ := f.Stat()
// return info.Size()
// }
// }
// })
//
// Use Cases:
// - Database connections: Acquire connection, execute queries, close connection
// - File handles: Open file, read/write, close file
// - Network connections: Establish connection, transfer data, close connection
// - Locks: Acquire lock, perform critical section, release lock
// - Temporary resources: Create temp file/directory, use it, clean up
//
//go:inline
func WithResource[A, B, ANY any](
onCreate ReaderIO[A], onRelease Kleisli[A, ANY]) Kleisli[Kleisli[A, B], B] {
return function.Bind13of3(Bracket[A, B, ANY])(onCreate, function.Ignore2of2[B](onRelease))
}

View File

@@ -0,0 +1,454 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/io"
"github.com/stretchr/testify/assert"
)
// mockResource simulates a resource that tracks its lifecycle
type mockResource struct {
id int
acquired bool
released bool
used bool
}
// TestBracket_Success tests that Bracket properly manages resource lifecycle on success
func TestBracket_Success(t *testing.T) {
resource := &mockResource{id: 1}
// Acquire resource
acquire := func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
}
// Use resource
use := func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r.used = true
return "success"
}
}
}
// Release resource
release := func(r *mockResource, result string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
}
// Execute bracket
operation := Bracket(acquire, use, release)
result := operation(context.Background())()
// Verify lifecycle
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.used, "Resource should be used")
assert.True(t, resource.released, "Resource should be released")
assert.Equal(t, "success", result)
}
// TestBracket_MultipleResources tests managing multiple resources
func TestBracket_MultipleResources(t *testing.T) {
resource1 := &mockResource{id: 1}
resource2 := &mockResource{id: 2}
acquire1 := func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource1.acquired = true
return resource1
}
}
use1 := func(r1 *mockResource) ReaderIO[*mockResource] {
return func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
r1.used = true
resource2.acquired = true
return resource2
}
}
}
release1 := func(r1 *mockResource, result string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r1.released = true
return nil
}
}
}
// Nested bracket for second resource
use2 := func(r2 *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r2.used = true
return "both used"
}
}
}
release2 := func(r2 *mockResource, result string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r2.released = true
return nil
}
}
}
// Compose brackets
operation := Bracket(acquire1, func(r1 *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
r2 := use1(r1)(ctx)()
return Bracket(
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource { return r2 }
},
use2,
release2,
)(ctx)
}
}, release1)
result := operation(context.Background())()
assert.True(t, resource1.acquired)
assert.True(t, resource1.used)
assert.True(t, resource1.released)
assert.True(t, resource2.acquired)
assert.True(t, resource2.used)
assert.True(t, resource2.released)
assert.Equal(t, "both used", result)
}
// TestWithResource_Success tests WithResource with successful operation
func TestWithResource_Success(t *testing.T) {
resource := &mockResource{id: 1}
// Define resource management
withResource := WithResource[*mockResource, string, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// Use resource
operation := withResource(func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r.used = true
return "result"
}
}
})
result := operation(context.Background())()
assert.True(t, resource.acquired)
assert.True(t, resource.used)
assert.True(t, resource.released)
assert.Equal(t, "result", result)
}
// TestWithResource_Reusability tests that WithResource can be reused with different operations
func TestWithResource_Reusability(t *testing.T) {
callCount := 0
withResource := WithResource[*mockResource, int, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
callCount++
return &mockResource{id: callCount, acquired: true}
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// First operation
op1 := withResource(func(r *mockResource) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
r.used = true
return r.id * 2
}
}
})
result1 := op1(context.Background())()
assert.Equal(t, 2, result1)
assert.Equal(t, 1, callCount)
// Second operation (should create new resource)
op2 := withResource(func(r *mockResource) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
r.used = true
return r.id * 3
}
}
})
result2 := op2(context.Background())()
assert.Equal(t, 6, result2)
assert.Equal(t, 2, callCount)
}
// TestWithResource_DifferentResultTypes tests WithResource with different result types
func TestWithResource_DifferentResultTypes(t *testing.T) {
resource := &mockResource{id: 42}
withResourceInt := WithResource[*mockResource, int, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// Operation returning int
opInt := withResourceInt(func(r *mockResource) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
return r.id
}
}
})
resultInt := opInt(context.Background())()
assert.Equal(t, 42, resultInt)
// Reset resource state
resource.acquired = false
resource.released = false
// Create new WithResource for string type
withResourceString := WithResource[*mockResource, string, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// Operation returning string
opString := withResourceString(func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
return "value"
}
}
})
resultString := opString(context.Background())()
assert.Equal(t, "value", resultString)
assert.True(t, resource.released)
}
// TestWithResource_ContextPropagation tests that context is properly propagated
func TestWithResource_ContextPropagation(t *testing.T) {
type contextKey string
const key contextKey = "test-key"
withResource := WithResource[string, string, any](
func(ctx context.Context) io.IO[string] {
return func() string {
value := ctx.Value(key)
if value != nil {
return value.(string)
}
return "no-value"
}
},
func(r string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
return nil
}
}
},
)
operation := withResource(func(r string) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
return r + "-processed"
}
}
})
ctx := context.WithValue(context.Background(), key, "test-value")
result := operation(ctx)()
assert.Equal(t, "test-value-processed", result)
}
// TestWithResource_ErrorInRelease tests behavior when release function encounters an error
func TestWithResource_ErrorInRelease(t *testing.T) {
resource := &mockResource{id: 1}
releaseError := errors.New("release failed")
withResource := WithResource[*mockResource, string, error](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[error] {
return func(ctx context.Context) io.IO[error] {
return func() error {
r.released = true
return releaseError
}
}
},
)
operation := withResource(func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r.used = true
return "success"
}
}
})
result := operation(context.Background())()
// Operation should succeed even if release returns error
assert.Equal(t, "success", result)
assert.True(t, resource.acquired)
assert.True(t, resource.used)
assert.True(t, resource.released)
}
// BenchmarkBracket benchmarks the Bracket function
func BenchmarkBracket(b *testing.B) {
acquire := func(ctx context.Context) io.IO[int] {
return func() int {
return 42
}
}
use := func(n int) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
return n * 2
}
}
}
release := func(n int, result int) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
return nil
}
}
}
operation := Bracket(acquire, use, release)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
operation(ctx)()
}
}
// BenchmarkWithResource benchmarks the WithResource function
func BenchmarkWithResource(b *testing.B) {
withResource := WithResource[int, int, any](
func(ctx context.Context) io.IO[int] {
return func() int {
return 42
}
},
func(n int) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
return nil
}
}
},
)
operation := withResource(func(n int) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
return n * 2
}
}
})
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
operation(ctx)()
}
}

View File

@@ -50,7 +50,7 @@ import (
// // Sequence it to apply Config first
// sequenced := SequenceReader[Config, int](getMultiplier)
// cfg := Config{Timeout: 30}
// result := sequenced(cfg)(context.Background())() // Returns 60
// result := sequenced(cfg)(t.Context())() // Returns 60
//
//go:inline
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
@@ -107,7 +107,7 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
//
// // Provide Config to get final result
// cfg := Config{Multiplier: 5}
// finalResult := result(cfg)(context.Background())() // Returns 50
// finalResult := result(cfg)(t.Context())() // Returns 50
//
//go:inline
func TraverseReader[R, A, B any](

View File

@@ -81,7 +81,7 @@ func SLogWithCallback[A any](
// Chain(SLog[string]("Extracted name")),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Logs: "Extracted name" value="Alice"
//

View File

@@ -19,6 +19,9 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
RIO "github.com/IBM/fp-go/v2/readerio"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
@@ -33,21 +36,24 @@ import (
// The function f returns both a new context and a CancelFunc that should be called to release resources.
//
// Type Parameters:
// - R: The input environment type that f transforms into context.Context
// - A: The original result type produced by the ReaderIO
// - B: The new output result type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - f: Function to transform the input environment R into context.Context (contravariant)
// - g: Function to transform the output value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
// - A Kleisli arrow that takes a ReaderIO[A] and returns a function from R to B
//
// Note: When R is context.Context, this simplifies to an Operator[A, B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RIO.Kleisli[R, ReaderIO[A], B] {
return function.Flow2(
Local[A](f),
Map(g),
RIO.Map[R](g),
)
}
@@ -61,14 +67,87 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
//
// Type Parameters:
// - A: The result type (unchanged)
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
// - A Kleisli arrow that takes a ReaderIO[A] and returns a function from R to A
//
// Note: When R is context.Context, this simplifies to an Operator[A, A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
return Local[A](f)
}
// LocalIOK transforms the context using an IO effect before passing it to a ReaderIO computation.
//
// This is similar to Local, but the context transformation itself is wrapped in an IO effect,
// allowing for side-effectful context transformations. The transformation function receives
// the current context and returns an IO effect that produces a new context along with a
// cancel function. The cancel function is automatically called when the computation completes
// (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Context transformations that require side effects (e.g., loading configuration)
// - Lazy initialization of context values
// - Context transformations that may fail or need to perform I/O
// - Composing effectful context setup with computations
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - f: An IO Kleisli arrow that transforms the context with side effects
//
// Returns:
// - An Operator that runs the computation with the effectfully transformed context
//
// Example:
//
// import (
// "context"
// G "github.com/IBM/fp-go/v2/io"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Context transformation with side effects (e.g., loading config)
// loadConfig := func(ctx context.Context) G.IO[ContextCancel] {
// return func() ContextCancel {
// // Simulate loading configuration
// config := loadConfigFromFile()
// newCtx := context.WithValue(ctx, "config", config)
// return pair.MakePair[context.CancelFunc](func() {}, newCtx)
// }
// }
//
// getValue := readerio.FromReader(func(ctx context.Context) string {
// if cfg := ctx.Value("config"); cfg != nil {
// return cfg.(string)
// }
// return "default"
// })
//
// result := F.Pipe1(
// getValue,
// readerio.LocalIOK[string](loadConfig),
// )
// value := result(t.Context())() // Loads config and uses it
//
// Comparison with Local:
// - Local: Takes a pure function that transforms the context
// - LocalIOK: Takes an IO effect that transforms the context, allowing side effects
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return func(r ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
p := f(ctx)
return func() A {
otherCancel, otherCtx := pair.Unpack(p())
defer otherCancel()
return r(otherCtx)()
}
}
}
}

View File

@@ -21,6 +21,7 @@ import (
"testing"
"time"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -38,14 +39,14 @@ func TestPromapBasic(t *testing.T) {
}
// Transform context and result
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) ContextCancel {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, "42", result)
})
@@ -63,13 +64,13 @@ func TestContramapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) ContextCancel {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, 100, result)
})
@@ -85,13 +86,92 @@ func TestLocalBasic(t *testing.T) {
}
}
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, time.Second)
addTimeout := func(ctx context.Context) ContextCancel {
newCtx, cancelFct := context.WithTimeout(ctx, time.Second)
return pair.MakePair(cancelFct, newCtx)
}
adapted := Local[bool](addTimeout)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.True(t, result)
})
}
// TestLocalIOKBasic tests basic LocalIOK functionality
func TestLocalIOKBasic(t *testing.T) {
t.Run("context transformation with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IO[string] {
return func() string {
if v := ctx.Value("key"); v != nil {
return v.(string)
}
return "default"
}
}
// Context transformation wrapped in IO effect
addKeyIO := func(ctx context.Context) IO[ContextCancel] {
return func() ContextCancel {
// Simulate side effect (e.g., loading config)
newCtx := context.WithValue(ctx, "key", "loaded-value")
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
}
adapted := LocalIOK[string](addKeyIO)(getValue)
result := adapted(t.Context())()
assert.Equal(t, "loaded-value", result)
})
t.Run("cleanup function is called", func(t *testing.T) {
cleanupCalled := false
getValue := func(ctx context.Context) IO[int] {
return func() int {
if v := ctx.Value("value"); v != nil {
return v.(int)
}
return 0
}
}
addValueIO := func(ctx context.Context) IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "value", 42)
cleanup := context.CancelFunc(func() {
cleanupCalled = true
})
return pair.MakePair(cleanup, newCtx)
}
}
adapted := LocalIOK[int](addValueIO)(getValue)
result := adapted(t.Context())()
assert.Equal(t, 42, result)
assert.True(t, cleanupCalled, "cleanup function should be called")
})
t.Run("works with timeout context", func(t *testing.T) {
getValue := func(ctx context.Context) IO[bool] {
return func() bool {
_, hasDeadline := ctx.Deadline()
return hasDeadline
}
}
addTimeoutIO := func(ctx context.Context) IO[ContextCancel] {
return func() ContextCancel {
newCtx, cancelFct := context.WithTimeout(ctx, time.Second)
return pair.MakePair(cancelFct, newCtx)
}
}
adapted := LocalIOK[bool](addTimeoutIO)(getValue)
result := adapted(t.Context())()
assert.True(t, result, "context should have deadline")
})
}

View File

@@ -20,6 +20,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
)
@@ -594,7 +595,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
// )
//
// // Create context with side effects (e.g., loading config)
// createContext := G.Of(context.WithValue(context.Background(), "key", "value"))
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
//
// // A computation that uses the context
// getValue := readerio.FromReader(func(ctx context.Context) string {
@@ -633,12 +634,15 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
//
// Type Parameters:
// - A: The value type of the ReaderIO
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
// - A Kleisli arrow that runs the computation with the transformed context
//
// Note: When R is context.Context, this simplifies to an Operator[A, A]
//
// Example:
//
@@ -648,9 +652,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
// type key int
// const userKey key = 0
//
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// addUser := readerio.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// return pair.MakePair(func() {}, newCtx) // No-op cancel
// })
//
// getUser := readerio.FromReader(func(ctx context.Context) string {
@@ -664,24 +668,25 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
// user := result(t.Context())() // Returns "Alice"
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// withTimeout := readerio.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
// return pair.MakePair(cancel, newCtx)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
return func(rr ReaderIO[A]) RIO.ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
otherCtx, otherCancel := f(ctx)
otherCancel, otherCtx := pair.Unpack(f(r))
defer otherCancel()
return rr(otherCtx)()
}
@@ -731,7 +736,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
// data := result(t.Context())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
@@ -740,10 +745,11 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
// data := result(t.Context())() // Returns Data{Value: "quick"}
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
return Local[A](func(ctx context.Context) ContextCancel {
newCtx, cancelFct := context.WithTimeout(ctx, timeout)
return pair.MakePair(cancelFct, newCtx)
})
}
@@ -791,12 +797,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
// data := result(t.Context())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
@@ -806,8 +812,9 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// )
// data := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
return Local[A](func(ctx context.Context) ContextCancel {
newCtx, cancelFct := context.WithDeadline(ctx, deadline)
return pair.MakePair(cancelFct, newCtx)
})
}

View File

@@ -31,7 +31,7 @@ func TestMonadMap(t *testing.T) {
rio := Of(5)
doubled := MonadMap(rio, N.Mul(2))
result := doubled(context.Background())()
result := doubled(t.Context())()
assert.Equal(t, 10, result)
}
@@ -41,14 +41,14 @@ func TestMap(t *testing.T) {
Map(utils.Double),
)
assert.Equal(t, 2, g(context.Background())())
assert.Equal(t, 2, g(t.Context())())
}
func TestMonadMapTo(t *testing.T) {
rio := Of(42)
replaced := MonadMapTo(rio, "constant")
result := replaced(context.Background())()
result := replaced(t.Context())()
assert.Equal(t, "constant", result)
}
@@ -58,7 +58,7 @@ func TestMapTo(t *testing.T) {
MapTo[int]("constant"),
)
assert.Equal(t, "constant", result(context.Background())())
assert.Equal(t, "constant", result(t.Context())())
}
func TestMonadChain(t *testing.T) {
@@ -67,7 +67,7 @@ func TestMonadChain(t *testing.T) {
return Of(n * 3)
})
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestChain(t *testing.T) {
@@ -78,7 +78,7 @@ func TestChain(t *testing.T) {
}),
)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestMonadChainFirst(t *testing.T) {
@@ -89,7 +89,7 @@ func TestMonadChainFirst(t *testing.T) {
return Of("side effect")
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -104,7 +104,7 @@ func TestChainFirst(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -117,7 +117,7 @@ func TestMonadTap(t *testing.T) {
return Of(func() {})
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -132,14 +132,14 @@ func TestTap(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestOf(t *testing.T) {
rio := Of(100)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 100, result)
}
@@ -149,7 +149,7 @@ func TestMonadAp(t *testing.T) {
faIO := Of(5)
result := MonadAp(fabIO, faIO)
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestAp(t *testing.T) {
@@ -158,7 +158,7 @@ func TestAp(t *testing.T) {
Ap[int](Of(1)),
)
assert.Equal(t, 2, g(context.Background())())
assert.Equal(t, 2, g(t.Context())())
}
func TestMonadApSeq(t *testing.T) {
@@ -166,7 +166,7 @@ func TestMonadApSeq(t *testing.T) {
faIO := Of(5)
result := MonadApSeq(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestApSeq(t *testing.T) {
@@ -175,7 +175,7 @@ func TestApSeq(t *testing.T) {
ApSeq[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
assert.Equal(t, 15, g(t.Context())())
}
func TestMonadApPar(t *testing.T) {
@@ -183,7 +183,7 @@ func TestMonadApPar(t *testing.T) {
faIO := Of(5)
result := MonadApPar(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestApPar(t *testing.T) {
@@ -192,12 +192,12 @@ func TestApPar(t *testing.T) {
ApPar[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
assert.Equal(t, 15, g(t.Context())())
}
func TestAsk(t *testing.T) {
rio := Ask()
ctx := context.WithValue(context.Background(), "key", "value")
ctx := context.WithValue(t.Context(), "key", "value")
result := rio(ctx)()
assert.Equal(t, ctx, result)
@@ -207,7 +207,7 @@ func TestFromIO(t *testing.T) {
ioAction := G.Of(42)
rio := FromIO(ioAction)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -217,7 +217,7 @@ func TestFromReader(t *testing.T) {
}
rio := FromReader(rdr)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -226,7 +226,7 @@ func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
rio := FromLazy(lazy)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -236,7 +236,7 @@ func TestMonadChainIOK(t *testing.T) {
return G.Of(n * 4)
})
assert.Equal(t, 20, result(context.Background())())
assert.Equal(t, 20, result(t.Context())())
}
func TestChainIOK(t *testing.T) {
@@ -247,7 +247,7 @@ func TestChainIOK(t *testing.T) {
}),
)
assert.Equal(t, 20, result(context.Background())())
assert.Equal(t, 20, result(t.Context())())
}
func TestMonadChainFirstIOK(t *testing.T) {
@@ -258,7 +258,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
return G.Of("side effect")
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -273,7 +273,7 @@ func TestChainFirstIOK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -286,7 +286,7 @@ func TestMonadTapIOK(t *testing.T) {
return G.Of(func() {})
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -301,7 +301,7 @@ func TestTapIOK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -313,8 +313,8 @@ func TestDefer(t *testing.T) {
return Of(counter)
})
result1 := rio(context.Background())()
result2 := rio(context.Background())()
result1 := rio(t.Context())()
result2 := rio(t.Context())()
assert.Equal(t, 1, result1)
assert.Equal(t, 2, result2)
@@ -328,8 +328,8 @@ func TestMemoize(t *testing.T) {
return counter
}))
result1 := memoized(context.Background())()
result2 := memoized(context.Background())()
result1 := memoized(t.Context())()
result2 := memoized(t.Context())()
assert.Equal(t, 1, result1)
assert.Equal(t, 1, result2) // Same value, memoized
@@ -339,7 +339,7 @@ func TestFlatten(t *testing.T) {
nested := Of(Of(42))
flattened := Flatten(nested)
result := flattened(context.Background())()
result := flattened(t.Context())()
assert.Equal(t, 42, result)
}
@@ -347,7 +347,7 @@ func TestMonadFlap(t *testing.T) {
fabIO := Of(N.Mul(3))
result := MonadFlap(fabIO, 7)
assert.Equal(t, 21, result(context.Background())())
assert.Equal(t, 21, result(t.Context())())
}
func TestFlap(t *testing.T) {
@@ -356,7 +356,7 @@ func TestFlap(t *testing.T) {
Flap[int](7),
)
assert.Equal(t, 21, result(context.Background())())
assert.Equal(t, 21, result(t.Context())())
}
func TestMonadChainReaderK(t *testing.T) {
@@ -365,7 +365,7 @@ func TestMonadChainReaderK(t *testing.T) {
return func(ctx context.Context) int { return n * 2 }
})
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestChainReaderK(t *testing.T) {
@@ -376,7 +376,7 @@ func TestChainReaderK(t *testing.T) {
}),
)
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestMonadChainFirstReaderK(t *testing.T) {
@@ -389,7 +389,7 @@ func TestMonadChainFirstReaderK(t *testing.T) {
}
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -406,7 +406,7 @@ func TestChainFirstReaderK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -421,7 +421,7 @@ func TestMonadTapReaderK(t *testing.T) {
}
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -438,14 +438,14 @@ func TestTapReaderK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestRead(t *testing.T) {
rio := Of(42)
ctx := context.Background()
ctx := t.Context()
ioAction := Read[int](ctx)(rio)
result := ioAction()
@@ -463,7 +463,7 @@ func TestComplexPipeline(t *testing.T) {
Map(N.Add(10)),
)
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
assert.Equal(t, 20, result(t.Context())()) // (5 * 2) + 10 = 20
}
func TestFromIOWithChain(t *testing.T) {
@@ -476,7 +476,7 @@ func TestFromIOWithChain(t *testing.T) {
}),
)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestTapWithLogging(t *testing.T) {
@@ -496,14 +496,14 @@ func TestTapWithLogging(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 84, value)
assert.Equal(t, []int{42, 84}, logged)
}
func TestReadIO(t *testing.T) {
// Test basic ReadIO functionality
contextIO := G.Of(context.WithValue(context.Background(), "testKey", "testValue"))
contextIO := G.Of(context.WithValue(t.Context(), "testKey", "testValue"))
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("testKey"); val != nil {
return val.(string)
@@ -519,7 +519,7 @@ func TestReadIO(t *testing.T) {
func TestReadIOWithBackground(t *testing.T) {
// Test ReadIO with plain background context
contextIO := G.Of(context.Background())
contextIO := G.Of(t.Context())
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
@@ -530,7 +530,7 @@ func TestReadIOWithBackground(t *testing.T) {
func TestReadIOWithChain(t *testing.T) {
// Test ReadIO with chained operations
contextIO := G.Of(context.WithValue(context.Background(), "multiplier", 3))
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
result := F.Pipe1(
FromReader(func(ctx context.Context) int {
@@ -552,7 +552,7 @@ func TestReadIOWithChain(t *testing.T) {
func TestReadIOWithMap(t *testing.T) {
// Test ReadIO with Map operations
contextIO := G.Of(context.Background())
contextIO := G.Of(t.Context())
result := F.Pipe2(
Of(5),
@@ -571,7 +571,7 @@ func TestReadIOWithSideEffects(t *testing.T) {
counter := 0
contextIO := func() context.Context {
counter++
return context.WithValue(context.Background(), "counter", counter)
return context.WithValue(t.Context(), "counter", counter)
}
rio := FromReader(func(ctx context.Context) int {
@@ -593,7 +593,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
counter := 0
contextIO := func() context.Context {
counter++
return context.Background()
return t.Context()
}
rio := Of(42)
@@ -609,7 +609,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
func TestReadIOComparisonWithRead(t *testing.T) {
// Compare ReadIO with Read to show the difference
ctx := context.WithValue(context.Background(), "key", "value")
ctx := context.WithValue(t.Context(), "key", "value")
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("key"); val != nil {
@@ -642,7 +642,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
contextIO := G.Of(
context.WithValue(
context.WithValue(context.Background(), userKey, "Alice"),
context.WithValue(t.Context(), userKey, "Alice"),
tokenKey,
"secret123",
),
@@ -668,7 +668,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
func TestReadIOWithAsk(t *testing.T) {
// Test ReadIO combined with Ask
contextIO := G.Of(context.WithValue(context.Background(), "data", 100))
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
result := F.Pipe1(
Ask(),

View File

@@ -53,7 +53,7 @@ import (
// }
//
// countdown := TailRec(countdownStep)
// result := countdown(10)(context.Background())() // Returns "Done!"
// result := countdown(10)(t.Context())() // Returns "Done!"
//
// Example - Sum with context:
//
@@ -77,7 +77,7 @@ import (
// }
//
// sum := TailRec(sumStep)
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(t.Context())()
// // Returns 15, safe even for very large slices
//
//go:inline

View File

@@ -80,7 +80,7 @@ import (
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute
// ctx := context.Background()
// ctx := t.Context()
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
//
//go:inline

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
@@ -81,4 +82,15 @@ type (
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

View File

@@ -56,7 +56,7 @@ This creates several problems:
```go
computation := getComputation()
ctx := context.Background()
ctx := t.Context()
cfg := Config{Value: 42}
// Must apply in this specific order
@@ -176,7 +176,7 @@ db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db) // ✅ Database injected
// Use query with any context
result := query(context.Background())()
result := query(t.Context())()
```
### 3. Point-Free Composition
@@ -289,7 +289,7 @@ withConfig := traversed(getValue)
// Now we can provide Config to get the final result
cfg := Config{Multiplier: 5, Prefix: "Result"}
ctx := context.Background()
ctx := t.Context()
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
```

View File

@@ -21,6 +21,7 @@ import (
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/pair"
)
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
@@ -74,7 +75,7 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
// safeFetch := WithContextK(fetchUser)
//
// // If context is cancelled, returns immediately without executing fetchUser
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
//
@@ -85,3 +86,7 @@ func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
WithContext,
)
}
func pairFromContextCancel(newCtx context.Context, cancelFct context.CancelFunc) ContextCancel {
return pair.MakePair(cancelFct, newCtx)
}

View File

@@ -187,7 +187,7 @@ func main() {
result := cb(env)
// Execute the protected operation
ctx := context.Background()
ctx := t.Context()
protectedOp := pair.Tail(result)
outcome := protectedOp(ctx)()
}

View File

@@ -113,7 +113,7 @@
// }
//
// // Execute the computation
// ctx := context.Background()
// ctx := t.Context()
// result := fetchUser("123")(ctx)()
// // result is Either[error, User]
//
@@ -161,7 +161,7 @@
// All operations respect context cancellation. When a context is cancelled, operations
// will return an error containing the cancellation cause:
//
// ctx, cancel := context.WithCancelCause(context.Background())
// ctx, cancel := context.WithCancelCause(t.Context())
// cancel(errors.New("operation cancelled"))
// result := computation(ctx)() // Returns Left with cancellation error
//

View File

@@ -37,7 +37,7 @@ import (
// return either.Eq(eq.FromEquals(func(x, y int) bool { return x == y }))(a, b)
// })
// eqRIE := Eq(eqInt)
// ctx := context.Background()
// ctx := t.Context()
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
//
//go:inline

View File

@@ -13,6 +13,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides context-aware file operations that integrate with the ReaderIOResult monad.
// It offers safe, composable file I/O operations that respect context cancellation and properly
// manage resources using the RAII pattern.
//
// All operations in this package:
// - Respect context.Context for cancellation and timeouts
// - Return ReaderIOResult for composable error handling
// - Automatically manage resource cleanup
// - Are safe to use in concurrent environments
//
// # Example Usage
//
// // Read a file with automatic resource management
// readOp := ReadFile("data.txt")
// result := readOp(ctx)()
//
// // Open and manually manage a file
// fileOp := Open("config.json")
// fileResult := fileOp(ctx)()
package file
import (
@@ -29,32 +48,181 @@ import (
)
var (
// Open opens a file for reading within the given context
// Open opens a file for reading within the given context.
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or ReadFile.
//
// Parameters:
// - path: The path to the file to open
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that opens the file
//
// Example:
//
// openFile := Open("data.txt")
// result := openFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// // Use file...
// },
// )
//
// See Also:
// - ReadFile: For reading entire file contents with automatic resource management
// - Close: For closing file handles
Open = F.Flow3(
IOEF.Open,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name
// Create creates or truncates a file for writing within the given context.
// If the file already exists, it is truncated. If it doesn't exist, it is created
// with mode 0666 (before umask).
//
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or WriteFile.
//
// Parameters:
// - path: The path to the file to create or truncate
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
//
// Example:
//
// createFile := Create("output.txt")
// result := createFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// f.WriteString("Hello, World!")
// },
// )
//
// See Also:
// - WriteFile: For writing data to a file with automatic resource management
// - Open: For opening files for reading
// - Close: For closing file handles
Create = F.Flow3(
IOEF.Create,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name.
// The operation returns the filename on success, allowing for easy composition
// with other file operations.
//
// Parameters:
// - name: The path to the file to remove
//
// Returns:
// - ReaderIOResult[string]: A computation that removes the file and returns its name
//
// Example:
//
// removeOp := Remove("temp.txt")
// result := removeOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Failed to remove: %v", err) },
// func(name string) { log.Printf("Removed: %s", name) },
// )
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading file contents
Remove = F.Flow2(
IOEF.Remove,
RIOE.FromIOEither[string],
)
)
// Close closes an object
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
// Close closes an io.Closer resource and returns a ReaderIOResult.
// This function is generic and works with any type that implements io.Closer,
// including os.File, network connections, and other closeable resources.
//
// The function captures any error that occurs during closing and returns it
// as part of the ReaderIOResult. On success, it returns Void (empty struct).
//
// Type Parameters:
// - C: Any type that implements io.Closer
//
// Parameters:
// - c: The resource to close
//
// Returns:
// - ReaderIOResult[Void]: A computation that closes the resource
//
// Example:
//
// file, _ := os.Open("data.txt")
// closeOp := Close(file)
// result := closeOp(ctx)()
//
// Note: This function is typically used with WithResource for automatic resource management
// rather than being called directly.
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading files with automatic closing
func Close[C io.Closer](c C) ReaderIOResult[Void] {
return F.Pipe2(
c,
IOEF.Close[C],
RIOE.FromIOEither[struct{}],
RIOE.FromIOEither[Void],
)
}
// ReadFile reads a file in the scope of a context
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
// ReadFile reads the entire contents of a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// The operation:
// - Opens the file for reading
// - Reads all contents into a byte slice
// - Automatically closes the file when done
// - Respects context cancellation during the read operation
//
// Parameters:
// - path: The path to the file to read
//
// Returns:
// - ReaderIOResult[[]byte]: A computation that reads the file contents
//
// Example:
//
// readOp := ReadFile("config.json")
// result := readOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Read error: %v", err) },
// func(data []byte) { log.Printf("Read %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// ReadFile(path) = WithResource(Open(path), Close)(readAllBytes)
//
// See Also:
// - Open: For opening files without automatic reading
// - Close: For closing file handles
// - WithResource: For custom resource management patterns
func ReadFile(path string) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) ReaderIOResult[[]byte] {
return func(ctx context.Context) IOE.IOEither[error, []byte] {
return func() ET.Either[error, []byte] {
return file.ReadAll(ctx, r)
@@ -62,3 +230,48 @@ func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
}
})
}
// WriteFile writes data to a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// If the file doesn't exist, it is created with mode 0666 (before umask).
// If the file already exists, it is truncated before writing.
//
// The operation:
// - Creates or truncates the file for writing
// - Writes all data to the file
// - Automatically closes the file when done
// - Respects context cancellation during the write operation
//
// Parameters:
// - data: The byte slice to write to the file
//
// Returns:
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
// that writes the data and returns the written bytes on success
//
// Example:
//
// writeOp := WriteFile([]byte("Hello, World!"))
// result := writeOp("output.txt")(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Write error: %v", err) },
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// WriteFile(data) = Create >> WriteAll(data) >> Close
//
// See Also:
// - ReadFile: For reading file contents with automatic resource management
// - Create: For creating files without automatic writing
// - WriteAll: For writing to an already-open file handle
func WriteFile(data []byte) Kleisli[string, []byte] {
return F.Flow2(
Create,
WriteAll[*os.File](data),
)
}

View File

@@ -18,11 +18,16 @@ package file
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"
"github.com/stretchr/testify/assert"
)
type RecordType struct {
@@ -49,3 +54,267 @@ func ExampleReadFile() {
// Output:
// Right[string](Carsten)
}
func TestCreate(t *testing.T) {
ctx := context.Background()
t.Run("Success - creates new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was created
_, err := os.Stat(tempFile)
assert.NoError(t, err)
// Clean up file handle
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
})
t.Run("Success - truncates existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
// Create file with initial content
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
assert.NoError(t, err)
// Create should truncate
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Close the file
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
// Verify file was truncated
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
// Try to create file in non-existent directory
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
createOp := Create(invalidPath)
result := createOp(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - file can be written to", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Write to the file
E.MonadFold(result,
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
func(f *os.File) *os.File {
defer f.Close()
_, err := f.WriteString("test content")
assert.NoError(t, err)
return f
},
)
// Verify content was written
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, "test content", string(content))
})
t.Run("Context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
createOp := Create(tempFile)
result := createOp(cancelCtx)()
// Note: File creation itself doesn't check context, but this tests the pattern
// In practice, context cancellation would affect subsequent operations
_ = result
})
}
func TestWriteFile(t *testing.T) {
ctx := context.Background()
t.Run("Success - writes data to new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
testData := []byte("Hello, World!")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify returned data
E.MonadFold(result,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("Success - overwrites existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
// Write initial content
err := os.WriteFile(tempFile, []byte("old content"), 0644)
assert.NoError(t, err)
// Overwrite with new content
newData := []byte("new content")
writeOp := WriteFile(newData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was overwritten
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, newData, content)
})
t.Run("Success - writes empty data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
emptyData := []byte{}
writeOp := WriteFile(emptyData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file is empty
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Success - writes large data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
largeData := make([]byte, 1024*1024) // 1MB
for i := range largeData {
largeData[i] = byte(i % 256)
}
writeOp := WriteFile(largeData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, largeData, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(invalidPath)(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - writes binary data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
writeOp := WriteFile(binaryData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify binary content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, binaryData, content)
})
t.Run("Integration - write then read", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
testData := []byte("Round trip test data")
// Write data
writeOp := WriteFile(testData)
writeResult := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(writeResult))
// Read data back
readOp := ReadFile(tempFile)
readResult := readOp(ctx)()
assert.True(t, E.IsRight(readResult))
// Verify data matches
E.MonadFold(readResult,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
})
t.Run("Composition with Map", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
testData := []byte("test data")
// Write and transform result
pipeline := F.Pipe1(
WriteFile(testData)(tempFile),
R.Map(func(data []byte) int { return len(data) }),
)
result := pipeline(ctx)()
assert.True(t, E.IsRight(result))
E.MonadFold(result,
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
func(length int) int {
assert.Equal(t, len(testData), length)
return length
},
)
})
t.Run("Context cancellation during write", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(cancelCtx)()
// Note: The actual write may complete before cancellation is checked
// This test verifies the pattern works with cancelled contexts
_ = result
})
}

View File

@@ -38,7 +38,7 @@ var (
)
// CreateTemp created a temp file with proper parametrization
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
func CreateTemp(dir, pattern string) ReaderIOResult[*os.File] {
return F.Pipe2(
IOEF.CreateTemp(dir, pattern),
RIOE.FromIOEither[*os.File],
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
}
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
func WithTempFile[A any](f Kleisli[*os.File, A]) ReaderIOResult[A] {
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -16,7 +16,6 @@
package file
import (
"context"
"os"
"testing"
@@ -30,12 +29,12 @@ func TestWithTempFile(t *testing.T) {
res := WithTempFile(onWriteAll[*os.File]([]byte("Carsten")))
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
}
func TestWithTempFileOnClosedFile(t *testing.T) {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
res := WithTempFile(func(f *os.File) ReaderIOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),
@@ -43,5 +42,5 @@ func TestWithTempFileOnClosedFile(t *testing.T) {
)
})
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
)
type (
// ReaderIOResult represents a context-aware computation that performs side effects
// and can fail with an error. This is the main type used throughout the file package
// for all file operations.
//
// ReaderIOResult[A] is equivalent to:
// func(context.Context) func() Either[error, A]
//
// The computation:
// - Takes a context.Context for cancellation and timeouts
// - Performs side effects (IO operations)
// - Returns Either an error or a value of type A
//
// See Also:
// - readerioresult.ReaderIOResult: The underlying type definition
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Void represents the absence of a meaningful value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful result.
//
// Void is typically used as the success type in operations like Close that perform
// an action but don't produce a useful value.
//
// Example:
// Close[*os.File](file) // Returns ReaderIOResult[Void]
//
// See Also:
// - function.Void: The underlying type definition
Void = function.Void
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain and Bind operations.
//
// Kleisli[A, B] is equivalent to:
// func(A) ReaderIOResult[B]
//
// Example:
// // A Kleisli arrow that reads a file given its path
// var readFileK Kleisli[string, []byte] = ReadFile
//
// See Also:
// - readerioresult.Kleisli: The underlying type definition
// - Operator: For transforming ReaderIOResults
Kleisli[A, B any] = readerioresult.Kleisli[A, B]
// Operator represents a transformation from one ReaderIOResult to another.
// This is useful for point-free style composition and building reusable transformations.
//
// Operator[A, B] is equivalent to:
// func(ReaderIOResult[A]) ReaderIOResult[B]
//
// Operators are used to transform computations without executing them, enabling
// powerful composition patterns.
//
// Example:
// // An operator that maps over file contents
// var toUpper Operator[[]byte, string] = Map(func(data []byte) string {
// return strings.ToUpper(string(data))
// })
//
// See Also:
// - readerioresult.Operator: The underlying type definition
// - Kleisli: For functions that produce ReaderIOResults
Operator[A, B any] = readerioresult.Operator[A, B]
)

View File

@@ -23,8 +23,8 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
return func(w W) RIOE.ReaderIOResult[[]byte] {
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
return func(w W) ReaderIOResult[[]byte] {
return F.Pipe1(
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte]
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
onWrite := onWriteAll[W](data)
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
return func(onCreate ReaderIOResult[W]) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](
onCreate,
Close[W])(
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
func Write[R any, W io.WriteCloser](acquire ReaderIOResult[W]) Kleisli[Kleisli[W, R], R] {
return RIOE.WithResource[R](
acquire,
Close[W])

View File

@@ -18,6 +18,10 @@ package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/internal/witherable"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/option"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
@@ -43,9 +47,49 @@ import (
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
//
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
// result := filter(readerioresult.Right(42))(context.Background())()
// result := filter(readerioresult.Right(42))(t.Context())()
//
//go:inline
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
return RIOR.FilterOrElse[context.Context](pred, onFalse)
}
//go:inline
func Filter[HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[HKTA, HKTA] {
return witherable.Filter(
Map,
filter,
)
}
//go:inline
func FilterArray[A any](p Predicate[A]) Operator[[]A, []A] {
return Filter(array.Filter[A])(p)
}
//go:inline
func FilterIter[A any](p Predicate[A]) Operator[Seq[A], Seq[A]] {
return Filter(iter.Filter[A])(p)
}
//go:inline
func FilterMap[HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[HKTA, HKTB] {
return witherable.FilterMap(
Map,
filter,
)
}
//go:inline
func FilterMapArray[A, B any](p option.Kleisli[A, B]) Operator[[]A, []B] {
return FilterMap(array.FilterMap[A, B])(p)
}
//go:inline
func FilterMapIter[A, B any](p option.Kleisli[A, B]) Operator[Seq[A], Seq[B]] {
return FilterMap(iter.FilterMap[A, B])(p)
}

View File

@@ -71,7 +71,7 @@ import (
//
// // Now we can partially apply the Config
// cfg := Config{Timeout: 30}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(cfg)() // Returns Right(60)
//
// This is especially useful in point-free style when building computation pipelines:
@@ -133,7 +133,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
//
// // Partially apply the Database
// db := Database{ConnectionString: "localhost:5432"}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
//
// In point-free style, this enables clean composition:
@@ -195,7 +195,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R
//
// // Partially apply the Config
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(cfg)() // Returns Right(3)
//
// // With invalid config
@@ -276,7 +276,7 @@ func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kl
//
// // Now we can provide the Config to get the final result
// cfg := Config{Multiplier: 5}
// ctx := context.Background()
// ctx := t.Context()
// finalResult := result(cfg)(ctx)() // Returns Right(50)
//
// In point-free style, this enables clean composition:

View File

@@ -22,6 +22,7 @@ import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
@@ -233,7 +234,7 @@ func Example_sequenceReaderResult_errorHandling() {
ctx := context.Background()
pipeline := F.Pipe2(
sequenced(ctx),
RIOE.Map(func(x int) int { return x * 2 }),
RIOE.Map(N.Mul(2)),
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
return RIOE.Of(fmt.Sprintf("Result: %d", x))
}),

View File

@@ -41,7 +41,7 @@ func TestSequenceReader(t *testing.T) {
// The Reader environment (string) is now the first parameter
sequenced := SequenceReader(original)
ctx := context.Background()
ctx := t.Context()
// Test original
result1 := original(ctx)()
@@ -75,7 +75,7 @@ func TestSequenceReader(t *testing.T) {
}
db := Database{ConnectionString: "localhost:5432"}
ctx := context.Background()
ctx := t.Context()
expected := "Query on localhost:5432"
@@ -106,7 +106,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -132,7 +132,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Sequence
sequenced := SequenceReader(original)
@@ -158,7 +158,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReader(original)
// Test with zero values
@@ -184,7 +184,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReader(original)
@@ -217,14 +217,14 @@ func TestSequenceReader(t *testing.T) {
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used in different contexts
ctx1 := context.Background()
ctx1 := t.Context()
result1 := withConfig(ctx1)()
assert.True(t, either.IsRight(result1))
value1, _ := either.Unwrap(result1)
assert.Equal(t, 50, value1)
// Can reuse with different context
ctx2 := context.Background()
ctx2 := t.Context()
result2 := withConfig(ctx2)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
@@ -246,7 +246,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReaderIO(original)
// Test original
@@ -273,7 +273,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -303,7 +303,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReaderIO(original)
@@ -327,7 +327,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReaderResult(original)
// Test original
@@ -356,7 +356,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -384,7 +384,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with inner error
result1 := original(ctx)()
@@ -421,7 +421,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test outer error
sequenced1 := SequenceReaderResult(makeOriginal(-20))
@@ -460,7 +460,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReaderResult(original)
@@ -484,7 +484,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
empty := Empty{}
sequenced := SequenceReader(original)
@@ -514,7 +514,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
data := &Data{Value: 100}
sequenced := SequenceReader(original)
@@ -544,7 +544,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReader(original)
// Call multiple times with same inputs
@@ -583,7 +583,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -614,7 +614,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
@@ -643,7 +643,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Database and execute
db := Database{Prefix: "ID"}
ctx := context.Background()
ctx := t.Context()
finalResult := result(db)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Settings and execute
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := context.Background()
ctx := t.Context()
finalResult := result(settings)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -705,14 +705,14 @@ func TestTraverseReader(t *testing.T) {
withConfig := result(cfg)
// Can now use with different contexts
ctx1 := context.Background()
ctx1 := t.Context()
finalResult1 := withConfig(ctx1)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 30, value1)
// Reuse with different context
ctx2 := context.Background()
ctx2 := t.Context()
finalResult2 := withConfig(ctx2)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
@@ -746,7 +746,7 @@ func TestTraverseReader(t *testing.T) {
result := traversed(original)
// Use canceled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
cfg := Config{Value: 5}
@@ -778,7 +778,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config with zero offset
cfg := Config{Offset: 0}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -807,7 +807,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 4}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -843,7 +843,7 @@ func TestTraverseReader(t *testing.T) {
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
ctx := context.Background()
ctx := t.Context()
finalResult1 := result(rules1)(ctx)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)

View File

@@ -42,7 +42,7 @@
// )
//
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
package builder
import (
@@ -103,7 +103,7 @@ import (
// B.WithJSONBody(map[string]string{"name": "John"}),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
//
// Example without body:
//
@@ -113,7 +113,7 @@ import (
// B.WithMethod("GET"),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
func Requester(builder *R.Builder) RIOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {

View File

@@ -46,7 +46,7 @@ func TestBuilderWithQuery(t *testing.T) {
RIOE.Map(func(r *http.Request) *url.URL {
return r.URL
}),
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[any] {
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[Void] {
return IO.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))
@@ -55,7 +55,7 @@ func TestBuilderWithQuery(t *testing.T) {
}),
)
assert.True(t, E.IsRight(req(context.Background())()))
assert.True(t, E.IsRight(req(t.Context())()))
}
// TestBuilderWithoutBody tests creating a request without a body
@@ -67,7 +67,7 @@ func TestBuilderWithoutBody(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -90,7 +90,7 @@ func TestBuilderWithBody(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -112,7 +112,7 @@ func TestBuilderWithHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -130,7 +130,7 @@ func TestBuilderWithInvalidURL(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
@@ -144,7 +144,7 @@ func TestBuilderWithEmptyMethod(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
// Empty method should still work (defaults to GET in http.NewRequest)
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -161,7 +161,7 @@ func TestBuilderWithMultipleHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -185,7 +185,7 @@ func TestBuilderWithBodyAndHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -207,7 +207,7 @@ func TestBuilderContextCancellation(t *testing.T) {
requester := Requester(builder)
// Create a cancelled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel() // Cancel immediately
result := requester(ctx)()
@@ -233,7 +233,7 @@ func TestBuilderWithDifferentMethods(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
@@ -256,7 +256,7 @@ func TestBuilderWithJSON(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -277,7 +277,7 @@ func TestBuilderWithBearer(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")

View File

@@ -0,0 +1,7 @@
package builder
import "github.com/IBM/fp-go/v2/function"
type (
Void = function.Void
)

View File

@@ -28,7 +28,7 @@
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// result := ReadJSON[MyType](client)(request)
// response := result(context.Background())()
// response := result(t.Context())()
package http
import (
@@ -157,8 +157,8 @@ func MakeClient(httpClient *http.Client) Client {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(context.Background())()
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
// result := fullResp(t.Context())()
func ReadFullResponse(client Client) RIOE.Operator[*http.Request, H.FullResponse] {
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
return F.Flow3(
client.Do(req),
@@ -194,8 +194,8 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(context.Background())()
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
// result := readBytes(request)(t.Context())()
func ReadAll(client Client) RIOE.Operator[*http.Request, []byte] {
return F.Flow2(
ReadFullResponse(client),
RIOE.Map(H.Body),
@@ -218,8 +218,8 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(context.Background())()
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
// result := readText(request)(t.Context())()
func ReadText(client Client) RIOE.Operator[*http.Request, string] {
return F.Flow2(
ReadAll(client),
RIOE.Map(B.ToString),
@@ -231,7 +231,7 @@ func ReadText(client Client) RIOE.Kleisli[Requester, string] {
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
// but will be removed in a future version. The capitalized version follows Go naming
// conventions for acronyms.
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
func ReadJson[A any](client Client) RIOE.Operator[*http.Request, A] {
return ReadJSON[A](client)
}
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
// 3. Reads the response body as bytes
//
// This function is used internally by ReadJSON to ensure proper JSON response handling.
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
func readJSON(client Client) RIOE.Operator[*http.Request, []byte] {
return F.Flow3(
ReadFullResponse(client),
RIOE.ChainFirstEitherK(F.Flow2(
@@ -277,8 +277,8 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/user/1")
// readUser := ReadJSON[User](client)
// result := readUser(request)(context.Background())()
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
// result := readUser(request)(t.Context())()
func ReadJSON[A any](client Client) RIOE.Operator[*http.Request, A] {
return F.Flow2(
readJSON(client),
RIOE.ChainEitherK(J.Unmarshal[A]),

Some files were not shown because too many files have changed in this diff Show More