1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-21 01:07:29 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
f80ca31e14 doc: document how to use a request builder for http
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-09-04 11:13:58 +02:00
Carsten Leue
8650a8a600 Merge pull request #33 from IBM/cleue-mostly-adequate-fp-go
fix: add examples for [Frisby's Mostly Adequate Guide]
2023-09-01 17:50:59 +02:00
Dr. Carsten Leue
fb3b1f115c fix: add examples for [Frisby's Mostly Adequate Guide] 2023-09-01 17:31:47 +02:00
Carsten Leue
ce66cf2295 Merge pull request #31 from IBM/cleue-add-cache
fix: implement simple cache for pure functions
2023-08-31 11:07:46 +02:00
Dr. Carsten Leue
80e579dd0b fix: implement simple cache for pure functions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2023-08-31 10:27:32 +02:00
16 changed files with 696 additions and 25 deletions

View File

@@ -21,6 +21,13 @@ import (
"testing"
H "net/http"
R "github.com/IBM/fp-go/context/readerioeither"
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"
"github.com/stretchr/testify/assert"
)
type PostItem struct {
@@ -30,6 +37,47 @@ type PostItem struct {
Body string `json:"body"`
}
func getTitle(item PostItem) string {
return item.Title
}
type simpleRequestBuilder struct {
method string
url string
headers H.Header
}
func requestBuilder() simpleRequestBuilder {
return simpleRequestBuilder{method: "GET"}
}
func (b simpleRequestBuilder) WithURL(url string) simpleRequestBuilder {
b.url = url
return b
}
func (b simpleRequestBuilder) WithHeader(key, value string) simpleRequestBuilder {
if b.headers == nil {
b.headers = make(H.Header)
} else {
b.headers = b.headers.Clone()
}
b.headers.Set(key, value)
return b
}
func (b simpleRequestBuilder) Build() R.ReaderIOEither[*H.Request] {
return func(ctx context.Context) IOE.IOEither[error, *H.Request] {
return IOE.TryCatchError(func() (*H.Request, error) {
req, err := H.NewRequestWithContext(ctx, b.method, b.url, nil)
if err == nil {
req.Header = b.headers
}
return req, err
})
}
}
func TestSendSingleRequest(t *testing.T) {
client := MakeClient(H.DefaultClient)
@@ -40,7 +88,70 @@ func TestSendSingleRequest(t *testing.T) {
resp1 := readItem(req1)
resE := resp1(context.Background())()
resE := resp1(context.TODO())()
fmt.Println(resE)
}
// setHeaderUnsafe updates a header value in a request object by mutating the request object
func setHeaderUnsafe(key, value string) func(*H.Request) *H.Request {
return func(req *H.Request) *H.Request {
req.Header.Set(key, value)
return req
}
}
func TestSendSingleRequestWithHeaderUnsafe(t *testing.T) {
client := MakeClient(H.DefaultClient)
// this is not safe from a puristic perspective, because the map call mutates the request object
req1 := F.Pipe2(
"https://jsonplaceholder.typicode.com/posts/1",
MakeGetRequest,
R.Map(setHeaderUnsafe("Content-Type", "text/html")),
)
readItem := ReadJson[PostItem](client)
resp1 := F.Pipe2(
req1,
readItem,
R.Map(getTitle),
)
res := F.Pipe1(
resp1(context.TODO())(),
E.GetOrElse(errors.ToString),
)
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
}
func TestSendSingleRequestWithHeaderSafe(t *testing.T) {
client := MakeClient(H.DefaultClient)
// the request builder assembles config values to construct
// the final http request. Each `With` step creates a copy of the settings
// so the flow is pure
request := requestBuilder().
WithURL("https://jsonplaceholder.typicode.com/posts/1").
WithHeader("Content-Type", "text/html").
Build()
readItem := ReadJson[PostItem](client)
response := F.Pipe2(
request,
readItem,
R.Map(getTitle),
)
res := F.Pipe1(
response(context.TODO())(),
E.GetOrElse(errors.ToString),
)
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
}

30
function/cache.go Normal file
View File

@@ -0,0 +1,30 @@
// 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 function
import (
G "github.com/IBM/fp-go/function/generic"
)
// Cache converts a unary function into a unary function that caches the value depending on the parameter
func Cache[K comparable, T any](f func(K) T) func(K) T {
return G.Cache(f)
}
// ContramapCache converts a unary function into a unary function that caches the value depending on the parameter
func ContramapCache[A any, K comparable, T any](kf func(A) K) func(func(A) T) func(A) T {
return G.ContramapCache[func(A) T](kf)
}

50
function/cache_test.go Normal file
View File

@@ -0,0 +1,50 @@
// 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 function
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCache(t *testing.T) {
var count int
withSideEffect := func(n int) int {
count++
return n
}
cached := Cache(withSideEffect)
assert.Equal(t, 0, count)
assert.Equal(t, 10, cached(10))
assert.Equal(t, 1, count)
assert.Equal(t, 10, cached(10))
assert.Equal(t, 1, count)
assert.Equal(t, 20, cached(20))
assert.Equal(t, 2, count)
assert.Equal(t, 20, cached(20))
assert.Equal(t, 2, count)
assert.Equal(t, 10, cached(10))
assert.Equal(t, 2, count)
}

65
function/generic/cache.go Normal file
View File

@@ -0,0 +1,65 @@
// 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 generic
import (
"sync"
L "github.com/IBM/fp-go/internal/lazy"
)
// Cache converts a unary function into a unary function that caches the value depending on the parameter
func Cache[F ~func(K) T, K comparable, T any](f F) F {
return ContramapCache[F](func(k K) K { return k })(f)
}
// ContramapCache converts a unary function into a unary function that caches the value depending on the parameter
func ContramapCache[F ~func(A) T, KF func(A) K, A any, K comparable, T any](kf KF) func(F) F {
return CacheCallback[F](kf, getOrCreate[K, T]())
}
// getOrCreate is a naive implementation of a cache, without bounds
func getOrCreate[K comparable, T any]() func(K, func() func() T) func() T {
cache := make(map[K]func() T)
var l sync.Mutex
return func(k K, cb func() func() T) func() T {
// only lock to access a lazy accessor to the value
l.Lock()
existing, ok := cache[k]
if !ok {
existing = cb()
cache[k] = existing
}
l.Unlock()
// compute the value outside of the lock
return existing
}
}
// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter
func CacheCallback[F ~func(A) T, KF func(A) K, C ~func(K, func() func() T) func() T, A any, K comparable, T any](kf KF, getOrCreate C) func(F) F {
return func(f F) F {
return func(a A) T {
// cache entry
return getOrCreate(kf(a), func() func() T {
return L.Memoize[func() T](func() T {
return f(a)
})
})()
}
}
}

34
internal/lazy/memoize.go Normal file
View File

@@ -0,0 +1,34 @@
// 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 lazy
import "sync"
// Memoize computes the value of the provided IO monad lazily but exactly once
func Memoize[GA ~func() A, A any](ma GA) GA {
// synchronization primitives
var once sync.Once
var result A
// callback
gen := func() {
result = ma()
}
// returns our memoized wrapper
return func() A {
once.Do(gen)
return result
}
}

View File

@@ -16,11 +16,11 @@
package generic
import (
"sync"
"time"
F "github.com/IBM/fp-go/function"
C "github.com/IBM/fp-go/internal/chain"
L "github.com/IBM/fp-go/internal/lazy"
)
// type IO[A any] = func() A
@@ -119,18 +119,7 @@ func Flatten[GA ~func() A, GAA ~func() GA, A any](mma GAA) GA {
// Memoize computes the value of the provided IO monad lazily but exactly once
func Memoize[GA ~func() A, A any](ma GA) GA {
// synchronization primitives
var once sync.Once
var result A
// callback
gen := func() {
result = ma()
}
// returns our memoized wrapper
return func() A {
once.Do(gen)
return result
}
return L.Memoize[GA, A](ma)
}
// Delay creates an operation that passes in the value after some delay

View File

@@ -83,7 +83,7 @@ func Flatten[A any](mma Lazy[Lazy[A]]) Lazy[A] {
return G.Flatten(mma)
}
// Memoize computes the value of the provided IO monad lazily but exactly once
// Memoize computes the value of the provided [Lazy] monad lazily but exactly once
func Memoize[A any](ma Lazy[A]) Lazy[A] {
return G.Memoize(ma)
}

23
number/integer/string.go Normal file
View File

@@ -0,0 +1,23 @@
// 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 integer
import "strconv"
var (
// ToString converts an integer to a string
ToString = strconv.Itoa
)

View File

@@ -131,9 +131,7 @@ func FromStrictCompare[A C.Ordered]() Ord[A] {
return MakeOrd(strictCompare[A], strictEq[A])
}
/**
* Test whether one value is _strictly less than_ another
*/
// Lt tests whether one value is _strictly less than_ another
func Lt[A any](O Ord[A]) func(A) func(A) bool {
return func(second A) func(A) bool {
return func(first A) bool {
@@ -142,9 +140,7 @@ func Lt[A any](O Ord[A]) func(A) func(A) bool {
}
}
/**
* Test whether one value is less or equal than_ another
*/
// Leq Tests whether one value is less or equal than_ another
func Leq[A any](O Ord[A]) func(A) func(A) bool {
return func(second A) func(A) bool {
return func(first A) bool {
@@ -156,7 +152,7 @@ func Leq[A any](O Ord[A]) func(A) func(A) bool {
/**
* Test whether one value is _strictly greater than_ another
*/
func Gt[A any](O Ord[A]) func(A) func(A) bool {
func cc[A any](O Ord[A]) func(A) func(A) bool {
return func(second A) func(A) bool {
return func(first A) bool {
return O.Compare(first, second) > 0
@@ -164,9 +160,7 @@ func Gt[A any](O Ord[A]) func(A) func(A) bool {
}
}
/**
* Test whether one value is greater or equal than_ another
*/
// Geq tests whether one value is greater or equal than_ another
func Geq[A any](O Ord[A]) func(A) func(A) bool {
return func(second A) func(A) bool {
return func(first A) bool {

View File

@@ -0,0 +1,6 @@
# Mostly Adequate: fp-go Companion Guide
This resource is meant to serve as a go "companion" resource to Professor [Frisby's Mostly Adequate Guide](https://github.com/MostlyAdequate/mostly-adequate-guide).
It is a port of the [mostly-adequate-fp-ts](https://github.com/ChuckJonas/mostly-adequate-fp-ts/) book.

View File

@@ -0,0 +1,47 @@
// 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"
type Flock struct {
Seagulls int
}
func MakeFlock(n int) Flock {
return Flock{Seagulls: n}
}
func (f *Flock) Conjoin(other *Flock) *Flock {
f.Seagulls += other.Seagulls
return f
}
func (f *Flock) Breed(other *Flock) *Flock {
f.Seagulls = f.Seagulls * other.Seagulls
return f
}
func Example_flock() {
flockA := MakeFlock(4)
flockB := MakeFlock(2)
flockC := MakeFlock(0)
fmt.Println(flockA.Conjoin(&flockC).Breed(&flockB).Conjoin(flockA.Breed(&flockB)).Seagulls)
// Output: 32
}

View File

@@ -0,0 +1,46 @@
// 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 (
"regexp"
"strings"
F "github.com/IBM/fp-go/function"
N "github.com/IBM/fp-go/number"
I "github.com/IBM/fp-go/number/integer"
S "github.com/IBM/fp-go/string"
)
var (
Match = F.Curry2((*regexp.Regexp).FindStringSubmatch)
Split = F.Curry2(F.Bind3of3((*regexp.Regexp).Split)(-1))
Add = N.Add[int]
ToString = I.ToString
ToLower = strings.ToLower
ToUpper = strings.ToUpper
Concat = F.Curry2(S.Monoid.Concat)
)
// Replace cannot be generated via [F.Curry3] because the order of parameters does not match our desired curried order
func Replace(search *regexp.Regexp) func(replace string) func(s string) string {
return func(replace string) func(s string) string {
return func(s string) string {
return search.ReplaceAllString(s, replace)
}
}
}

View File

@@ -0,0 +1,60 @@
// 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"
F "github.com/IBM/fp-go/function"
S "github.com/IBM/fp-go/string"
)
var (
Exclaim = S.Format[string]("%s!")
Shout = F.Flow2(ToUpper, Exclaim)
Dasherize = F.Flow4(
Replace(regexp.MustCompile(`\s{2,}`))(" "),
Split(regexp.MustCompile(` `)),
A.Map(ToLower),
A.Intercalate(S.Monoid)("-"),
)
)
func Example_shout() {
fmt.Println(Shout("send in the clowns"))
// Output: SEND IN THE CLOWNS!
}
func Example_dasherize() {
fmt.Println(Dasherize("The world is a vampire"))
// Output: the-world-is-a-vampire
}
func Example_pipe() {
output := F.Pipe2(
"send in the clowns",
ToUpper,
Exclaim,
)
fmt.Println(output)
// Output: SEND IN THE CLOWNS!
}

View File

@@ -0,0 +1,133 @@
// 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"
"time"
E "github.com/IBM/fp-go/either"
"github.com/IBM/fp-go/errors"
F "github.com/IBM/fp-go/function"
N "github.com/IBM/fp-go/number"
O "github.com/IBM/fp-go/option"
"github.com/IBM/fp-go/ord"
S "github.com/IBM/fp-go/string"
)
type Account struct {
Balance float32
}
func MakeAccount(b float32) Account {
return Account{Balance: b}
}
func getBalance(a Account) float32 {
return a.Balance
}
var (
ordFloat32 = ord.FromStrictCompare[float32]()
UpdateLedger = F.Identity[Account]
RemainingBalance = F.Flow2(
getBalance,
S.Format[float32]("Your balance is $%0.2f"),
)
FinishTransaction = F.Flow2(
UpdateLedger,
RemainingBalance,
)
getTwenty = F.Flow2(
Withdraw(20),
O.Fold(F.Constant("You're broke!"), FinishTransaction),
)
)
func Withdraw(amount float32) func(account Account) O.Option[Account] {
return F.Flow3(
getBalance,
O.FromPredicate(ord.Geq(ordFloat32)(amount)),
O.Map(F.Flow2(
N.Add(-amount),
MakeAccount,
)))
}
type User struct {
BirthDate string
}
func getBirthDate(u User) string {
return u.BirthDate
}
func MakeUser(d string) User {
return User{BirthDate: d}
}
var parseDate = F.Bind1of2(E.Eitherize2(time.Parse))(time.DateOnly)
func GetAge(now time.Time) func(User) E.Either[error, float64] {
return F.Flow3(
getBirthDate,
parseDate,
E.Map[error](F.Flow3(
now.Sub,
time.Duration.Hours,
N.Mul(1/24.0),
)),
)
}
func Example_widthdraw() {
fmt.Println(getTwenty(MakeAccount(200)))
fmt.Println(getTwenty(MakeAccount(10)))
// Output:
// Your balance is $180.00
// You're broke!
}
func Example_getAge() {
now, err := time.Parse(time.DateOnly, "2023-09-01")
if err != nil {
panic(err)
}
fmt.Println(GetAge(now)(MakeUser("2005-12-12")))
fmt.Println(GetAge(now)(MakeUser("July 4, 2001")))
fortune := F.Flow3(
N.Add(365.0),
S.Format[float64]("%0.0f"),
Concat("If you survive, you will be "),
)
zoltar := F.Flow3(
GetAge(now),
E.Map[error](fortune),
E.GetOrElse(errors.ToString),
)
fmt.Println(zoltar(MakeUser("2005-12-12")))
// Output:
// Right[<nil>, float64](6472)
// Left[*time.ParseError, float64](parsing time "July 4, 2001" as "2006-01-02": cannot parse "July 4, 2001" as "2006")
// If you survive, you will be 6837
}

View File

@@ -0,0 +1,64 @@
// 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"
F "github.com/IBM/fp-go/function"
O "github.com/IBM/fp-go/option"
)
type (
Street struct {
Name string
Number int
}
Address struct {
Street Street
Postcode string
}
AddressBook struct {
Addresses []Address
}
)
func getAddresses(ab AddressBook) []Address {
return ab.Addresses
}
func getStreet(s Address) Street {
return s.Street
}
var FirstAddressStreet = F.Flow3(
getAddresses,
A.Head[Address],
O.Map(getStreet),
)
func Example_street() {
s := FirstAddressStreet(AddressBook{
Addresses: A.From(Address{Street: Street{Name: "Mulburry", Number: 8402}, Postcode: "WC2N"}),
})
fmt.Println(s)
// Output:
// Some[mostlyadequate.Street]({Mulburry 8402})
}

View File

@@ -0,0 +1,19 @@
// 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 is meant to serve as a go "companion" resource to Professor [Frisby's Mostly Adequate Guide].
//
// [Frisby's Mostly Adequate Guide]: https://github.com/MostlyAdequate/mostly-adequate-guide
package mostlyadequate