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

Compare commits

..

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
45cc0a7fc1 fix: better traversal support
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-10 17:24:08 +02:00
25 changed files with 1770 additions and 51 deletions

View File

@@ -1510,3 +1510,8 @@ func Extend[A, B any](f func([]A) B) Operator[A, B] {
func Extract[A any](as []A) A {
return G.Extract(as)
}
//go:inline
func UpdateAt[T any](i int, v T) func([]T) Option[[]T] {
return G.UpdateAt[[]T](i, v)
}

View File

@@ -489,3 +489,16 @@ func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
}
}
func UpdateAt[GT ~[]T, T any](i int, v T) func(GT) O.Option[GT] {
none := O.None[GT]()
if i < 0 {
return F.Constant1[GT](none)
}
return func(g GT) O.Option[GT] {
if i >= len(g) {
return none
}
return O.Of(array.UnsafeUpdateAt(g, i, v))
}
}

View File

@@ -15,6 +15,8 @@
package array
import "slices"
func Of[GA ~[]A, A any](a A) GA {
return GA{a}
}
@@ -197,3 +199,9 @@ func Reverse[GT ~[]T, T any](as GT) GT {
}
return ras
}
func UnsafeUpdateAt[GT ~[]T, T any](as GT, i int, v T) GT {
c := slices.Clone(as)
c[i] = v
return c
}

View File

@@ -0,0 +1,23 @@
package generic
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
I "github.com/IBM/fp-go/v2/optics/iso"
)
// AsTraversal converts a iso to a traversal
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
fmap functor.MapType[A, S, HKTA, HKTS],
) func(I.Iso[S, A]) R {
return func(sa I.Iso[S, A]) R {
saSet := fmap(sa.ReverseGet)
return func(f func(A) HKTA) func(S) HKTS {
return F.Flow3(
sa.Get,
f,
saSet,
)
}
}
}

View File

@@ -23,5 +23,5 @@ import (
)
func AsTraversal[E, S, A any]() func(L.Lens[S, A]) T.Traversal[E, S, A] {
return LG.AsTraversal[T.Traversal[E, S, A]](ET.MonadMap[E, A, S])
return LG.AsTraversal[T.Traversal[E, S, A]](ET.Map[E, A, S])
}

View File

@@ -16,19 +16,24 @@
package generic
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// AsTraversal converts a lens to a traversal
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
fmap func(HKTA, func(A) S) HKTS,
fmap functor.MapType[A, S, HKTA, HKTS],
) func(L.Lens[S, A]) R {
return func(sa L.Lens[S, A]) R {
return func(f func(a A) HKTA) func(S) HKTS {
return func(f func(A) HKTA) func(S) HKTS {
return func(s S) HKTS {
return fmap(f(sa.Get(s)), func(a A) S {
return sa.Set(a)(s)
})
return F.Pipe1(
f(sa.Get(s)),
fmap(func(a A) S {
return sa.Set(a)(s)
}),
)
}
}
}

View File

@@ -60,5 +60,5 @@ import (
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
// // Apply operations across all configs using the traversal
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
return LG.AsTraversal[T.Traversal[S, A]](O.Map[A, S])
}

View File

@@ -0,0 +1,86 @@
// 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 identity
import (
I "github.com/IBM/fp-go/v2/identity"
G "github.com/IBM/fp-go/v2/optics/lens/traversal/generic"
)
// Compose composes a lens with a traversal to create a new traversal.
//
// This function allows you to focus deeper into a data structure by first using
// a lens to access a field, then using a traversal to access multiple values within
// that field. The result is a traversal that can operate on all the nested values.
//
// The composition follows the pattern: Lens[S, A] → Traversal[A, B] → Traversal[S, B]
// where the lens focuses on field A within structure S, and the traversal focuses on
// multiple B values within A.
//
// Type Parameters:
// - S: The outer structure type
// - A: The intermediate field type (target of the lens)
// - B: The final focus type (targets of the traversal)
//
// Parameters:
// - t: A traversal that focuses on B values within A
//
// Returns:
// - A function that takes a Lens[S, A] and returns a Traversal[S, B]
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/optics/lens"
// LT "github.com/IBM/fp-go/v2/optics/lens/traversal"
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
// )
//
// type Team struct {
// Name string
// Members []string
// }
//
// // Lens to access the Members field
// membersLens := lens.MakeLens(
// func(t Team) []string { return t.Members },
// func(t Team, m []string) Team { t.Members = m; return t },
// )
//
// // Traversal for array elements
// arrayTraversal := AI.FromArray[string]()
//
// // Compose lens with traversal to access all member names
// memberTraversal := F.Pipe1(
// membersLens,
// LT.Compose[Team, []string, string](arrayTraversal),
// )
//
// team := Team{Name: "Engineering", Members: []string{"Alice", "Bob"}}
// // Uppercase all member names
// updated := memberTraversal(strings.ToUpper)(team)
// // updated.Members: ["ALICE", "BOB"]
//
// See Also:
// - Lens: A functional reference to a subpart of a data structure
// - Traversal: A functional reference to multiple subparts
// - traversal.Compose: Composes two traversals
func Compose[S, A, B any](t Traversal[A, B, A, B]) func(Lens[S, A]) Traversal[S, B, S, B] {
return G.Compose[S, A, B, S, A, B](
I.Map,
)(t)
}

View File

@@ -0,0 +1,253 @@
// 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 identity
import (
"strings"
"testing"
AR "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/lens"
AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
"github.com/stretchr/testify/assert"
)
type Team struct {
Name string
Members []string
}
type Company struct {
Name string
Teams []Team
}
func TestCompose_Success(t *testing.T) {
t.Run("composes lens with array traversal to modify nested values", func(t *testing.T) {
// Arrange
membersLens := lens.MakeLens(
func(team Team) []string { return team.Members },
func(team Team, members []string) Team {
team.Members = members
return team
},
)
arrayTraversal := AI.FromArray[string]()
memberTraversal := F.Pipe1(
membersLens,
Compose[Team](arrayTraversal),
)
team := Team{
Name: "Engineering",
Members: []string{"alice", "bob", "charlie"},
}
// Act - uppercase all member names
result := memberTraversal(strings.ToUpper)(team)
// Assert
expected := Team{
Name: "Engineering",
Members: []string{"ALICE", "BOB", "CHARLIE"},
}
assert.Equal(t, expected, result)
})
t.Run("composes lens with array traversal on empty array", func(t *testing.T) {
// Arrange
membersLens := lens.MakeLens(
func(team Team) []string { return team.Members },
func(team Team, members []string) Team {
team.Members = members
return team
},
)
arrayTraversal := AI.FromArray[string]()
memberTraversal := F.Pipe1(
membersLens,
Compose[Team](arrayTraversal),
)
team := Team{
Name: "Engineering",
Members: []string{},
}
// Act
result := memberTraversal(strings.ToUpper)(team)
// Assert
assert.Equal(t, team, result)
})
t.Run("composes lens with array traversal to transform numbers", func(t *testing.T) {
// Arrange
type Stats struct {
Name string
Scores []int
}
scoresLens := lens.MakeLens(
func(s Stats) []int { return s.Scores },
func(s Stats, scores []int) Stats {
s.Scores = scores
return s
},
)
arrayTraversal := AI.FromArray[int]()
scoreTraversal := F.Pipe1(
scoresLens,
Compose[Stats, []int, int](arrayTraversal),
)
stats := Stats{
Name: "Player1",
Scores: []int{10, 20, 30},
}
// Act - double all scores
result := scoreTraversal(func(n int) int { return n * 2 })(stats)
// Assert
expected := Stats{
Name: "Player1",
Scores: []int{20, 40, 60},
}
assert.Equal(t, expected, result)
})
}
func TestCompose_Integration(t *testing.T) {
t.Run("composes multiple lenses and traversals", func(t *testing.T) {
// Arrange - nested structure with Company -> Teams -> Members
teamsLens := lens.MakeLens(
func(c Company) []Team { return c.Teams },
func(c Company, teams []Team) Company {
c.Teams = teams
return c
},
)
// First compose: Company -> []Team -> Team
teamArrayTraversal := AI.FromArray[Team]()
companyToTeamTraversal := F.Pipe1(
teamsLens,
Compose[Company, []Team, Team](teamArrayTraversal),
)
// Second compose: Team -> []string -> string
membersLens := lens.MakeLens(
func(team Team) []string { return team.Members },
func(team Team, members []string) Team {
team.Members = members
return team
},
)
memberArrayTraversal := AI.FromArray[string]()
teamToMemberTraversal := F.Pipe1(
membersLens,
Compose[Team](memberArrayTraversal),
)
company := Company{
Name: "TechCorp",
Teams: []Team{
{Name: "Engineering", Members: []string{"alice", "bob"}},
{Name: "Design", Members: []string{"charlie", "diana"}},
},
}
// Act - uppercase all members in all teams
// First traverse to teams, then for each team traverse to members
result := companyToTeamTraversal(func(team Team) Team {
return teamToMemberTraversal(strings.ToUpper)(team)
})(company)
// Assert
expected := Company{
Name: "TechCorp",
Teams: []Team{
{Name: "Engineering", Members: []string{"ALICE", "BOB"}},
{Name: "Design", Members: []string{"CHARLIE", "DIANA"}},
},
}
assert.Equal(t, expected, result)
})
}
func TestCompose_EdgeCases(t *testing.T) {
t.Run("preserves structure name when modifying members", func(t *testing.T) {
// Arrange
membersLens := lens.MakeLens(
func(team Team) []string { return team.Members },
func(team Team, members []string) Team {
team.Members = members
return team
},
)
arrayTraversal := AI.FromArray[string]()
memberTraversal := F.Pipe1(
membersLens,
Compose[Team](arrayTraversal),
)
team := Team{
Name: "Engineering",
Members: []string{"alice"},
}
// Act
result := memberTraversal(strings.ToUpper)(team)
// Assert - Name should be unchanged
assert.Equal(t, "Engineering", result.Name)
assert.Equal(t, AR.From("ALICE"), result.Members)
})
t.Run("handles identity transformation", func(t *testing.T) {
// Arrange
membersLens := lens.MakeLens(
func(team Team) []string { return team.Members },
func(team Team, members []string) Team {
team.Members = members
return team
},
)
arrayTraversal := AI.FromArray[string]()
memberTraversal := F.Pipe1(
membersLens,
Compose[Team](arrayTraversal),
)
team := Team{
Name: "Engineering",
Members: []string{"alice", "bob"},
}
// Act - apply identity function
result := memberTraversal(F.Identity[string])(team)
// Assert - should be unchanged
assert.Equal(t, team, result)
})
}

View File

@@ -0,0 +1,14 @@
package identity
import (
"github.com/IBM/fp-go/v2/optics/lens"
T "github.com/IBM/fp-go/v2/optics/traversal"
)
type (
// Lens is a functional reference to a subpart of a data structure.
Lens[S, A any] = lens.Lens[S, A]
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
)

View File

@@ -0,0 +1,25 @@
package generic
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
G "github.com/IBM/fp-go/v2/optics/lens/generic"
TG "github.com/IBM/fp-go/v2/optics/traversal/generic"
)
func Compose[S, A, B, HKTS, HKTA, HKTB any](
fmap functor.MapType[A, S, HKTA, HKTS],
) func(Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
lensTrav := G.AsTraversal[Traversal[S, A, HKTS, HKTA]](fmap)
return func(ab Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
return F.Flow2(
lensTrav,
TG.Compose[
Traversal[A, B, HKTA, HKTB],
Traversal[S, A, HKTS, HKTA],
Traversal[S, B, HKTS, HKTB],
](ab),
)
}
}

View File

@@ -0,0 +1,14 @@
package generic
import (
"github.com/IBM/fp-go/v2/optics/lens"
T "github.com/IBM/fp-go/v2/optics/traversal"
)
type (
// Lens is a functional reference to a subpart of a data structure.
Lens[S, A any] = lens.Lens[S, A]
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
)

View File

@@ -0,0 +1,79 @@
// 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 (
OP "github.com/IBM/fp-go/v2/optics/optional"
G "github.com/IBM/fp-go/v2/optics/optional/array/generic"
)
// At creates an Optional that focuses on the element at a specific index in an array.
//
// This function returns an Optional that can get and set the element at the given index.
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
// (the array is returned unchanged). This follows the Optional laws where operations
// on non-existent values have no effect.
//
// The Optional provides safe array access without panicking on invalid indices, making
// it ideal for functional transformations where you want to modify array elements only
// when they exist.
//
// Type Parameters:
// - A: The type of elements in the array
//
// Parameters:
// - idx: The zero-based index to focus on
//
// Returns:
// - An Optional that focuses on the element at the specified index
//
// Example:
//
// import (
// AR "github.com/IBM/fp-go/v2/array"
// OP "github.com/IBM/fp-go/v2/optics/optional"
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
// )
//
// numbers := []int{10, 20, 30, 40}
//
// // Create an optional focusing on index 1
// second := OA.At[int](1)
//
// // Get the element at index 1
// value := second.GetOption(numbers)
// // value: option.Some(20)
//
// // Set the element at index 1
// updated := second.Set(25)(numbers)
// // updated: []int{10, 25, 30, 40}
//
// // Out of bounds access returns None
// outOfBounds := OA.At[int](10)
// value = outOfBounds.GetOption(numbers)
// // value: option.None[int]()
//
// // Out of bounds set is a no-op
// unchanged := outOfBounds.Set(99)(numbers)
// // unchanged: []int{10, 20, 30, 40} (original array)
//
// See Also:
// - AR.Lookup: Gets an element at an index, returning an Option
// - AR.UpdateAt: Updates an element at an index, returning an Option
// - OP.Optional: The Optional optic type
func At[A any](idx int) OP.Optional[[]A, A] {
return G.At[[]A](idx)
}

View File

@@ -0,0 +1,466 @@
// 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"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestAt_GetOption tests the GetOption functionality
func TestAt_GetOption(t *testing.T) {
t.Run("returns Some for valid index", func(t *testing.T) {
numbers := []int{10, 20, 30, 40}
optional := At[int](1)
result := optional.GetOption(numbers)
assert.Equal(t, O.Some(20), result)
})
t.Run("returns Some for first element", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](0)
result := optional.GetOption(numbers)
assert.Equal(t, O.Some(10), result)
})
t.Run("returns Some for last element", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](2)
result := optional.GetOption(numbers)
assert.Equal(t, O.Some(30), result)
})
t.Run("returns None for negative index", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](-1)
result := optional.GetOption(numbers)
assert.Equal(t, O.None[int](), result)
})
t.Run("returns None for out of bounds index", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](10)
result := optional.GetOption(numbers)
assert.Equal(t, O.None[int](), result)
})
t.Run("returns None for empty array", func(t *testing.T) {
numbers := []int{}
optional := At[int](0)
result := optional.GetOption(numbers)
assert.Equal(t, O.None[int](), result)
})
t.Run("returns None for nil array", func(t *testing.T) {
var numbers []int
optional := At[int](0)
result := optional.GetOption(numbers)
assert.Equal(t, O.None[int](), result)
})
}
// TestAt_Set tests the Set functionality
func TestAt_Set(t *testing.T) {
t.Run("updates element at valid index", func(t *testing.T) {
numbers := []int{10, 20, 30, 40}
optional := At[int](1)
result := optional.Set(25)(numbers)
assert.Equal(t, []int{10, 25, 30, 40}, result)
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
})
t.Run("updates first element", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](0)
result := optional.Set(5)(numbers)
assert.Equal(t, []int{5, 20, 30}, result)
})
t.Run("updates last element", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](2)
result := optional.Set(35)(numbers)
assert.Equal(t, []int{10, 20, 35}, result)
})
t.Run("is no-op for negative index", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](-1)
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
t.Run("is no-op for out of bounds index", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](10)
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
t.Run("is no-op for empty array", func(t *testing.T) {
numbers := []int{}
optional := At[int](0)
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
t.Run("is no-op for nil array", func(t *testing.T) {
var numbers []int
optional := At[int](0)
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
}
// TestAt_OptionalLaw1_GetSetNoOp tests Optional Law 1: GetSet Law (No-op on None)
// If GetOption(s) returns None, then Set(a)(s) must return s unchanged (no-op).
func TestAt_OptionalLaw1_GetSetNoOp(t *testing.T) {
t.Run("out of bounds index - set is no-op", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](10)
// Verify GetOption returns None
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
// Set should be a no-op
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
t.Run("negative index - set is no-op", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](-1)
// Verify GetOption returns None
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
// Set should be a no-op
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
t.Run("empty array - set is no-op", func(t *testing.T) {
numbers := []int{}
optional := At[int](0)
// Verify GetOption returns None
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
// Set should be a no-op
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
t.Run("nil array - set is no-op", func(t *testing.T) {
var numbers []int
optional := At[int](0)
// Verify GetOption returns None
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
// Set should be a no-op
result := optional.Set(99)(numbers)
assert.Equal(t, numbers, result)
})
}
// TestAt_OptionalLaw2_SetGet tests Optional Law 2: SetGet Law (Get what you Set)
// If GetOption(s) returns Some(_), then GetOption(Set(a)(s)) must return Some(a).
func TestAt_OptionalLaw2_SetGet(t *testing.T) {
t.Run("set then get returns the set value", func(t *testing.T) {
numbers := []int{10, 20, 30, 40}
optional := At[int](1)
// Verify GetOption returns Some (precondition)
assert.True(t, O.IsSome(optional.GetOption(numbers)))
// Set a new value
newValue := 25
updated := optional.Set(newValue)(numbers)
// GetOption on updated should return Some(newValue)
result := optional.GetOption(updated)
assert.Equal(t, O.Some(newValue), result)
})
t.Run("set first element then get", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](0)
assert.True(t, O.IsSome(optional.GetOption(numbers)))
newValue := 5
updated := optional.Set(newValue)(numbers)
result := optional.GetOption(updated)
assert.Equal(t, O.Some(newValue), result)
})
t.Run("set last element then get", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](2)
assert.True(t, O.IsSome(optional.GetOption(numbers)))
newValue := 35
updated := optional.Set(newValue)(numbers)
result := optional.GetOption(updated)
assert.Equal(t, O.Some(newValue), result)
})
t.Run("multiple indices satisfy law", func(t *testing.T) {
numbers := []int{10, 20, 30, 40, 50}
for i := range 5 {
optional := At[int](i)
assert.True(t, O.IsSome(optional.GetOption(numbers)))
newValue := i * 100
updated := optional.Set(newValue)(numbers)
result := optional.GetOption(updated)
assert.Equal(t, O.Some(newValue), result)
}
})
}
// TestAt_OptionalLaw3_SetSet tests Optional Law 3: SetSet Law (Last Set Wins)
// Setting twice is the same as setting once with the final value.
// Formally: Set(b)(Set(a)(s)) = Set(b)(s)
func TestAt_OptionalLaw3_SetSet(t *testing.T) {
eqSlice := EQ.FromEquals(func(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range len(a) {
if a[i] != b[i] {
return false
}
}
return true
})
t.Run("setting twice equals setting once with final value", func(t *testing.T) {
numbers := []int{10, 20, 30, 40}
optional := At[int](1)
// Set twice: first to 25, then to 99
setTwice := F.Pipe2(
numbers,
optional.Set(25),
optional.Set(99),
)
// Set once with final value
setOnce := optional.Set(99)(numbers)
assert.True(t, eqSlice.Equals(setTwice, setOnce))
})
t.Run("multiple sets - last one wins", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](0)
// Set multiple times
result := F.Pipe4(
numbers,
optional.Set(1),
optional.Set(2),
optional.Set(3),
optional.Set(4),
)
// Should equal setting once with final value
expected := optional.Set(4)(numbers)
assert.True(t, eqSlice.Equals(result, expected))
})
t.Run("set twice on out of bounds - both no-ops", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](10)
// Set twice on out of bounds
setTwice := F.Pipe2(
numbers,
optional.Set(25),
optional.Set(99),
)
// Set once on out of bounds
setOnce := optional.Set(99)(numbers)
// Both should be no-ops, returning original
assert.True(t, eqSlice.Equals(setTwice, numbers))
assert.True(t, eqSlice.Equals(setOnce, numbers))
assert.True(t, eqSlice.Equals(setTwice, setOnce))
})
}
// TestAt_EdgeCases tests edge cases and boundary conditions
func TestAt_EdgeCases(t *testing.T) {
t.Run("single element array", func(t *testing.T) {
numbers := []int{42}
optional := At[int](0)
// Get
assert.Equal(t, O.Some(42), optional.GetOption(numbers))
// Set
updated := optional.Set(99)(numbers)
assert.Equal(t, []int{99}, updated)
// Out of bounds
outOfBounds := At[int](1)
assert.Equal(t, O.None[int](), outOfBounds.GetOption(numbers))
assert.Equal(t, numbers, outOfBounds.Set(99)(numbers))
})
t.Run("large array", func(t *testing.T) {
numbers := make([]int, 1000)
for i := range 1000 {
numbers[i] = i
}
optional := At[int](500)
// Get
assert.Equal(t, O.Some(500), optional.GetOption(numbers))
// Set
updated := optional.Set(9999)(numbers)
assert.Equal(t, 9999, updated[500])
assert.Equal(t, 500, numbers[500]) // Original unchanged
})
t.Run("works with different types", func(t *testing.T) {
// String array
strings := []string{"a", "b", "c"}
strOptional := At[string](1)
assert.Equal(t, O.Some("b"), strOptional.GetOption(strings))
assert.Equal(t, []string{"a", "x", "c"}, strOptional.Set("x")(strings))
// Bool array
bools := []bool{true, false, true}
boolOptional := At[bool](1)
assert.Equal(t, O.Some(false), boolOptional.GetOption(bools))
assert.Equal(t, []bool{true, true, true}, boolOptional.Set(true)(bools))
})
t.Run("preserves array capacity", func(t *testing.T) {
numbers := make([]int, 3, 10)
numbers[0], numbers[1], numbers[2] = 10, 20, 30
optional := At[int](1)
updated := optional.Set(25)(numbers)
assert.Equal(t, []int{10, 25, 30}, updated)
assert.Equal(t, 3, len(updated))
})
}
// TestAt_Integration tests integration scenarios
func TestAt_Integration(t *testing.T) {
t.Run("multiple optionals on same array", func(t *testing.T) {
numbers := []int{10, 20, 30, 40}
first := At[int](0)
second := At[int](1)
third := At[int](2)
// Update multiple indices
result := F.Pipe3(
numbers,
first.Set(1),
second.Set(2),
third.Set(3),
)
assert.Equal(t, []int{1, 2, 3, 40}, result)
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
})
t.Run("chaining operations", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](1)
// Get, verify, set, get again
original := optional.GetOption(numbers)
assert.Equal(t, O.Some(20), original)
updated := optional.Set(25)(numbers)
newValue := optional.GetOption(updated)
assert.Equal(t, O.Some(25), newValue)
// Original still unchanged
assert.Equal(t, O.Some(20), optional.GetOption(numbers))
})
t.Run("conditional update based on current value", func(t *testing.T) {
numbers := []int{10, 20, 30}
optional := At[int](1)
// Get current value and conditionally update
result := F.Pipe1(
optional.GetOption(numbers),
O.Fold(
func() []int { return numbers },
func(current int) []int {
if current > 15 {
return optional.Set(current * 2)(numbers)
}
return numbers
},
),
)
assert.Equal(t, []int{10, 40, 30}, result)
})
}

View File

@@ -0,0 +1,98 @@
// 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 generic
import (
"fmt"
AR "github.com/IBM/fp-go/v2/array/generic"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
OP "github.com/IBM/fp-go/v2/optics/optional"
O "github.com/IBM/fp-go/v2/option"
)
// At creates an Optional that focuses on the element at a specific index in an array.
//
// This function returns an Optional that can get and set the element at the given index.
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
// (the array is returned unchanged). This follows the Optional laws where operations
// on non-existent values have no effect.
//
// The Optional provides safe array access without panicking on invalid indices, making
// it ideal for functional transformations where you want to modify array elements only
// when they exist.
//
// Type Parameters:
// - A: The type of elements in the array
//
// Parameters:
// - idx: The zero-based index to focus on
//
// Returns:
// - An Optional that focuses on the element at the specified index
//
// Example:
//
// import (
// AR "github.com/IBM/fp-go/v2/array"
// OP "github.com/IBM/fp-go/v2/optics/optional"
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
// )
//
// numbers := []int{10, 20, 30, 40}
//
// // Create an optional focusing on index 1
// second := OA.At[int](1)
//
// // Get the element at index 1
// value := second.GetOption(numbers)
// // value: option.Some(20)
//
// // Set the element at index 1
// updated := second.Set(25)(numbers)
// // updated: []int{10, 25, 30, 40}
//
// // Out of bounds access returns None
// outOfBounds := OA.At[int](10)
// value = outOfBounds.GetOption(numbers)
// // value: option.None[int]()
//
// // Out of bounds set is a no-op
// unchanged := outOfBounds.Set(99)(numbers)
// // unchanged: []int{10, 20, 30, 40} (original array)
//
// See Also:
// - AR.Lookup: Gets an element at an index, returning an Option
// - AR.UpdateAt: Updates an element at an index, returning an Option
// - OP.Optional: The Optional optic type
func At[GA ~[]A, A any](idx int) OP.Optional[GA, A] {
lookup := AR.Lookup[GA](idx)
return OP.MakeOptionalCurriedWithName(
lookup,
func(a A) func(GA) GA {
update := AR.UpdateAt[GA](idx, a)
return func(as GA) GA {
return F.Pipe2(
as,
update,
O.GetOrElse(lazy.Of(as)),
)
}
},
fmt.Sprintf("At[%d]", idx),
)
}

View File

@@ -0,0 +1,34 @@
package optional
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
"github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
)
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
fof pointed.OfType[S, HKTS],
fmap functor.MapType[A, S, HKTA, HKTS],
) func(Optional[S, A]) R {
return func(sa Optional[S, A]) R {
return func(f func(A) HKTA) func(S) HKTS {
return func(s S) HKTS {
return F.Pipe2(
s,
sa.GetOption,
O.Fold(
lazy.Of(fof(s)),
F.Flow2(
f,
fmap(func(a A) S {
return sa.Set(a)(s)
}),
),
),
)
}
}
}
}

View File

@@ -310,8 +310,10 @@ func TestAsTraversal(t *testing.T) {
return Identity[Option[int]]{Value: s}
}
fmap := func(ia Identity[int], f func(int) Option[int]) Identity[Option[int]] {
return Identity[Option[int]]{Value: f(ia.Value)}
fmap := func(f func(int) Option[int]) func(Identity[int]) Identity[Option[int]] {
return func(ia Identity[int]) Identity[Option[int]] {
return Identity[Option[int]]{Value: f(ia.Value)}
}
}
type TraversalFunc func(func(int) Identity[int]) func(Option[int]) Identity[Option[int]]

View File

@@ -17,6 +17,9 @@ package prism
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
"github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
)
@@ -58,24 +61,23 @@ import (
// higher-kinded types and applicative functors. Most users will work
// directly with prisms rather than converting them to traversals.
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
fof func(S) HKTS,
fmap func(HKTA, func(A) S) HKTS,
fof pointed.OfType[S, HKTS],
fmap functor.MapType[A, S, HKTA, HKTS],
) func(Prism[S, A]) R {
return func(sa Prism[S, A]) R {
return func(f func(a A) HKTA) func(S) HKTS {
return func(f func(A) HKTA) func(S) HKTS {
return func(s S) HKTS {
return F.Pipe2(
s,
sa.GetOption,
O.Fold(
// If prism doesn't match, return the original value lifted into HKTS
F.Nullary2(F.Constant(s), fof),
// If prism matches, apply f to the extracted value and map back
func(a A) HKTS {
return fmap(f(a), func(a A) S {
return prismModify(F.Constant1[A](a), sa, s)
})
},
lazy.Of(fof(s)),
F.Flow2(
f,
fmap(func(a A) S {
return Set[S](a)(sa)(s)
}),
),
),
)
}

View File

@@ -23,6 +23,6 @@ import (
)
// FromArray returns a traversal from an array for the identity [Monoid]
func FromArray[E, A any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
func FromArray[A, E any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
return AR.FromArray[[]A](m)
}

View File

@@ -21,7 +21,51 @@ import (
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
)
// FromArray returns a traversal from an array for the identity monad
// FromArray creates a traversal for array elements using the Identity functor.
//
// This is a specialized version of the generic FromArray that uses the Identity
// functor, which provides the simplest possible computational context (no context).
// This makes it ideal for straightforward array transformations where you want to
// modify elements directly without additional effects.
//
// The Identity functor means that operations are applied directly to values without
// wrapping them in any additional structure. This results in clean, efficient
// traversals that simply map functions over array elements.
//
// Type Parameters:
// - GA: Array type constraint (e.g., []A)
// - A: The element type within the array
//
// Returns:
// - A Traversal that can transform all elements in an array
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// T "github.com/IBM/fp-go/v2/optics/traversal"
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
// )
//
// // Create a traversal for integer arrays
// arrayTraversal := TI.FromArray[[]int, int]()
//
// // Compose with identity traversal
// traversal := F.Pipe1(
// T.Id[[]int, []int](),
// T.Compose[[]int, []int, []int, int](arrayTraversal),
// )
//
// // Double all numbers in the array
// numbers := []int{1, 2, 3, 4, 5}
// doubled := traversal(func(n int) int { return n * 2 })(numbers)
// // doubled: []int{2, 4, 6, 8, 10}
//
// See Also:
// - AR.FromArray: Generic version with configurable functor
// - I.Of: Identity functor's pure/of operation
// - I.Map: Identity functor's map operation
// - I.Ap: Identity functor's applicative operation
func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
return AR.FromArray[GA](
I.Of[GA],
@@ -29,3 +73,75 @@ func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
I.Ap[GA, A],
)
}
// At creates a function that focuses a traversal on a specific array index using the Identity functor.
//
// This is a specialized version of the generic At that uses the Identity functor,
// providing the simplest computational context for array element access. It transforms
// a traversal focusing on an array into a traversal focusing on the element at the
// specified index.
//
// The Identity functor means operations are applied directly without additional wrapping,
// making this ideal for straightforward element modifications. If the index is out of
// bounds, the traversal focuses on zero elements (no-op).
//
// Type Parameters:
// - GA: Array type constraint (e.g., []A)
// - S: The source type of the outer traversal
// - A: The element type within the array
//
// Parameters:
// - idx: The zero-based index to focus on
//
// Returns:
// - A function that transforms a traversal on arrays into a traversal on a specific element
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// T "github.com/IBM/fp-go/v2/optics/traversal"
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
// )
//
// type Person struct {
// Name string
// Hobbies []string
// }
//
// // Create a traversal focusing on hobbies
// hobbiesTraversal := T.Id[Person, []string]()
//
// // Focus on the second hobby (index 1)
// secondHobby := F.Pipe1(
// hobbiesTraversal,
// TI.At[[]string, Person, string](1),
// )
//
// // Modify the second hobby
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding", "gaming"}}
// updated := secondHobby(func(s string) string {
// return s + "!"
// })(person)
// // updated.Hobbies: []string{"reading", "coding!", "gaming"}
//
// // Out of bounds index is a no-op
// outOfBounds := F.Pipe1(
// hobbiesTraversal,
// TI.At[[]string, Person, string](10),
// )
// unchanged := outOfBounds(func(s string) string {
// return s + "!"
// })(person)
// // unchanged.Hobbies: []string{"reading", "coding", "gaming"} (no change)
//
// See Also:
// - AR.At: Generic version with configurable functor
// - I.Of: Identity functor's pure/of operation
// - I.Map: Identity functor's map operation
func At[GA ~[]A, S, A any](idx int) func(G.Traversal[S, GA, S, GA]) G.Traversal[S, A, S, A] {
return AR.At[GA, S, A, S](
I.Of[GA],
I.Map[A, GA],
)(idx)
}

View File

@@ -16,19 +16,105 @@
package generic
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
AR "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
"github.com/IBM/fp-go/v2/optics/optional"
OA "github.com/IBM/fp-go/v2/optics/optional/array/generic"
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
)
// FromArray returns a traversal from an array
func FromArray[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
fof pointed.OfType[GB, HKTRB],
fmap functor.MapType[GB, func(B) GB, HKTRB, HKTAB],
fap apply.ApType[HKTB, HKTRB, HKTAB],
) G.Traversal[GA, A, HKTRB, HKTB] {
return func(f func(A) HKTB) func(s GA) HKTRB {
return func(s GA) HKTRB {
return AR.MonadTraverse(fof, fmap, fap, s, f)
}
return func(f func(A) HKTB) func(GA) HKTRB {
return AR.Traverse[GA](fof, fmap, fap, f)
}
}
// At creates a function that focuses a traversal on a specific array index.
//
// This function takes an index and returns a function that transforms a traversal
// focusing on an array into a traversal focusing on the element at that index.
// It works by:
// 1. Creating an Optional that focuses on the array element at the given index
// 2. Converting that Optional into a Traversal
// 3. Composing it with the original traversal
//
// If the index is out of bounds, the traversal will focus on zero elements (no-op),
// following the Optional laws where operations on non-existent values have no effect.
//
// This is particularly useful when you have a nested structure containing arrays
// and want to traverse to a specific element within those arrays.
//
// Type Parameters:
// - GA: Array type constraint (e.g., []A)
// - S: The source type of the outer traversal
// - A: The element type within the array
// - HKTS: Higher-kinded type for S (functor/applicative context)
// - HKTGA: Higher-kinded type for GA (functor/applicative context)
// - HKTA: Higher-kinded type for A (functor/applicative context)
//
// Parameters:
// - fof: Function to lift GA into the higher-kinded type HKTGA (pure/of operation)
// - fmap: Function to map over HKTA and produce HKTGA (functor map operation)
//
// Returns:
// - A function that takes an index and returns a traversal transformer
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/identity"
// T "github.com/IBM/fp-go/v2/optics/traversal"
// TA "github.com/IBM/fp-go/v2/optics/traversal/array/generic"
// )
//
// type Person struct {
// Name string
// Hobbies []string
// }
//
// // Create a traversal focusing on the hobbies array
// hobbiesTraversal := T.Id[Person, []string]()
//
// // Focus on the first hobby (index 0)
// firstHobby := F.Pipe1(
// hobbiesTraversal,
// TA.At[[]string, Person, string](
// identity.Of[[]string],
// identity.Map[string, []string],
// )(0),
// )
//
// // Modify the first hobby
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding"}}
// updated := firstHobby(func(s string) string {
// return s + "!"
// })(person)
// // updated.Hobbies: []string{"reading!", "coding"}
//
// See Also:
// - OA.At: Creates an Optional focusing on an array element
// - optional.AsTraversal: Converts an Optional to a Traversal
// - G.Compose: Composes two traversals
func At[GA ~[]A, S, A, HKTS, HKTGA, HKTA any](
fof pointed.OfType[GA, HKTGA],
fmap functor.MapType[A, GA, HKTA, HKTGA],
) func(int) func(G.Traversal[S, GA, HKTS, HKTGA]) G.Traversal[S, A, HKTS, HKTA] {
return F.Flow3(
OA.At[GA],
optional.AsTraversal[G.Traversal[GA, A, HKTGA, HKTA]](fof, fmap),
G.Compose[
G.Traversal[GA, A, HKTGA, HKTA],
G.Traversal[S, GA, HKTS, HKTGA],
G.Traversal[S, A, HKTS, HKTA],
],
)
}

View File

@@ -18,7 +18,12 @@ package generic
import (
AR "github.com/IBM/fp-go/v2/array/generic"
C "github.com/IBM/fp-go/v2/constant"
"github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/predicate"
)
type (
@@ -47,7 +52,7 @@ func FromTraversable[
}
// FoldMap maps each target to a `Monoid` and combines the result
func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
return func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
return F.Flow2(
F.Pipe1(
@@ -61,13 +66,84 @@ func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.
// Fold maps each target to a `Monoid` and combines the result
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
return FoldMap[A, S](F.Identity[A])(sa)
return FoldMap[S](F.Identity[A])(sa)
}
// GetAll gets all the targets of a traversal
func GetAll[GA ~[]A, S, A any](s S) func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
fmap := FoldMap[GA, S](AR.Of[GA, A])
fmap := FoldMap[S](AR.Of[GA, A])
return func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
return fmap(sa)(s)
}
}
// Filter creates a function that filters the targets of a traversal based on a predicate.
//
// This function allows you to refine a traversal to only focus on values that satisfy
// a given predicate. It works by converting the predicate into a prism, then converting
// that prism into a traversal, and finally composing it with the original traversal.
//
// The filtering is selective: when modifying values through the filtered traversal,
// only values that satisfy the predicate will be transformed. Values that don't
// satisfy the predicate remain unchanged.
//
// Type Parameters:
// - S: The source type
// - A: The focus type (the values being filtered)
// - HKTS: Higher-kinded type for S (functor/applicative context)
// - HKTA: Higher-kinded type for A (functor/applicative context)
//
// Parameters:
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
// - fmap: Function to map over HKTA (functor map operation)
//
// Returns:
// - A function that takes a predicate and returns an endomorphism on traversals
//
// Example:
//
// import (
// AR "github.com/IBM/fp-go/v2/array"
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/identity"
// N "github.com/IBM/fp-go/v2/number"
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
// )
//
// // Create a traversal for array elements
// arrayTraversal := AI.FromArray[int]()
// baseTraversal := F.Pipe1(
// Id[[]int, []int](),
// Compose[[]int, []int, []int, int](arrayTraversal),
// )
//
// // Filter to only positive numbers
// isPositive := N.MoreThan(0)
// filteredTraversal := F.Pipe1(
// baseTraversal,
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
// )
//
// // Double only positive numbers
// numbers := []int{-2, -1, 0, 1, 2, 3}
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
// // result: [-2, -1, 0, 2, 4, 6]
//
// See Also:
// - prism.FromPredicate: Creates a prism from a predicate
// - prism.AsTraversal: Converts a prism to a traversal
// - Compose: Composes two traversals
func Filter[
S, HKTS, A, HKTA any](
fof pointed.OfType[A, HKTA],
fmap functor.MapType[A, A, HKTA, HKTA],
) func(predicate.Predicate[A]) endomorphism.Endomorphism[Traversal[S, A, HKTS, HKTA]] {
return F.Flow3(
prism.FromPredicate,
prism.AsTraversal[Traversal[A, A, HKTA, HKTA]](fof, fmap),
Compose[
Traversal[A, A, HKTA, HKTA],
Traversal[S, A, HKTS, HKTA],
Traversal[S, A, HKTS, HKTA]],
)
}

View File

@@ -18,46 +18,110 @@ package traversal
import (
C "github.com/IBM/fp-go/v2/constant"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
)
// Id is the identity constructor of a traversal
func Id[S, A any]() G.Traversal[S, S, A, A] {
func Id[S, A any]() Traversal[S, S, A, A] {
return F.Identity[func(S) A]
}
// Modify applies a transformation function to a traversal
func Modify[S, A any](f func(A) A) func(sa G.Traversal[S, A, S, A]) func(S) S {
return func(sa G.Traversal[S, A, S, A]) func(S) S {
return sa(f)
}
func Modify[S, A any](f Endomorphism[A]) func(Traversal[S, A, S, A]) Endomorphism[S] {
return identity.Flap[Endomorphism[S]](f)
}
// Set sets a constant value for all values of the traversal
func Set[S, A any](a A) func(sa G.Traversal[S, A, S, A]) func(S) S {
func Set[S, A any](a A) func(Traversal[S, A, S, A]) Endomorphism[S] {
return Modify[S](F.Constant1[A](a))
}
// FoldMap maps each target to a `Monoid` and combines the result
func FoldMap[M, S, A any](f func(A) M) func(sa G.Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
return G.FoldMap[M, S](f)
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
return G.FoldMap[S](f)
}
// Fold maps each target to a `Monoid` and combines the result
func Fold[S, A any](sa G.Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
return G.Fold(sa)
}
// GetAll gets all the targets of a traversal
func GetAll[S, A any](s S) func(sa G.Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
func GetAll[A, S any](s S) func(sa Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
return G.GetAll[[]A](s)
}
// Compose composes two traversables
func Compose[
S, A, B, HKTS, HKTA, HKTB any](ab G.Traversal[A, B, HKTA, HKTB]) func(sa G.Traversal[S, A, HKTS, HKTA]) G.Traversal[S, B, HKTS, HKTB] {
S, HKTS, A, B, HKTA, HKTB any](ab Traversal[A, B, HKTA, HKTB]) func(Traversal[S, A, HKTS, HKTA]) Traversal[S, B, HKTS, HKTB] {
return G.Compose[
G.Traversal[A, B, HKTA, HKTB],
G.Traversal[S, A, HKTS, HKTA],
G.Traversal[S, B, HKTS, HKTB]](ab)
Traversal[A, B, HKTA, HKTB],
Traversal[S, A, HKTS, HKTA],
Traversal[S, B, HKTS, HKTB]](ab)
}
// Filter creates a function that filters the targets of a traversal based on a predicate.
//
// This function allows you to refine a traversal to only focus on values that satisfy
// a given predicate. It works by converting the predicate into a prism, then converting
// that prism into a traversal, and finally composing it with the original traversal.
//
// The filtering is selective: when modifying values through the filtered traversal,
// only values that satisfy the predicate will be transformed. Values that don't
// satisfy the predicate remain unchanged.
//
// Type Parameters:
// - S: The source type
// - A: The focus type (the values being filtered)
// - HKTS: Higher-kinded type for S (functor/applicative context)
// - HKTA: Higher-kinded type for A (functor/applicative context)
//
// Parameters:
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
// - fmap: Function to map over HKTA (functor map operation)
//
// Returns:
// - A function that takes a predicate and returns an endomorphism on traversals
//
// Example:
//
// import (
// AR "github.com/IBM/fp-go/v2/array"
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/identity"
// N "github.com/IBM/fp-go/v2/number"
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
// )
//
// // Create a traversal for array elements
// arrayTraversal := AI.FromArray[int]()
// baseTraversal := F.Pipe1(
// Id[[]int, []int](),
// Compose[[]int, []int, []int, int](arrayTraversal),
// )
//
// // Filter to only positive numbers
// isPositive := N.MoreThan(0)
// filteredTraversal := F.Pipe1(
// baseTraversal,
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
// )
//
// // Double only positive numbers
// numbers := []int{-2, -1, 0, 1, 2, 3}
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
// // result: [-2, -1, 0, 2, 4, 6]
//
// See Also:
// - prism.FromPredicate: Creates a prism from a predicate
// - prism.AsTraversal: Converts a prism to a traversal
// - Compose: Composes two traversals
func Filter[S, HKTS, A, HKTA any](
fof pointed.OfType[A, HKTA],
fmap functor.MapType[A, A, HKTA, HKTA],
) func(Predicate[A]) Endomorphism[Traversal[S, A, HKTS, HKTA]] {
return G.Filter[S, HKTS](fof, fmap)
}

View File

@@ -32,14 +32,14 @@ func TestGetAll(t *testing.T) {
as := AR.From(1, 2, 3)
tr := AT.FromArray[[]int, int](AR.Monoid[int]())
tr := AT.FromArray[int](AR.Monoid[int]())
sa := F.Pipe1(
Id[[]int, C.Const[[]int, []int]](),
Compose[[]int, []int, int, C.Const[[]int, []int]](tr),
Compose[[]int, C.Const[[]int, []int], []int, int](tr),
)
getall := GetAll[[]int, int](as)(sa)
getall := GetAll[int](as)(sa)
assert.Equal(t, AR.From(1, 2, 3), getall)
}
@@ -54,7 +54,7 @@ func TestFold(t *testing.T) {
sa := F.Pipe1(
Id[[]int, C.Const[int, []int]](),
Compose[[]int, []int, int, C.Const[int, []int]](tr),
Compose[[]int, C.Const[int, []int], []int, int](tr),
)
folded := Fold(sa)(as)
@@ -70,10 +70,245 @@ func TestTraverse(t *testing.T) {
sa := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, int, []int](tr),
Compose[[]int, []int, []int, int](tr),
)
res := sa(utils.Double)(as)
assert.Equal(t, AR.From(2, 4, 6), res)
}
func TestFilter_Success(t *testing.T) {
t.Run("filters and modifies only matching elements", func(t *testing.T) {
// Arrange
numbers := []int{-2, -1, 0, 1, 2, 3}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
// Filter to only positive numbers
isPositive := N.MoreThan(0)
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
)
// Act - double only positive numbers
result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
// Assert
assert.Equal(t, []int{-2, -1, 0, 2, 4, 6}, result)
})
t.Run("filters even numbers and triples them", func(t *testing.T) {
// Arrange
numbers := []int{1, 2, 3, 4, 5, 6}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
// Filter to only even numbers
isEven := func(n int) bool { return n%2 == 0 }
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isEven),
)
// Act
result := filteredTraversal(func(n int) int { return n * 3 })(numbers)
// Assert
assert.Equal(t, []int{1, 6, 3, 12, 5, 18}, result)
})
t.Run("filters strings by length", func(t *testing.T) {
// Arrange
words := []string{"a", "ab", "abc", "abcd", "abcde"}
arrayTraversal := AI.FromArray[string]()
baseTraversal := F.Pipe1(
Id[[]string, []string](),
Compose[[]string, []string, []string, string](arrayTraversal),
)
// Filter strings with length > 2
longerThanTwo := func(s string) bool { return len(s) > 2 }
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]string, []string, string, string](F.Identity[string], F.Identity[func(string) string])(longerThanTwo),
)
// Act - convert to uppercase
result := filteredTraversal(func(s string) string {
return s + "!"
})(words)
// Assert
assert.Equal(t, []string{"a", "ab", "abc!", "abcd!", "abcde!"}, result)
})
}
func TestFilter_EdgeCases(t *testing.T) {
t.Run("empty array returns empty array", func(t *testing.T) {
// Arrange
numbers := []int{}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
isPositive := N.MoreThan(0)
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
)
// Act
result := filteredTraversal(utils.Double)(numbers)
// Assert
assert.Equal(t, []int{}, result)
})
t.Run("no elements match predicate", func(t *testing.T) {
// Arrange
numbers := []int{-5, -4, -3, -2, -1}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
isPositive := N.MoreThan(0)
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
)
// Act
result := filteredTraversal(utils.Double)(numbers)
// Assert - all elements unchanged
assert.Equal(t, []int{-5, -4, -3, -2, -1}, result)
})
t.Run("all elements match predicate", func(t *testing.T) {
// Arrange
numbers := []int{1, 2, 3, 4, 5}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
isPositive := N.MoreThan(0)
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
)
// Act
result := filteredTraversal(utils.Double)(numbers)
// Assert - all elements doubled
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
t.Run("single element matching", func(t *testing.T) {
// Arrange
numbers := []int{42}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
isPositive := N.MoreThan(0)
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
)
// Act
result := filteredTraversal(utils.Double)(numbers)
// Assert
assert.Equal(t, []int{84}, result)
})
t.Run("single element not matching", func(t *testing.T) {
// Arrange
numbers := []int{-42}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
isPositive := N.MoreThan(0)
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
)
// Act
result := filteredTraversal(utils.Double)(numbers)
// Assert
assert.Equal(t, []int{-42}, result)
})
}
func TestFilter_Integration(t *testing.T) {
t.Run("multiple filters composed", func(t *testing.T) {
// Arrange
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
// Filter to only even numbers, then only those > 4
isEven := func(n int) bool { return n%2 == 0 }
greaterThanFour := N.MoreThan(4)
filteredTraversal := F.Pipe2(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isEven),
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(greaterThanFour),
)
// Act - add 100 to matching elements
result := filteredTraversal(func(n int) int { return n + 100 })(numbers)
// Assert - only 6, 8, 10 should be modified
assert.Equal(t, []int{1, 2, 3, 4, 5, 106, 7, 108, 9, 110}, result)
})
t.Run("filter with identity transformation", func(t *testing.T) {
// Arrange
numbers := []int{1, 2, 3, 4, 5}
arrayTraversal := AI.FromArray[int]()
baseTraversal := F.Pipe1(
Id[[]int, []int](),
Compose[[]int, []int, []int, int](arrayTraversal),
)
isEven := func(n int) bool { return n%2 == 0 }
filteredTraversal := F.Pipe1(
baseTraversal,
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isEven),
)
// Act - identity transformation
result := filteredTraversal(F.Identity[int])(numbers)
// Assert - array unchanged
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
}

View File

@@ -0,0 +1,15 @@
package traversal
import (
"github.com/IBM/fp-go/v2/endomorphism"
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
"github.com/IBM/fp-go/v2/predicate"
)
type (
Endomorphism[A any] = endomorphism.Endomorphism[A]
Traversal[S, A, HKTS, HKTA any] = G.Traversal[S, A, HKTS, HKTA]
Predicate[A any] = predicate.Predicate[A]
)