Hi! I have a few comments and ideas on the optics implementation. I'm just learning rust, coming from the Haskell/Swift/Kotlin world, so apologies for not using rusty syntax. Further apologies if all this information is known to you.
A first thought is that your lens definition is not polymorphic. It is parameterized over just <S,A>, and not <S,T,A,B>. Is that intentional for simplicity?
Another general comment: you've chosen what I think are called "normal" lens structures (i.e., a pair of functions), which is how I think Monocle defines them. The other notable approaches are profunctor optics (type Optic p s t a b = p a b -> p s t) and Van Laarhoven optics (e.g., type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t). The primary difference has to do with the cardinality of manual compositions you have to define for all the different combinations of optics types.
Your lens structure getter is returning an Option<A>. However, this is not the definition of a lens. That would be more correctly called an affine traversal (or also called Option or Optional, though that naming obviously conflicts with the Option<A> type in Rust).
Sorry for the Swift, but here's a quick set of definitions as I understand these types:
// Iso: a lossless conversion between types
struct IsoP<S,T,A,B> {
let to: (S) -> A
let from: (B) -> T
}
// Non-polymorphic variant
typealias Iso<S,A> = IsoP<S,S,A,A>
// Lens: a getter/setter for a product type
struct LensP<S,T,A,B> {
let get: (S) -> A
let set: (S, B) -> T
}
// Non-polymorphic variant
typealias Lens<S,A> = LensP<S,S,A,A>
// Prism: a getter/setter for a sum type
struct PrismP<S,T,A,B> {
let extract: (S) -> A?, // Or, more properly "match: (S) -> Either<T,A>"
let embed: (B) -> T
}
// Non-polymorphic variant
typealias Prism<S,A> = PrismP<S,S,A,A>
// Affine Traversal: a composition of a lens and a prism.
struct AffineP<S,T,A,B> {
let tryGet: (S) -> A?,
let set: (S, B) -> T
}
// Non-polymorphic variant
typealias Affine<S,A> = AffineP<S,S,A,A>
Using this approach, I find it easiest to define 1) how an optic composes with itself (e.g., Lens<S,T,A,B>+Lens<A,B,C,D>=Lens<S,T,C,D>), then 2) define how types can be promoted (e.g., an iso is trivially a prism and a lens, and a lens and a prism are trivially and affine traversal), then finally, 3) all the other compositions are trivial definitions (e.g., Lens<S,T,A,B>+Prism<A,B,C,D>=Affine<S,T,C,D> by way of something like func compose(lens, prism) -> Affine { compose(lens.asAffine(), prism.asAffine()) }).
Defining a fairly complete family of optics in this manner turns out not to be too complicated, with the only downside that you don't get composition via function composition; you have to provide your own composition operator/function. (Whereas profunctor lenses are simply functions that compose naturally.)
I'd be curious to hear more of your thoughts on the direction you want to go on optics within this library. If you are interested, I can also share a more complete example of the above swift code that includes all the composition implementations.
Hi! I have a few comments and ideas on the optics implementation. I'm just learning rust, coming from the Haskell/Swift/Kotlin world, so apologies for not using rusty syntax. Further apologies if all this information is known to you.
A first thought is that your lens definition is not polymorphic. It is parameterized over just
<S,A>
, and not<S,T,A,B>
. Is that intentional for simplicity?Another general comment: you've chosen what I think are called "normal" lens structures (i.e., a pair of functions), which is how I think Monocle defines them. The other notable approaches are profunctor optics (
type Optic p s t a b = p a b -> p s t
) and Van Laarhoven optics (e.g.,type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
). The primary difference has to do with the cardinality of manual compositions you have to define for all the different combinations of optics types.Your lens structure getter is returning an
Option<A>
. However, this is not the definition of a lens. That would be more correctly called an affine traversal (or also calledOption
orOptional
, though that naming obviously conflicts with theOption<A>
type in Rust).Sorry for the Swift, but here's a quick set of definitions as I understand these types:
Using this approach, I find it easiest to define 1) how an optic composes with itself (e.g.,
Lens<S,T,A,B>
+Lens<A,B,C,D>
=Lens<S,T,C,D>
), then 2) define how types can be promoted (e.g., an iso is trivially a prism and a lens, and a lens and a prism are trivially and affine traversal), then finally, 3) all the other compositions are trivial definitions (e.g.,Lens<S,T,A,B>
+Prism<A,B,C,D>
=Affine<S,T,C,D>
by way of something likefunc compose(lens, prism) -> Affine { compose(lens.asAffine(), prism.asAffine()) }
).Defining a fairly complete family of optics in this manner turns out not to be too complicated, with the only downside that you don't get composition via function composition; you have to provide your own composition operator/function. (Whereas profunctor lenses are simply functions that compose naturally.)
I'd be curious to hear more of your thoughts on the direction you want to go on optics within this library. If you are interested, I can also share a more complete example of the above swift code that includes all the composition implementations.