diff --git a/either/traverse.go b/either/traverse.go index 3010c26..33a0e39 100644 --- a/either/traverse.go +++ b/either/traverse.go @@ -27,13 +27,13 @@ HKTRB = HKT HKTA = HKT HKTB = HKT */ -func traverse[E, A, B, HKTA, HKTB, HKTRB any]( - _of func(Either[E, B]) HKTRB, - _map func(HKTB, func(B) Either[E, B]) HKTRB, +func traverse[E, A, B, HKTB, HKTRB any]( + mof func(Either[E, B]) HKTRB, + mmap func(func(B) Either[E, B]) func(HKTB) HKTRB, ) func(Either[E, A], func(A) HKTB) HKTRB { - left := F.Flow2(Left[B, E], _of) - right := F.Bind2nd(_map, Right[E, B]) + left := F.Flow2(Left[B, E], mof) + right := mmap(Right[E, B]) return func(ta Either[E, A], f func(A) HKTB) HKTRB { return MonadFold(ta, @@ -43,27 +43,21 @@ func traverse[E, A, B, HKTA, HKTB, HKTRB any]( } } -func Traverse[E, A, B, HKTA, HKTB, HKTRB any]( - _of func(Either[E, B]) HKTRB, - _map func(HKTB, func(B) Either[E, B]) HKTRB, +// Traverse converts an [Either] of some higher kinded type into the higher kinded type of an [Either] +func Traverse[A, E, B, HKTB, HKTRB any]( + mof func(Either[E, B]) HKTRB, + mmap func(func(B) Either[E, B]) func(HKTB) HKTRB, ) func(func(A) HKTB) func(Either[E, A]) HKTRB { - delegate := traverse[E, A, B, HKTA](_of, _map) + delegate := traverse[E, A, B](mof, mmap) return func(f func(A) HKTB) func(Either[E, A]) HKTRB { return F.Bind2nd(delegate, f) } } -/* -* -We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces - -HKTRA = HKT -HKTA = HKT -HKTB = HKT -*/ +// Sequence converts an [Either] of some higher kinded type into the higher kinded type of an [Either] func Sequence[E, A, HKTA, HKTRA any]( - _of func(Either[E, A]) HKTRA, - _map func(HKTA, func(A) Either[E, A]) HKTRA, + mof func(Either[E, A]) HKTRA, + mmap func(func(A) Either[E, A]) func(HKTA) HKTRA, ) func(Either[E, HKTA]) HKTRA { - return Fold(F.Flow2(Left[A, E], _of), F.Bind2nd(_map, Right[E, A])) + return Fold(F.Flow2(Left[A, E], mof), mmap(Right[E, A])) } diff --git a/either/traverse_test.go b/either/traverse_test.go index c27bac9..3b0e86b 100644 --- a/either/traverse_test.go +++ b/either/traverse_test.go @@ -30,9 +30,9 @@ func TestTraverse(t *testing.T) { } return O.None[int]() } - trav := Traverse[string, int, int, O.Option[Either[string, int]]]( + trav := Traverse[int]( O.Of[Either[string, int]], - O.MonadMap[int, Either[string, int]], + O.Map[int, Either[string, int]], )(f) assert.Equal(t, O.Of(Left[int]("a")), F.Pipe1(Left[int]("a"), trav)) @@ -44,7 +44,7 @@ func TestSequence(t *testing.T) { seq := Sequence( O.Of[Either[string, int]], - O.MonadMap[int, Either[string, int]], + O.Map[int, Either[string, int]], ) assert.Equal(t, O.Of(Right[string](1)), seq(Right[string](O.Of(1)))) diff --git a/option/sequence.go b/option/sequence.go index 61782e7..2d91cd3 100644 --- a/option/sequence.go +++ b/option/sequence.go @@ -19,13 +19,22 @@ import ( F "github.com/IBM/fp-go/function" ) -// HKTA = HKT -// HKTOA = HKT> -// -// Sequence converts an option of some higher kinded type into the higher kinded type of an option +// Sequence converts an [Option] of some higher kinded type into the higher kinded type of an [Option] func Sequence[A, HKTA, HKTOA any]( - _of func(Option[A]) HKTOA, - _map func(HKTA, func(A) Option[A]) HKTOA, + mof func(Option[A]) HKTOA, + mmap func(func(A) Option[A]) func(HKTA) HKTOA, ) func(Option[HKTA]) HKTOA { - return Fold(F.Nullary2(None[A], _of), F.Bind2nd(_map, Some[A])) + return Fold(F.Nullary2(None[A], mof), mmap(Some[A])) +} + +// Traverse converts an [Option] of some higher kinded type into the higher kinded type of an [Option] +func Traverse[A, B, HKTB, HKTOB any]( + mof func(Option[B]) HKTOB, + mmap func(func(B) Option[B]) func(HKTB) HKTOB, +) func(func(A) HKTB) func(Option[A]) HKTOB { + onNone := F.Nullary2(None[B], mof) + onSome := mmap(Some[B]) + return func(f func(A) HKTB) func(Option[A]) HKTOB { + return Fold(onNone, F.Flow2(f, onSome)) + } } diff --git a/record/generic/record.go b/record/generic/record.go index 5c13b9b..a69eed3 100644 --- a/record/generic/record.go +++ b/record/generic/record.go @@ -186,6 +186,13 @@ func MapRefWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](f func(K, * return F.Bind2nd(MonadMapRefWithIndex[M, N, K, V, R], f) } +func MonadLookup[M ~map[K]V, K comparable, V any](m M, k K) O.Option[V] { + if val, ok := m[k]; ok { + return O.Some(val) + } + return O.None[V]() +} + func Lookup[M ~map[K]V, K comparable, V any](k K) func(M) O.Option[V] { n := O.None[V]() return func(m M) O.Option[V] { diff --git a/record/record.go b/record/record.go index 7bebd1f..6ef02bb 100644 --- a/record/record.go +++ b/record/record.go @@ -102,6 +102,11 @@ func Lookup[V any, K comparable](k K) func(map[K]V) O.Option[V] { return G.Lookup[map[K]V](k) } +// MonadLookup returns the entry for a key in a map if it exists +func MonadLookup[V any, K comparable](m map[K]V, k K) O.Option[V] { + return G.MonadLookup[map[K]V](m, k) +} + // Has tests if a key is contained in a map func Has[K comparable, V any](k K, r map[K]V) bool { return G.Has(k, r) diff --git a/samples/mostly-adequate/chapter09_monadiconions_test.go b/samples/mostly-adequate/chapter09_monadiconions_test.go index 0e54b5b..5628e98 100644 --- a/samples/mostly-adequate/chapter09_monadiconions_test.go +++ b/samples/mostly-adequate/chapter09_monadiconions_test.go @@ -18,10 +18,14 @@ package mostlyadequate import ( "fmt" "path" + "regexp" A "github.com/IBM/fp-go/array" + E "github.com/IBM/fp-go/either" + "github.com/IBM/fp-go/errors" F "github.com/IBM/fp-go/function" "github.com/IBM/fp-go/io" + IOE "github.com/IBM/fp-go/ioeither" O "github.com/IBM/fp-go/option" S "github.com/IBM/fp-go/string" ) @@ -104,6 +108,21 @@ var ( // pureLog :: String -> IO () pureLog = io.Logf[string]("%s") + + // addToMailingList :: Email -> IOEither([Email]) + addToMailingList = F.Flow2( + A.Of[string], + IOE.Of[error, []string], + ) + + // validateEmail :: Email -> Either error Email + validateEmail = E.FromPredicate(Matches(regexp.MustCompile(`\S+@\S+\.\S+`)), errors.OnSome[string]("email %s is invalid")) + + // emailBlast :: [Email] -> IO () + emailBlast = F.Flow2( + A.Intercalate(S.Monoid)(","), + IOE.Of[error, string], + ) ) func Example_street() { @@ -147,3 +166,21 @@ func Example_solution09B() { // Output: // ch09.md } + +func Example_solution09C() { + + // // joinMailingList :: Email -> Either String (IO ()) + joinMailingList := F.Flow4( + validateEmail, + IOE.FromEither[error, string], + IOE.Chain(addToMailingList), + IOE.Chain(emailBlast), + ) + + fmt.Println(joinMailingList("sleepy@grandpa.net")()) + fmt.Println(joinMailingList("notanemail")()) + + // Output: + // Right[, string](sleepy@grandpa.net) + // Left[*errors.errorString, string](email notanemail is invalid) +} diff --git a/samples/mostly-adequate/chapter10_applicativefunctor_test.go b/samples/mostly-adequate/chapter10_applicativefunctor_test.go index b1cf1f7..a2f52ef 100644 --- a/samples/mostly-adequate/chapter10_applicativefunctor_test.go +++ b/samples/mostly-adequate/chapter10_applicativefunctor_test.go @@ -23,16 +23,65 @@ import ( R "github.com/IBM/fp-go/context/readerioeither" H "github.com/IBM/fp-go/context/readerioeither/http" F "github.com/IBM/fp-go/function" + IOO "github.com/IBM/fp-go/iooption" + N "github.com/IBM/fp-go/number" + O "github.com/IBM/fp-go/option" + M "github.com/IBM/fp-go/record" + T "github.com/IBM/fp-go/tuple" ) -type PostItem struct { - UserId uint `json:"userId"` - Id uint `json:"id"` - Title string `json:"title"` - Body string `json:"body"` +type ( + PostItem struct { + UserId uint `json:"userId"` + Id uint `json:"id"` + Title string `json:"title"` + Body string `json:"body"` + } + + Player struct { + Id int + Name string + } + + LocalStorage = map[string]Player +) + +var ( + playerAlbert = Player{ + Id: 1, + Name: "Albert", + } + playerTheresa = Player{ + Id: 2, + Name: "Theresa", + } + localStorage = LocalStorage{ + "player1": playerAlbert, + "player2": playerTheresa, + } + + // getFromCache :: String -> IO User + getFromCache = func(name string) IOO.IOOption[Player] { + return func() O.Option[Player] { + return M.MonadLookup(localStorage, name) + } + } + + // game :: User -> User -> String + game = F.Curry2(func(a, b Player) string { + return fmt.Sprintf("%s vs %s", a.Name, b.Name) + }) +) + +func (player Player) getName() string { + return player.Name } -func getTitle(item PostItem) string { +func (player Player) getId() int { + return player.Id +} + +func (item PostItem) getTitle() string { return item.Title } @@ -55,7 +104,7 @@ func Example_renderPage() { idxToUrl, H.MakeGetRequest, H.ReadJson[PostItem](client), - R.Map(getTitle), + R.Map(PostItem.getTitle), ) res := F.Pipe2( @@ -71,3 +120,64 @@ func Example_renderPage() { // Right[, string](
Destinations: [qui est esse], Events: [ea molestias quasi exercitationem repellat qui ipsa sit aut]
) } + +func Example_solution10A() { + safeAdd := F.Curry2(func(a, b O.Option[int]) O.Option[int] { + return F.Pipe3( + N.Add[int], + O.Of[func(int) func(int) int], + O.Ap[func(int) int](a), + O.Ap[int](b), + ) + }) + + fmt.Println(safeAdd(O.Of(2))(O.Of(3))) + fmt.Println(safeAdd(O.None[int]())(O.Of(3))) + fmt.Println(safeAdd(O.Of(2))(O.None[int]())) + + // Output: + // Some[int](5) + // None[int] + // None[int] +} + +func Example_solution10B() { + + safeAdd := F.Curry2(T.Untupled2(F.Flow2( + O.SequenceTuple2[int, int], + O.Map(T.Tupled2(N.MonoidSum[int]().Concat)), + ))) + + fmt.Println(safeAdd(O.Of(2))(O.Of(3))) + fmt.Println(safeAdd(O.None[int]())(O.Of(3))) + fmt.Println(safeAdd(O.Of(2))(O.None[int]())) + + // Output: + // Some[int](5) + // None[int] + // None[int] +} + +func Example_solution10C() { + // startGame :: IO String + startGame := F.Pipe2( + IOO.Of(game), + IOO.Ap[func(Player) string](getFromCache("player1")), + IOO.Ap[string](getFromCache("player2")), + ) + + startGameTupled := F.Pipe2( + T.MakeTuple2("player1", "player2"), + IOO.TraverseTuple2(getFromCache, getFromCache), + IOO.Map(T.Tupled2(func(a, b Player) string { + return fmt.Sprintf("%s vs %s", a.Name, b.Name) + })), + ) + + fmt.Println(startGame()) + fmt.Println(startGameTupled()) + + // Output: + // Some[string](Albert vs Theresa) + // Some[string](Albert vs Theresa) +} diff --git a/samples/mostly-adequate/chapter11_transformagain_test.go b/samples/mostly-adequate/chapter11_transformagain_test.go new file mode 100644 index 0000000..1ef7dfe --- /dev/null +++ b/samples/mostly-adequate/chapter11_transformagain_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2023 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 mostlyadequate + +import ( + "fmt" + "regexp" + + A "github.com/IBM/fp-go/array" + E "github.com/IBM/fp-go/either" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + S "github.com/IBM/fp-go/string" +) + +func findUserById(id int) IOE.IOEither[error, Chapter08User] { + switch id { + case 1: + return IOE.Of[error](albert08) + case 2: + return IOE.Of[error](gary08) + case 3: + return IOE.Of[error](theresa08) + default: + return IOE.Left[Chapter08User](fmt.Errorf("user %d not found", id)) + } +} + +func Example_solution11A() { + // eitherToMaybe :: Either b a -> Maybe a + eitherToMaybe := E.ToOption[error, string] + + fmt.Println(eitherToMaybe(E.Of[error]("one eyed willy"))) + fmt.Println(eitherToMaybe(E.Left[string](fmt.Errorf("some error")))) + + // Output: + // Some[string](one eyed willy) + // None[string] +} + +func Example_solution11B() { + findByNameId := F.Flow2( + findUserById, + IOE.Map[error](Chapter08User.getName), + ) + + fmt.Println(findByNameId(1)()) + fmt.Println(findByNameId(2)()) + fmt.Println(findByNameId(3)()) + fmt.Println(findByNameId(4)()) + + // Output: + // Right[, string](Albert) + // Right[, string](Gary) + // Right[, string](Theresa) + // Left[*errors.errorString, string](user 4 not found) +} + +func Example_solution11C() { + // strToList :: String -> [Char + strToList := Split(regexp.MustCompile(``)) + + // listToStr :: [Char] -> String + listToStr := A.Intercalate(S.Monoid)("") + + sortLetters := F.Flow3( + strToList, + A.Sort(S.Ord), + listToStr, + ) + + fmt.Println(sortLetters("sortme")) + + // Output: + // emorst +} diff --git a/samples/mostly-adequate/chapter12_traversing_test.go b/samples/mostly-adequate/chapter12_traversing_test.go new file mode 100644 index 0000000..afdbe38 --- /dev/null +++ b/samples/mostly-adequate/chapter12_traversing_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2023 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 mostlyadequate + +import ( + "fmt" + + A "github.com/IBM/fp-go/array" + E "github.com/IBM/fp-go/either" + "github.com/IBM/fp-go/errors" + F "github.com/IBM/fp-go/function" + IOE "github.com/IBM/fp-go/ioeither" + O "github.com/IBM/fp-go/option" + P "github.com/IBM/fp-go/predicate" + S "github.com/IBM/fp-go/string" +) + +var ( + // httpGet :: Route -> Task Error JSON + httpGet = F.Flow2( + S.Format[string]("json for %s"), + IOE.Of[error, string], + ) + + // routes :: Map Route Route + routes = map[string]string{ + "/": "/", + "/about": "/about", + } + + // validate :: Player -> Either error Player + validatePlayer = E.FromPredicate(P.ContraMap(Player.getName)(S.IsNonEmpty), F.Flow2(Player.getId, errors.OnSome[int]("player %d must have a name"))) + + // readfile :: String -> String -> Task Error String + readfile = F.Curry2(func(encoding, file string) IOE.IOEither[error, string] { + return IOE.Of[error](fmt.Sprintf("content of %s (%s)", file, encoding)) + }) + + // readdir :: String -> Task Error [String] + readdir = IOE.Of[error](A.From("file1", "file2", "file3")) +) + +func Example_solution12A() { + // getJsons :: Map Route Route -> Task Error (Map Route JSON) + getJsons := IOE.TraverseRecord[string](httpGet) + + fmt.Println(getJsons(routes)()) + + // Output: + // Right[, map[string]string](map[/:json for / /about:json for /about]) +} + +func Example_solution12B() { + // startGame :: [Player] -> [Either Error String] + startGame := F.Flow2( + E.TraverseArray(validatePlayer), + E.MapTo[error, []Player]("Game started"), + ) + + fmt.Println(startGame(A.From(playerAlbert, playerTheresa))) + fmt.Println(startGame(A.From(playerAlbert, Player{Id: 4}))) + + // Output: + // Right[, string](Game started) + // Left[*errors.errorString, string](player 4 must have a name) +} + +func Example_solution12C() { + traverseO := O.Traverse[string]( + IOE.Of[error, O.Option[string]], + IOE.Map[error, string, O.Option[string]], + ) + + // readFirst :: String -> Task Error (Maybe String) + readFirst := F.Pipe2( + readdir, + IOE.Map[error](A.Head[string]), + IOE.Chain(traverseO(readfile("utf-8"))), + ) + + fmt.Println(readFirst()) + + // Output: + // Right[, option.Option[string]](Some[string](content of file1 (utf-8))) +}