mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-28 13:12:03 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47727fd514 | ||
|
|
ece7d088ea | ||
|
|
13d25eca32 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -385,5 +385,3 @@ func TestFlatMapOptionK_NestedOptions(t *testing.T) {
|
||||
expected := A.From(10, 30, 50)
|
||||
assert.Equal(t, expected, values)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
153
v2/optics/iso/prism/compose.go
Normal file
153
v2/optics/iso/prism/compose.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
435
v2/optics/iso/prism/compose_test.go
Normal file
435
v2/optics/iso/prism/compose_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
99
v2/optics/iso/prism/types.go
Normal file
99
v2/optics/iso/prism/types.go
Normal 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]
|
||||
)
|
||||
156
v2/optics/prism/iso/compose.go
Normal file
156
v2/optics/prism/iso/compose.go
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
369
v2/optics/prism/iso/compose_test.go
Normal file
369
v2/optics/prism/iso/compose_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
112
v2/optics/prism/iso/types.go
Normal file
112
v2/optics/prism/iso/types.go
Normal 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]
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -358,5 +358,3 @@ func TestChainFirstConsumer_ComplexType(t *testing.T) {
|
||||
assert.InDelta(t, 10.989, finalProduct.Price, 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -687,5 +687,3 @@ func BenchmarkPartitionMapError(b *testing.B) {
|
||||
_ = partitionMap(input)
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
Reference in New Issue
Block a user