1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-28 13:12:03 +02:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
47727fd514 fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:51:34 +01:00
Dr. Carsten Leue
ece7d088ea fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:50:30 +01:00
Dr. Carsten Leue
13d25eca32 fix: add composition logic to Iso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:46:41 +01:00
14 changed files with 1946 additions and 27 deletions

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
@@ -64,7 +64,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
go-version: ['1.24.x', '1.25.x', '1.26.x']
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:

View File

@@ -746,8 +746,6 @@ func BenchmarkFilterChained(b *testing.B) {
}
}
// Made with Bob
func TestFilterMap(t *testing.T) {
t.Run("Right value with Some result", func(t *testing.T) {
// Arrange

View File

@@ -385,5 +385,3 @@ func TestFlatMapOptionK_NestedOptions(t *testing.T) {
expected := A.From(10, 30, 50)
assert.Equal(t, expected, values)
}
// Made with Bob

View File

@@ -13,6 +13,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package monoid provides an implementation of the Monoid algebraic structure.
//
// # Monoid
//
// A Monoid is an algebraic structure that extends [Semigroup] by adding an identity element.
// It consists of:
// - A type A
// - An associative binary operation Concat: (A, A) → A
// - An identity element Empty: () → A
//
// # Laws
//
// A Monoid must satisfy the following laws:
//
// 1. Associativity (from Semigroup):
// Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// 2. Left Identity:
// Concat(Empty(), x) = x
//
// 3. Right Identity:
// Concat(x, Empty()) = x
//
// # Common Examples
//
// - Integer addition: Concat = (+), Empty = 0
// - Integer multiplication: Concat = (*), Empty = 1
// - String concatenation: Concat = (++), Empty = ""
// - List concatenation: Concat = (++), Empty = []
// - Boolean AND: Concat = (&&), Empty = true
// - Boolean OR: Concat = (||), Empty = false
// - Function composition: Concat = (∘), Empty = id
//
// # References
//
// - Haskell Data.Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Fantasy Land Monoid: https://github.com/fantasyland/fantasy-land#monoid
// - Semigroup: https://github.com/IBM/fp-go/v2/semigroup
//
// # Related Concepts
//
// - [Semigroup]: A Monoid without the identity element requirement
// - Magma: A set with a binary operation (no laws required)
// - Group: A Monoid where every element has an inverse
package monoid
import (
@@ -21,20 +65,31 @@ import (
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
//
// A Monoid extends Semigroup by adding an identity element (Empty) that satisfies:
// A Monoid extends [Semigroup] by adding an identity element (Empty) that satisfies:
// - Left identity: Concat(Empty(), x) = x
// - Right identity: Concat(x, Empty()) = x
//
// The Monoid must also satisfy the associativity law from Semigroup:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Common examples:
// # Methods
//
// - Concat(x, y A) A: Inherited from Semigroup, combines two values associatively
// - Empty() A: Returns the identity element for the monoid
//
// # Common Examples
//
// - Integer addition with 0 as identity
// - Integer multiplication with 1 as identity
// - String concatenation with "" as identity
// - List concatenation with [] as identity
// - Boolean AND with true as identity
// - Boolean OR with false as identity
//
// # References
//
// - Haskell Monoid typeclass: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
// - Fantasy Land Monoid specification: https://github.com/fantasyland/fantasy-land#monoid
type Monoid[A any] interface {
S.Semigroup[A]
Empty() A
@@ -58,16 +113,22 @@ func (m monoid[A]) Empty() A {
// The provided concat function must be associative, and the empty element must
// satisfy the identity laws (left and right identity).
//
// Parameters:
// - c: An associative binary operation func(A, A) A
// - e: The identity element of type A
// This is the primary constructor for creating custom monoid instances. It's the
// equivalent of defining a Monoid instance in Haskell or implementing the Fantasy Land
// Monoid specification.
//
// Returns:
// - A Monoid[A] instance
// # Parameters
//
// Example:
// - c: An associative binary operation func(A, A) A (equivalent to Haskell's mappend or <>)
// - e: The identity element of type A (equivalent to Haskell's mempty)
//
// // Integer addition monoid
// # Returns
//
// - A [Monoid][A] instance
//
// # Example
//
// // Integer addition monoid (Sum in Haskell)
// addMonoid := MakeMonoid(
// func(a, b int) int { return a + b },
// 0, // identity element
@@ -81,6 +142,11 @@ func (m monoid[A]) Empty() A {
// "", // identity element
// )
// result := stringMonoid.Concat("Hello", " World") // "Hello World"
//
// # References
//
// - Haskell Monoid instance: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
// - Fantasy Land Monoid.empty: https://github.com/fantasyland/fantasy-land#monoid
func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
return monoid[A]{c: c, e: e}
}
@@ -91,13 +157,18 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
// operation in the opposite order. This is useful for operations that are
// not commutative.
//
// Parameters:
// This corresponds to the Dual newtype wrapper in Haskell's Data.Monoid, which
// provides a Monoid instance with reversed operation order.
//
// # Parameters
//
// - m: The monoid to reverse
//
// Returns:
// - A new Monoid[A] with reversed operation order
// # Returns
//
// Example:
// - A new [Monoid][A] with reversed operation order
//
// # Example
//
// // Subtraction monoid (not commutative)
// subMonoid := MakeMonoid(
@@ -116,6 +187,10 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
// )
// reversed := Reverse(stringMonoid)
// result := reversed.Concat("Hello", "World") // "WorldHello"
//
// # References
//
// - Haskell Data.Monoid.Dual: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Dual
func Reverse[A any](m Monoid[A]) Monoid[A] {
return MakeMonoid(S.Reverse(m).Concat, m.Empty())
}
@@ -125,13 +200,19 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
// This is useful when you need to use a monoid in a context that only requires
// a semigroup (associative binary operation without identity).
//
// Parameters:
// Since every Monoid is also a Semigroup (Monoid extends Semigroup), this conversion
// is always safe. This reflects the mathematical relationship where monoids form a
// subset of semigroups.
//
// # Parameters
//
// - m: The monoid to convert
//
// Returns:
// - A Semigroup[A] that uses the same Concat operation
// # Returns
//
// Example:
// - A [Semigroup][A] that uses the same Concat operation
//
// # Example
//
// addMonoid := MakeMonoid(
// func(a, b int) int { return a + b },
@@ -139,6 +220,11 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
// )
// sg := ToSemigroup(addMonoid)
// result := sg.Concat(5, 3) // 8 (identity not available)
//
// # References
//
// - Haskell Semigroup: https://hackage.haskell.org/package/base/docs/Data-Semigroup.html
// - Fantasy Land Semigroup: https://github.com/fantasyland/fantasy-land#semigroup
func ToSemigroup[A any](m Monoid[A]) S.Semigroup[A] {
return S.Semigroup[A](m)
}

View File

@@ -0,0 +1,153 @@
// 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 prism
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
P "github.com/IBM/fp-go/v2/optics/prism"
)
// Compose creates a Kleisli arrow that composes a prism with an isomorphism.
//
// This function takes a Prism[A, B] and returns a Kleisli arrow that can transform
// any Iso[S, A] into a Prism[S, B]. The resulting prism changes the source type from
// A to S using the bidirectional transformation provided by the isomorphism, while
// maintaining the same focus type B.
//
// The composition works as follows:
// - GetOption: First transforms S to A using the iso's Get, then extracts B from A using the prism's GetOption
// - ReverseGet: First constructs A from B using the prism's ReverseGet, then transforms A to S using the iso's ReverseGet
//
// This is the dual operation of optics/prism/iso.Compose:
// - optics/prism/iso.Compose: Transforms the focus type (A → B) while keeping source type (S) constant
// - optics/iso/prism.Compose: Transforms the source type (A → S) while keeping focus type (B) constant
//
// This is particularly useful when you have a prism that works with one type but you
// need to adapt it to work with a different source type that has a lossless bidirectional
// transformation to the original type.
//
// Type Parameters:
// - S: The new source type after applying the isomorphism
// - A: The original source type of the prism
// - B: The focus type (remains constant through composition)
//
// Parameters:
// - ab: A prism that extracts B from A
//
// Returns:
// - A Kleisli arrow (function) that takes an Iso[S, A] and returns a Prism[S, B]
//
// Laws:
// The composed prism must satisfy the prism laws:
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
//
// These laws are preserved because:
// - The isomorphism satisfies: ia.ReverseGet(ia.Get(s)) == s and ia.Get(ia.ReverseGet(a)) == a
// - The original prism satisfies the prism laws
//
// Haskell Equivalent:
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
// specifically when composing an Iso with a Prism:
//
// iso . prism :: Iso s a -> Prism a b -> Prism s b
//
// In Haskell's lens library, this is part of the general optic composition mechanism.
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Iso.html
//
// Example - Composing with Either prism:
//
// import (
// "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Create an isomorphism between []byte and string
// bytesStringIso := iso.MakeIso(
// func(b []byte) string { return string(b) },
// func(s string) []byte { return []byte(s) },
// )
//
// // Compose them to get a prism that works with []byte as source
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
//
// // Use the composed prism
// // First converts []byte to string via iso, then extracts Right value
// bytes := []byte("hello")
// either := either.Right[error](string(bytes))
// result := bytesPrism.GetOption(bytes) // Extracts "hello" if Right
//
// // Construct []byte from string
// constructed := bytesPrism.ReverseGet("world")
// // Returns []byte("world") wrapped in Right
//
// Example - Composing with custom types:
//
// type JSON []byte
// type Config struct {
// Host string
// Port int
// }
//
// // Isomorphism between JSON and []byte
// jsonIso := iso.MakeIso(
// func(j JSON) []byte { return []byte(j) },
// func(b []byte) JSON { return JSON(b) },
// )
//
// // Prism that extracts Config from []byte (via JSON parsing)
// configPrism := prism.MakePrism(
// func(b []byte) option.Option[Config] {
// var cfg Config
// if err := json.Unmarshal(b, &cfg); err != nil {
// return option.None[Config]()
// }
// return option.Some(cfg)
// },
// func(cfg Config) []byte {
// b, _ := json.Marshal(cfg)
// return b
// },
// )
//
// // Compose to work with JSON type instead of []byte
// jsonConfigPrism := IP.Compose(configPrism)(jsonIso)
//
// jsonData := JSON(`{"host":"localhost","port":8080}`)
// config := jsonConfigPrism.GetOption(jsonData)
// // config is Some(Config{Host: "localhost", Port: 8080})
//
// See also:
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
// - github.com/IBM/fp-go/v2/optics/prism/iso for the dual composition (transforming focus type)
func Compose[S, A, B any](ab Prism[A, B]) P.Kleisli[S, Iso[S, A], B] {
return func(ia Iso[S, A]) Prism[S, B] {
return P.MakePrismWithName(
F.Flow2(ia.Get, ab.GetOption),
F.Flow2(ab.ReverseGet, ia.ReverseGet),
fmt.Sprintf("IsoCompose[%s -> %s]", ia, ab),
)
}
}

View File

@@ -0,0 +1,435 @@
// 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 prism
import (
"encoding/json"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestComposeWithEitherPrism tests composing a prism with an isomorphism using Either
func TestComposeWithEitherPrism(t *testing.T) {
// Create a prism that extracts Right values from Either[error, string]
rightPrism := P.FromEither[error, string]()
// Create an isomorphism between []byte and Either[error, string]
bytesEitherIso := I.MakeIso(
func(b []byte) E.Either[error, string] {
return E.Right[error](string(b))
},
func(e E.Either[error, string]) []byte {
return []byte(E.GetOrElse(func(error) string { return "" })(e))
},
)
// Compose them: Prism[Either, string] with Iso[[]byte, Either] -> Prism[[]byte, string]
bytesPrism := Compose[[]byte](rightPrism)(bytesEitherIso)
t.Run("GetOption extracts string from []byte", func(t *testing.T) {
bytes := []byte("hello")
result := bytesPrism.GetOption(bytes)
assert.True(t, O.IsSome(result))
str := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "hello", str)
})
t.Run("ReverseGet constructs []byte from string", func(t *testing.T) {
value := "world"
result := bytesPrism.ReverseGet(value)
assert.Equal(t, []byte("world"), result)
})
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
original := "test"
// ReverseGet to create []byte
bytes := bytesPrism.ReverseGet(original)
// GetOption to extract string back
result := bytesPrism.GetOption(bytes)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, original, extracted)
})
}
// TestComposeWithOptionPrism tests composing a prism with an isomorphism using Option
func TestComposeWithOptionPrism(t *testing.T) {
// Create a prism that extracts Some values from Option[int]
somePrism := P.FromOption[int]()
// Create an isomorphism between string and Option[int]
stringOptionIso := I.MakeIso(
func(s string) O.Option[int] {
i, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(i)
},
func(opt O.Option[int]) string {
return strconv.Itoa(O.GetOrElse(F.Constant(0))(opt))
},
)
// Compose them: Prism[Option, int] with Iso[string, Option] -> Prism[string, int]
stringPrism := Compose[string](somePrism)(stringOptionIso)
t.Run("GetOption extracts int from valid string", func(t *testing.T) {
result := stringPrism.GetOption("42")
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 42, num)
})
t.Run("GetOption returns None for invalid string", func(t *testing.T) {
result := stringPrism.GetOption("invalid")
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs string from int", func(t *testing.T) {
result := stringPrism.ReverseGet(100)
assert.Equal(t, "100", result)
})
}
// Custom types for testing
type Celsius float64
type Fahrenheit float64
type Temperature interface {
isTemperature()
}
type CelsiusTemp struct {
Value Celsius
}
func (c CelsiusTemp) isTemperature() {}
type FahrenheitTemp struct {
Value Fahrenheit
}
func (f FahrenheitTemp) isTemperature() {}
// TestComposeWithCustomPrism tests composing with custom types
func TestComposeWithCustomPrism(t *testing.T) {
// Prism that extracts Celsius from Temperature
celsiusPrism := P.MakePrism(
func(t Temperature) O.Option[Celsius] {
if ct, ok := t.(CelsiusTemp); ok {
return O.Some(ct.Value)
}
return O.None[Celsius]()
},
func(c Celsius) Temperature {
return CelsiusTemp{Value: c}
},
)
// Isomorphism between Fahrenheit and Temperature
fahrenheitTempIso := I.MakeIso(
func(f Fahrenheit) Temperature {
celsius := Celsius((f - 32) * 5 / 9)
return CelsiusTemp{Value: celsius}
},
func(t Temperature) Fahrenheit {
if ct, ok := t.(CelsiusTemp); ok {
return Fahrenheit(ct.Value*9/5 + 32)
}
return 0
},
)
// Compose: Prism[Temperature, Celsius] with Iso[Fahrenheit, Temperature] -> Prism[Fahrenheit, Celsius]
fahrenheitPrism := Compose[Fahrenheit](celsiusPrism)(fahrenheitTempIso)
t.Run("GetOption extracts Celsius from Fahrenheit", func(t *testing.T) {
fahrenheit := Fahrenheit(68)
result := fahrenheitPrism.GetOption(fahrenheit)
assert.True(t, O.IsSome(result))
celsius := O.GetOrElse(F.Constant(Celsius(0)))(result)
assert.InDelta(t, 20.0, float64(celsius), 0.01)
})
t.Run("ReverseGet constructs Fahrenheit from Celsius", func(t *testing.T) {
celsius := Celsius(20)
result := fahrenheitPrism.ReverseGet(celsius)
assert.InDelta(t, 68.0, float64(result), 0.01)
})
t.Run("Round-trip preserves value", func(t *testing.T) {
original := Celsius(25)
// ReverseGet to create Fahrenheit
fahrenheit := fahrenheitPrism.ReverseGet(original)
// GetOption to extract Celsius back
result := fahrenheitPrism.GetOption(fahrenheit)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(Celsius(0)))(result)
assert.InDelta(t, float64(original), float64(extracted), 0.01)
})
}
// TestComposeIdentityIso tests composing with an identity isomorphism
func TestComposeIdentityIso(t *testing.T) {
// Prism that extracts Right values
rightPrism := P.FromEither[error, string]()
// Identity isomorphism on Either
idIso := I.Id[E.Either[error, string]]()
// Compose with identity should not change behavior
composedPrism := Compose[E.Either[error, string]](rightPrism)(idIso)
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
either := E.Right[error]("test")
// Original prism
originalResult := rightPrism.GetOption(either)
// Composed prism
composedResult := composedPrism.GetOption(either)
assert.Equal(t, originalResult, composedResult)
})
t.Run("ReverseGet produces same result", func(t *testing.T) {
value := "test"
// Original prism
originalResult := rightPrism.ReverseGet(value)
// Composed prism
composedResult := composedPrism.ReverseGet(value)
assert.Equal(t, originalResult, composedResult)
})
}
// TestComposeChaining tests chaining multiple compositions
func TestComposeChaining(t *testing.T) {
// Prism: extracts Right values from Either[error, int]
rightPrism := P.FromEither[error, int]()
// Iso 1: string to Either[error, int]
stringEitherIso := I.MakeIso(
func(s string) E.Either[error, int] {
i, err := strconv.Atoi(s)
if err != nil {
return E.Left[int](err)
}
return E.Right[error](i)
},
func(e E.Either[error, int]) string {
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
},
)
// Iso 2: []byte to string
bytesStringIso := I.MakeIso(
func(b []byte) string { return string(b) },
func(s string) []byte { return []byte(s) },
)
// First composition: Prism[Either, int] with Iso[string, Either] -> Prism[string, int]
step1 := Compose[string](rightPrism)(stringEitherIso)
// Second composition: Prism[string, int] with Iso[[]byte, string] -> Prism[[]byte, int]
step2 := Compose[[]byte](step1)(bytesStringIso)
t.Run("Chained composition extracts correctly", func(t *testing.T) {
bytes := []byte("42")
result := step2.GetOption(bytes)
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 42, num)
})
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
num := 100
result := step2.ReverseGet(num)
assert.Equal(t, []byte("100"), result)
})
}
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
func TestComposePrismLaws(t *testing.T) {
// Create a prism
prism := P.FromEither[error, int]()
// Create an isomorphism from string to Either[error, int]
iso := I.MakeIso(
func(s string) E.Either[error, int] {
i, err := strconv.Atoi(s)
if err != nil {
return E.Left[int](err)
}
return E.Right[error](i)
},
func(e E.Either[error, int]) string {
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
},
)
// Compose them
composed := Compose[string](prism)(iso)
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
value := 42
// ReverseGet then GetOption should return Some(value)
source := composed.ReverseGet(value)
result := composed.GetOption(source)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, value, extracted)
})
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
source := "100"
// First GetOption
firstResult := composed.GetOption(source)
assert.True(t, O.IsSome(firstResult))
// Extract the value
value := O.GetOrElse(F.Constant(0))(firstResult)
// ReverseGet then GetOption again
reconstructed := composed.ReverseGet(value)
secondResult := composed.GetOption(reconstructed)
assert.True(t, O.IsSome(secondResult))
finalValue := O.GetOrElse(F.Constant(0))(secondResult)
assert.Equal(t, value, finalValue)
})
}
// TestComposeWithJSON tests a practical example with JSON parsing
func TestComposeWithJSON(t *testing.T) {
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
// Prism that extracts Config from []byte (via JSON parsing)
configPrism := P.MakePrism(
func(b []byte) O.Option[Config] {
var cfg Config
if err := json.Unmarshal(b, &cfg); err != nil {
return O.None[Config]()
}
return O.Some(cfg)
},
func(cfg Config) []byte {
b, _ := json.Marshal(cfg)
return b
},
)
// Isomorphism between string and []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Compose: Prism[[]byte, Config] with Iso[string, []byte] -> Prism[string, Config]
stringConfigPrism := Compose[string](configPrism)(stringBytesIso)
t.Run("GetOption parses valid JSON string", func(t *testing.T) {
jsonStr := `{"host":"localhost","port":8080}`
result := stringConfigPrism.GetOption(jsonStr)
assert.True(t, O.IsSome(result))
cfg := O.GetOrElse(F.Constant(Config{}))(result)
assert.Equal(t, "localhost", cfg.Host)
assert.Equal(t, 8080, cfg.Port)
})
t.Run("GetOption returns None for invalid JSON", func(t *testing.T) {
invalidJSON := `{invalid json}`
result := stringConfigPrism.GetOption(invalidJSON)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet creates JSON string from Config", func(t *testing.T) {
cfg := Config{Host: "example.com", Port: 443}
result := stringConfigPrism.ReverseGet(cfg)
// Parse it back to verify
var parsed Config
err := json.Unmarshal([]byte(result), &parsed)
assert.NoError(t, err)
assert.Equal(t, cfg, parsed)
})
}
// TestComposeWithEmptyValues tests edge cases with empty/zero values
func TestComposeWithEmptyValues(t *testing.T) {
// Prism that extracts Right values
prism := P.FromEither[error, []byte]()
// Isomorphism between string and Either[error, []byte]
iso := I.MakeIso(
func(s string) E.Either[error, []byte] {
return E.Right[error]([]byte(s))
},
func(e E.Either[error, []byte]) string {
return string(E.GetOrElse(func(error) []byte { return []byte{} })(e))
},
)
composed := Compose[string](prism)(iso)
t.Run("Empty string is handled correctly", func(t *testing.T) {
result := composed.GetOption("")
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
assert.Equal(t, []byte{}, bytes)
})
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
bytes := []byte{}
result := composed.ReverseGet(bytes)
assert.Equal(t, "", result)
})
}

View File

@@ -0,0 +1,99 @@
// 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 prism provides utilities for composing prisms with isomorphisms.
//
// This package enables the composition of prisms (optics for sum types) with
// isomorphisms (bidirectional transformations), allowing you to transform the
// source type of a prism using an isomorphism. This is the inverse operation
// of optics/prism/iso, where we transform the focus type instead of the source type.
//
// # Key Concepts
//
// A Prism[S, A] is an optic that focuses on a specific variant within a sum type S,
// extracting values of type A. An Iso[S, A] represents a bidirectional transformation
// between types S and A without loss of information.
//
// When you compose a Prism[A, B] with an Iso[S, A], you get a Prism[S, B] that:
// - Transforms S to A using the isomorphism's Get
// - Extracts values of type B from A (using the prism)
// - Can construct S from B by first using the prism's ReverseGet to get A, then the iso's ReverseGet
//
// # Example Usage
//
// import (
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between []byte and string
// bytesStringIso := iso.MakeIso(
// func(b []byte) string { return string(b) },
// func(s string) []byte { return []byte(s) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that works with []byte as source
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
//
// // Use the composed prism
// bytes := []byte(`{"status":"ok"}`)
// // First converts bytes to string via iso, then extracts Right value
// result := bytesPrism.GetOption(either.Right[error](string(bytes)))
//
// # Comparison with optics/prism/iso
//
// This package (optics/iso/prism) is the dual of optics/prism/iso:
// - optics/prism/iso: Composes Iso[A, B] with Prism[S, A] → Prism[S, B] (transforms focus type)
// - optics/iso/prism: Composes Prism[A, B] with Iso[S, A] → Prism[S, B] (transforms source type)
//
// # Type Aliases
//
// This package re-exports key types from the iso and prism packages for convenience:
// - Iso[S, A]: An isomorphism between types S and A
// - Prism[S, A]: A prism focusing on type A within sum type S
package prism
import (
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
)
type (
// Iso represents an isomorphism between types S and A.
// It is a bidirectional transformation that converts between two types
// without any loss of information.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
Iso[S, A any] = I.Iso[S, A]
// Prism is an optic used to select part of a sum type (tagged union).
// It provides operations to extract and construct values within sum types.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
Prism[S, A any] = P.Prism[S, A]
)

View File

@@ -0,0 +1,156 @@
// 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 iso
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
)
// Compose creates an operator that composes an isomorphism with a prism.
//
// This function takes an isomorphism Iso[A, B] and returns an operator that can
// transform any Prism[S, A] into a Prism[S, B]. The resulting prism maintains
// the same source type S but changes the focus type from A to B using the
// bidirectional transformation provided by the isomorphism.
//
// The composition works as follows:
// - GetOption: First extracts A from S using the prism, then transforms A to B using the iso's Get
// - ReverseGet: First transforms B to A using the iso's ReverseGet, then constructs S using the prism's ReverseGet
//
// This is particularly useful when you have a prism that focuses on one type but
// you need to work with a different type that has a lossless bidirectional
// transformation to the original type.
//
// Haskell Equivalent:
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
// specifically when composing a Prism with an Iso:
//
// prism . iso :: Prism s a -> Iso a b -> Prism s b
//
// In Haskell's lens library, this is part of the general optic composition mechanism.
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Prism.html
//
// Type Parameters:
// - S: The source type (sum type) that the prism operates on
// - A: The original focus type of the prism
// - B: The new focus type after applying the isomorphism
//
// Parameters:
// - ab: An isomorphism between types A and B that defines the bidirectional transformation
//
// Returns:
// - An Operator[S, A, B] that transforms Prism[S, A] into Prism[S, B]
//
// Laws:
// The composed prism must satisfy the prism laws:
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
//
// These laws are preserved because:
// - The isomorphism satisfies: ab.ReverseGet(ab.Get(a)) == a and ab.Get(ab.ReverseGet(b)) == b
// - The original prism satisfies the prism laws
//
// Example - Composing string/bytes isomorphism with Either prism:
//
// import (
// "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between string and []byte
// stringBytesIso := iso.MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that works with []byte instead of string
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
//
// // Extract bytes from a Right value
// success := either.Right[error]("hello")
// result := bytesPrism.GetOption(success)
// // result is Some([]byte("hello"))
//
// // Extract from a Left value returns None
// failure := either.Left[string](errors.New("error"))
// result = bytesPrism.GetOption(failure)
// // result is None
//
// // Construct an Either from bytes
// constructed := bytesPrism.ReverseGet([]byte("world"))
// // constructed is Right("world")
//
// Example - Composing with custom types:
//
// type Celsius float64
// type Fahrenheit float64
//
// // Isomorphism between Celsius and Fahrenheit
// tempIso := iso.MakeIso(
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
// )
//
// // Prism that extracts temperature from a weather report
// type WeatherReport struct {
// Temperature Celsius
// Condition string
// }
// tempPrism := prism.MakePrism(
// func(w WeatherReport) option.Option[Celsius] {
// return option.Some(w.Temperature)
// },
// func(c Celsius) WeatherReport {
// return WeatherReport{Temperature: c}
// },
// )
//
// // Compose to work with Fahrenheit instead
// fahrenheitPrism := PI.Compose(tempIso)(tempPrism)
//
// report := WeatherReport{Temperature: 20, Condition: "sunny"}
// temp := fahrenheitPrism.GetOption(report)
// // temp is Some(68.0) in Fahrenheit
//
// See also:
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
// - Operator for the type signature of the returned function
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
return func(pa Prism[S, A]) Prism[S, B] {
return P.MakePrismWithName(
F.Flow2(
pa.GetOption,
O.Map(ab.Get),
),
F.Flow2(
ab.ReverseGet,
pa.ReverseGet,
),
fmt.Sprintf("PrismCompose[%s -> %s]", pa, ab),
)
}
}

View File

@@ -0,0 +1,369 @@
// 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 iso
import (
"errors"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestComposeWithEitherPrism tests composing an isomorphism with an Either prism
func TestComposeWithEitherPrism(t *testing.T) {
// Create an isomorphism between string and []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Create a prism that extracts Right values from Either[error, string]
rightPrism := P.FromEither[error, string]()
// Compose them
bytesPrism := Compose[E.Either[error, string]](stringBytesIso)(rightPrism)
t.Run("GetOption extracts and transforms Right value", func(t *testing.T) {
success := E.Right[error]("hello")
result := bytesPrism.GetOption(success)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, []byte("hello"), bytes)
})
t.Run("GetOption returns None for Left value", func(t *testing.T) {
failure := E.Left[string](errors.New("error"))
result := bytesPrism.GetOption(failure)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Either from transformed value", func(t *testing.T) {
bytes := []byte("world")
result := bytesPrism.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
str := E.GetOrElse(func(error) string { return "" })(result)
assert.Equal(t, "world", str)
})
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
// Start with bytes
original := []byte("test")
// ReverseGet to create Either
either := bytesPrism.ReverseGet(original)
// GetOption to extract bytes back
result := bytesPrism.GetOption(either)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, original, extracted)
})
}
// TestComposeWithOptionPrism tests composing an isomorphism with an Option prism
func TestComposeWithOptionPrism(t *testing.T) {
// Create an isomorphism between int and string
intStringIso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Create a prism that extracts Some values from Option[int]
somePrism := P.FromOption[int]()
// Compose them
stringPrism := Compose[O.Option[int]](intStringIso)(somePrism)
t.Run("GetOption extracts and transforms Some value", func(t *testing.T) {
some := O.Some(42)
result := stringPrism.GetOption(some)
assert.True(t, O.IsSome(result))
str := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "42", str)
})
t.Run("GetOption returns None for None value", func(t *testing.T) {
none := O.None[int]()
result := stringPrism.GetOption(none)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Option from transformed value", func(t *testing.T) {
str := "100"
result := stringPrism.ReverseGet(str)
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 100, num)
})
}
// TestComposeWithCustomPrism tests composing with a custom prism
// Custom types for TestComposeWithCustomPrism
type Celsius float64
type Fahrenheit float64
type Temperature interface {
isTemperature()
}
type CelsiusTemp struct {
Value Celsius
}
func (c CelsiusTemp) isTemperature() {}
type KelvinTemp struct {
Value float64
}
func (k KelvinTemp) isTemperature() {}
func TestComposeWithCustomPrism(t *testing.T) {
// Isomorphism between Celsius and Fahrenheit
tempIso := I.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
// Prism that extracts Celsius from Temperature
celsiusPrism := P.MakePrism(
func(t Temperature) O.Option[Celsius] {
if ct, ok := t.(CelsiusTemp); ok {
return O.Some(ct.Value)
}
return O.None[Celsius]()
},
func(c Celsius) Temperature {
return CelsiusTemp{Value: c}
},
)
// Compose to work with Fahrenheit
fahrenheitPrism := Compose[Temperature](tempIso)(celsiusPrism)
t.Run("GetOption extracts and converts Celsius to Fahrenheit", func(t *testing.T) {
temp := CelsiusTemp{Value: 0}
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsSome(result))
fahrenheit := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
assert.InDelta(t, 32.0, float64(fahrenheit), 0.01)
})
t.Run("GetOption returns None for non-Celsius temperature", func(t *testing.T) {
temp := KelvinTemp{Value: 273.15}
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Temperature from Fahrenheit", func(t *testing.T) {
fahrenheit := Fahrenheit(68)
result := fahrenheitPrism.ReverseGet(fahrenheit)
celsiusTemp, ok := result.(CelsiusTemp)
assert.True(t, ok)
assert.InDelta(t, 20.0, float64(celsiusTemp.Value), 0.01)
})
t.Run("Round-trip preserves value", func(t *testing.T) {
original := Fahrenheit(100)
// ReverseGet to create Temperature
temp := fahrenheitPrism.ReverseGet(original)
// GetOption to extract Fahrenheit back
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
assert.InDelta(t, float64(original), float64(extracted), 0.01)
})
}
// TestComposeIdentityIso tests composing with an identity isomorphism
func TestComposeIdentityIso(t *testing.T) {
// Identity isomorphism (no transformation)
idIso := I.Id[string]()
// Prism that extracts Right values
rightPrism := P.FromEither[error, string]()
// Compose with identity should not change behavior
composedPrism := Compose[E.Either[error, string]](idIso)(rightPrism)
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
success := E.Right[error]("test")
// Original prism
originalResult := rightPrism.GetOption(success)
// Composed prism
composedResult := composedPrism.GetOption(success)
assert.Equal(t, originalResult, composedResult)
})
t.Run("ReverseGet produces same result", func(t *testing.T) {
value := "test"
// Original prism
originalEither := rightPrism.ReverseGet(value)
// Composed prism
composedEither := composedPrism.ReverseGet(value)
assert.Equal(t, originalEither, composedEither)
})
}
// TestComposeChaining tests chaining multiple compositions
func TestComposeChaining(t *testing.T) {
// First isomorphism: int to string
intStringIso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Second isomorphism: string to []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Prism that extracts Right values
rightPrism := P.FromEither[error, int]()
// Chain compositions: Either[error, int] -> int -> string -> []byte
step1 := Compose[E.Either[error, int]](intStringIso)(rightPrism) // Prism[Either[error, int], string]
step2 := Compose[E.Either[error, int]](stringBytesIso)(step1) // Prism[Either[error, int], []byte]
t.Run("Chained composition extracts and transforms correctly", func(t *testing.T) {
either := E.Right[error](42)
result := step2.GetOption(either)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, []byte("42"), bytes)
})
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
bytes := []byte("100")
result := step2.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
num := E.GetOrElse(func(error) int { return 0 })(result)
assert.Equal(t, 100, num)
})
}
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
func TestComposePrismLaws(t *testing.T) {
// Create an isomorphism
iso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Create a prism
prism := P.FromEither[error, int]()
// Compose them
composed := Compose[E.Either[error, int]](iso)(prism)
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
value := "42"
// ReverseGet then GetOption should return Some(value)
either := composed.ReverseGet(value)
result := composed.GetOption(either)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, value, extracted)
})
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
either := E.Right[error](100)
// First GetOption
firstResult := composed.GetOption(either)
assert.True(t, O.IsSome(firstResult))
// Extract the value
value := O.GetOrElse(F.Constant(""))(firstResult)
// ReverseGet then GetOption again
reconstructed := composed.ReverseGet(value)
secondResult := composed.GetOption(reconstructed)
assert.True(t, O.IsSome(secondResult))
finalValue := O.GetOrElse(F.Constant(""))(secondResult)
assert.Equal(t, value, finalValue)
})
}
// TestComposeWithEmptyValues tests edge cases with empty/zero values
func TestComposeWithEmptyValues(t *testing.T) {
// Isomorphism that handles empty strings
iso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
prism := P.FromEither[error, string]()
composed := Compose[E.Either[error, string]](iso)(prism)
t.Run("Empty string is handled correctly", func(t *testing.T) {
either := E.Right[error]("")
result := composed.GetOption(either)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
assert.Equal(t, []byte{}, bytes)
})
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
bytes := []byte{}
result := composed.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
str := E.GetOrElse(func(error) string { return "default" })(result)
assert.Equal(t, "", str)
})
}

View File

@@ -0,0 +1,112 @@
// 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 iso provides utilities for composing isomorphisms with prisms.
//
// This package enables the composition of isomorphisms (bidirectional transformations)
// with prisms (optics for sum types), allowing you to transform the focus type of a prism
// using an isomorphism. This is particularly useful when you need to work with prisms
// that focus on a type that can be bidirectionally converted to another type.
//
// # Key Concepts
//
// An Iso[S, A] represents a bidirectional transformation between types S and A without
// loss of information. A Prism[S, A] is an optic that focuses on a specific variant
// within a sum type S, extracting values of type A.
//
// When you compose an Iso[A, B] with a Prism[S, A], you get a Prism[S, B] that:
// - Extracts values of type A from S (using the prism)
// - Transforms them to type B (using the isomorphism's Get)
// - Can construct S from B by reversing the transformation (using the isomorphism's ReverseGet)
//
// # Example Usage
//
// import (
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between string and []byte
// stringBytesIso := iso.MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that extracts Right values as []byte
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
//
// // Use the composed prism
// either := either.Right[error]("hello")
// result := bytesPrism.GetOption(either) // Some([]byte("hello"))
//
// # Type Aliases
//
// This package re-exports key types from the iso and prism packages for convenience:
// - Iso[S, A]: An isomorphism between types S and A
// - Prism[S, A]: A prism focusing on type A within sum type S
// - Operator[S, A, B]: A function that transforms Prism[S, A] to Prism[S, B]
package iso
import (
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
)
type (
// Iso represents an isomorphism between types S and A.
// It is a bidirectional transformation that converts between two types
// without any loss of information.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
Iso[S, A any] = I.Iso[S, A]
// Prism is an optic used to select part of a sum type (tagged union).
// It provides operations to extract and construct values within sum types.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
Prism[S, A any] = P.Prism[S, A]
// Operator represents a function that transforms one prism into another.
// It takes a Prism[S, A] and returns a Prism[S, B], allowing for prism transformations.
//
// This is commonly used with the Compose function to create operators that
// transform the focus type of a prism using an isomorphism.
//
// Type Parameters:
// - S: The source type (remains constant)
// - A: The original focus type
// - B: The new focus type
//
// Example:
//
// // Create an operator that transforms string prisms to []byte prisms
// stringToBytesOp := Compose(stringBytesIso)
// // Apply it to a prism
// bytesPrism := stringToBytesOp(stringPrism)
Operator[S, A, B any] = P.Operator[S, A, B]
)

View File

@@ -23,8 +23,10 @@ import (
"strconv"
"time"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
J "github.com/IBM/fp-go/v2/json"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
)
@@ -322,6 +324,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
}
// FromResult creates a prism for extracting values from Result types.
// It provides a safe way to work with Result values (which are Either[error, T]),
// focusing on the success case and handling errors gracefully through the Option type.
//
// This is a convenience function that is equivalent to FromEither[error, T]().
//
// The prism's GetOption attempts to extract the success value from a Result.
// If the Result is successful, it returns Some(value); if it's an error, it returns None.
//
// The prism's ReverseGet always succeeds, wrapping a value into a successful Result.
//
// Type Parameters:
// - T: The value type contained in the Result
//
// Returns:
// - A Prism[Result[T], T] that safely extracts success values
//
// Example:
//
// // Create a prism for extracting successful results
// resultPrism := FromResult[int]()
//
// // Extract from successful result
// success := result.Of[int](42)
// value := resultPrism.GetOption(success) // Some(42)
//
// // Extract from error result
// failure := result.Error[int](errors.New("failed"))
// value = resultPrism.GetOption(failure) // None[int]()
//
// // Wrap value into successful Result
// wrapped := resultPrism.ReverseGet(100) // Result containing 100
//
// // Use with Set to update successful results
// setter := Set[Result[int], int](200)
// result := setter(resultPrism)(success) // Result containing 200
// result = setter(resultPrism)(failure) // Error result (unchanged)
//
// Common use cases:
// - Extracting successful values from Result types
// - Filtering out errors in data pipelines
// - Working with fallible operations that return Result
// - Composing with other prisms for complex error handling
//
//go:inline
func FromResult[T any]() Prism[Result[T], T] {
return FromEither[error, T]()
@@ -1261,3 +1307,71 @@ func MakeURLPrisms() URLPrisms {
RawFragment: _prismRawFragment,
}
}
// ParseJSON creates a prism for parsing and marshaling JSON data.
// It provides a safe way to convert between JSON bytes and Go types,
// handling parsing and marshaling errors gracefully through the Option type.
//
// The prism's GetOption attempts to unmarshal JSON bytes into type A.
// If unmarshaling succeeds, it returns Some(A); if it fails (e.g., invalid JSON
// or type mismatch), it returns None.
//
// The prism's ReverseGet marshals a value of type A into JSON bytes.
// If marshaling fails (which is rare), it returns an empty byte slice.
//
// Type Parameters:
// - A: The Go type to unmarshal JSON into
//
// Returns:
// - A Prism[[]byte, A] that safely handles JSON parsing/marshaling
//
// Example:
//
// // Define a struct type
// type Person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// // Create a JSON parsing prism
// jsonPrism := ParseJSON[Person]()
//
// // Parse valid JSON
// jsonData := []byte(`{"name":"Alice","age":30}`)
// person := jsonPrism.GetOption(jsonData)
// // Some(Person{Name: "Alice", Age: 30})
//
// // Parse invalid JSON
// invalidJSON := []byte(`{invalid json}`)
// result := jsonPrism.GetOption(invalidJSON) // None[Person]()
//
// // Marshal to JSON
// p := Person{Name: "Bob", Age: 25}
// jsonBytes := jsonPrism.ReverseGet(p)
// // []byte(`{"name":"Bob","age":25}`)
//
// // Use with Set to update JSON data
// newPerson := Person{Name: "Charlie", Age: 35}
// setter := Set[[]byte, Person](newPerson)
// updated := setter(jsonPrism)(jsonData)
// // []byte(`{"name":"Charlie","age":35}`)
//
// Common use cases:
// - Parsing JSON configuration files
// - Working with JSON API responses
// - Validating and transforming JSON data in pipelines
// - Type-safe JSON deserialization
// - Converting between JSON and Go structs
func ParseJSON[A any]() Prism[[]byte, A] {
return MakePrismWithName(
F.Flow2(
J.Unmarshal[A],
either.ToOption[error, A],
),
F.Flow2(
J.Marshal[A],
either.GetOrElse(F.Constant1[error](array.Empty[byte]())),
),
"JSON",
)
}

View File

@@ -16,11 +16,14 @@
package prism
import (
"errors"
"regexp"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -1396,3 +1399,403 @@ func TestNonEmptyStringValidation(t *testing.T) {
assert.Equal(t, []string{"hello", "world", "test"}, nonEmpty)
})
}
// TestFromResult tests the FromResult prism with Result types
func TestFromResult(t *testing.T) {
t.Run("extract from successful result", func(t *testing.T) {
prism := FromResult[int]()
success := result.Of[int](42)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("extract from error result", func(t *testing.T) {
prism := FromResult[int]()
failure := E.Left[int](errors.New("test error"))
extracted := prism.GetOption(failure)
assert.True(t, O.IsNone(extracted))
})
t.Run("ReverseGet wraps value in successful result", func(t *testing.T) {
prism := FromResult[int]()
wrapped := prism.ReverseGet(100)
// Verify it's a successful result
extracted := prism.GetOption(wrapped)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 100, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("works with string type", func(t *testing.T) {
prism := FromResult[string]()
success := result.Of[string]("hello")
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, "hello", O.GetOrElse(F.Constant(""))(extracted))
})
t.Run("works with struct type", func(t *testing.T) {
type Person struct {
Name string
Age int
}
prism := FromResult[Person]()
person := Person{Name: "Alice", Age: 30}
success := result.Of[Person](person)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
result := O.GetOrElse(F.Constant(Person{}))(extracted)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
// TestFromResultWithSet tests using Set with FromResult prism
func TestFromResultWithSet(t *testing.T) {
t.Run("set on successful result", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
success := result.Of[int](42)
updated := setter(prism)(success)
// Verify the value was updated
extracted := prism.GetOption(updated)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 200, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
failure := E.Left[int](errors.New("test error"))
updated := setter(prism)(failure)
// Verify it's still an error
extracted := prism.GetOption(updated)
assert.True(t, O.IsNone(extracted))
})
}
// TestFromResultPrismLaws tests that FromResult satisfies prism laws
func TestFromResultPrismLaws(t *testing.T) {
prism := FromResult[int]()
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
value := 42
wrapped := prism.ReverseGet(value)
extracted := prism.GetOption(wrapped)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, value, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
value := 42
result1 := prism.ReverseGet(value)
result2 := prism.ReverseGet(value)
// Both should extract the same value
extracted1 := prism.GetOption(result1)
extracted2 := prism.GetOption(result2)
val1 := O.GetOrElse(F.Constant(-1))(extracted1)
val2 := O.GetOrElse(F.Constant(-1))(extracted2)
assert.Equal(t, val1, val2)
})
}
// TestFromResultComposition tests composing FromResult with other prisms
func TestFromResultComposition(t *testing.T) {
t.Run("compose with predicate prism", func(t *testing.T) {
// Create a prism that only matches positive numbers
positivePrism := FromPredicate(func(n int) bool { return n > 0 })
// Compose: Result[int] -> int -> positive int
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
// Test with positive number
success := result.Of[int](42)
extracted := composed.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
// Test with negative number
negativeSuccess := result.Of[int](-5)
extracted = composed.GetOption(negativeSuccess)
assert.True(t, O.IsNone(extracted))
// Test with error
failure := E.Left[int](errors.New("test error"))
extracted = composed.GetOption(failure)
assert.True(t, O.IsNone(extracted))
})
}
// TestParseJSON tests the ParseJSON prism with various JSON data
func TestParseJSON(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("parse valid JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
jsonData := []byte(`{"name":"Alice","age":30}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
})
t.Run("parse invalid JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
invalidJSON := []byte(`{invalid json}`)
parsed := prism.GetOption(invalidJSON)
assert.True(t, O.IsNone(parsed))
})
t.Run("parse JSON with missing fields", func(t *testing.T) {
prism := ParseJSON[Person]()
// Missing age field - should use zero value
jsonData := []byte(`{"name":"Bob"}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Bob", person.Name)
assert.Equal(t, 0, person.Age)
})
t.Run("parse JSON with extra fields", func(t *testing.T) {
prism := ParseJSON[Person]()
// Extra field should be ignored
jsonData := []byte(`{"name":"Charlie","age":25,"extra":"ignored"}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Charlie", person.Name)
assert.Equal(t, 25, person.Age)
})
t.Run("ReverseGet marshals to JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
person := Person{Name: "David", Age: 35}
jsonBytes := prism.ReverseGet(person)
// Parse it back to verify
parsed := prism.GetOption(jsonBytes)
assert.True(t, O.IsSome(parsed))
result := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "David", result.Name)
assert.Equal(t, 35, result.Age)
})
t.Run("works with primitive types", func(t *testing.T) {
prism := ParseJSON[int]()
jsonData := []byte(`42`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(parsed))
})
t.Run("works with arrays", func(t *testing.T) {
prism := ParseJSON[[]string]()
jsonData := []byte(`["hello","world","test"]`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
arr := O.GetOrElse(F.Constant([]string{}))(parsed)
assert.Equal(t, []string{"hello", "world", "test"}, arr)
})
t.Run("works with maps", func(t *testing.T) {
prism := ParseJSON[map[string]int]()
jsonData := []byte(`{"a":1,"b":2,"c":3}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
m := O.GetOrElse(F.Constant(map[string]int{}))(parsed)
assert.Equal(t, 1, m["a"])
assert.Equal(t, 2, m["b"])
assert.Equal(t, 3, m["c"])
})
t.Run("works with nested structures", func(t *testing.T) {
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
type PersonWithAddress struct {
Name string `json:"name"`
Address Address `json:"address"`
}
prism := ParseJSON[PersonWithAddress]()
jsonData := []byte(`{"name":"Eve","address":{"street":"123 Main St","city":"NYC"}}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(PersonWithAddress{}))(parsed)
assert.Equal(t, "Eve", person.Name)
assert.Equal(t, "123 Main St", person.Address.Street)
assert.Equal(t, "NYC", person.Address.City)
})
t.Run("parse empty JSON object", func(t *testing.T) {
prism := ParseJSON[Person]()
jsonData := []byte(`{}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "", person.Name)
assert.Equal(t, 0, person.Age)
})
t.Run("parse null JSON", func(t *testing.T) {
prism := ParseJSON[*Person]()
jsonData := []byte(`null`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(&Person{}))(parsed)
assert.Nil(t, person)
})
}
// TestParseJSONWithSet tests using Set with ParseJSON prism
func TestParseJSONWithSet(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("set updates JSON data", func(t *testing.T) {
prism := ParseJSON[Person]()
originalJSON := []byte(`{"name":"Alice","age":30}`)
newPerson := Person{Name: "Bob", Age: 25}
setter := Set[[]byte, Person](newPerson)
updatedJSON := setter(prism)(originalJSON)
// Parse the updated JSON
parsed := prism.GetOption(updatedJSON)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Bob", person.Name)
assert.Equal(t, 25, person.Age)
})
t.Run("set on invalid JSON returns original unchanged", func(t *testing.T) {
prism := ParseJSON[Person]()
invalidJSON := []byte(`{invalid}`)
newPerson := Person{Name: "Charlie", Age: 35}
setter := Set[[]byte, Person](newPerson)
result := setter(prism)(invalidJSON)
// Should return original unchanged since it couldn't be parsed
assert.Equal(t, invalidJSON, result)
})
}
// TestParseJSONPrismLaws tests that ParseJSON satisfies prism laws
func TestParseJSONPrismLaws(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
prism := ParseJSON[Person]()
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
person := Person{Name: "Alice", Age: 30}
jsonBytes := prism.ReverseGet(person)
parsed := prism.GetOption(jsonBytes)
assert.True(t, O.IsSome(parsed))
result := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, person.Name, result.Name)
assert.Equal(t, person.Age, result.Age)
})
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
person := Person{Name: "Bob", Age: 25}
json1 := prism.ReverseGet(person)
json2 := prism.ReverseGet(person)
// Both should parse to the same value
parsed1 := prism.GetOption(json1)
parsed2 := prism.GetOption(json2)
result1 := O.GetOrElse(F.Constant(Person{}))(parsed1)
result2 := O.GetOrElse(F.Constant(Person{}))(parsed2)
assert.Equal(t, result1.Name, result2.Name)
assert.Equal(t, result1.Age, result2.Age)
})
}
// TestParseJSONComposition tests composing ParseJSON with other prisms
func TestParseJSONComposition(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("compose with predicate prism", func(t *testing.T) {
// Create a prism that only matches adults (age >= 18)
adultPrism := FromPredicate(func(p Person) bool { return p.Age >= 18 })
// Compose: []byte -> Person -> Adult
composed := Compose[[]byte](adultPrism)(ParseJSON[Person]())
// Test with adult
adultJSON := []byte(`{"name":"Alice","age":30}`)
parsed := composed.GetOption(adultJSON)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Alice", person.Name)
// Test with minor
minorJSON := []byte(`{"name":"Bob","age":15}`)
parsed = composed.GetOption(minorJSON)
assert.True(t, O.IsNone(parsed))
// Test with invalid JSON
invalidJSON := []byte(`{invalid}`)
parsed = composed.GetOption(invalidJSON)
assert.True(t, O.IsNone(parsed))
})
}

View File

@@ -358,5 +358,3 @@ func TestChainFirstConsumer_ComplexType(t *testing.T) {
assert.InDelta(t, 10.989, finalProduct.Price, 0.001)
}
}
// Made with Bob

View File

@@ -687,5 +687,3 @@ func BenchmarkPartitionMapError(b *testing.B) {
_ = partitionMap(input)
}
}
// Made with Bob