Skip to main content

Module profunctor_analysis

Module profunctor_analysis 

Source
Expand description

§Profunctor Classes: Rust vs PureScript Analysis

This document compares the profunctor class hierarchy in fp-library against the PureScript reference implementations in purescript-profunctor and purescript-profunctor-lenses, and analyses naming consistency within the Rust codebase.


§1. Class Hierarchy Comparison

PureScriptRustSuperclassStatus
Profunctor pProfunctor-Complete
Strong pStrongProfunctorComplete
Choice pChoiceProfunctorComplete
Closed pClosed<FunctionBrand: LiftFn>ProfunctorComplete (parameterized)
Cochoice pCochoiceProfunctorComplete
Costrong pCostrongProfunctorComplete
Wander pWanderStrong + ChoiceComplete

The class hierarchy is faithfully reproduced. All superclass relationships match.


§2. Method Comparison

§2.1 Profunctor

PureScriptRustNotes
dimap :: (a -> b) -> (c -> d) -> p b c -> p a ddimap<A, B, C, D, FuncAB, FuncCD>(ab, cd, pbc)Identical semantics
-lmap<A, B, C, FuncAB>(ab, pbc) (default impl)Trait method in Rust
-rmap<A, B, C, FuncBC>(bc, pab) (default impl)Trait method in Rust

PureScript defines only dimap as a class method. lcmap (PureScript’s equivalent of lmap) and rmap are standalone free functions. Rust promotes both to trait methods with default implementations derived from dimap. This is a reasonable Rust idiom -it allows implementors to override with more efficient versions.

§2.2 Strong

PureScriptRustNotes
first :: p a b -> p (Tuple a c) (Tuple b c)first<A, B, C>(pab)Identical. Uses native tuples.
second :: p b c -> p (Tuple a b) (Tuple a c)second<A, B, C>(pab)Identical

§2.3 Choice

PureScriptRustNotes
left :: p a b -> p (Either a c) (Either b c)left<A, B, C>(pab) -> ...Result<C, A>, Result<C, B>Either ->Result (see §3.1)
right :: p b c -> p (Either a b) (Either a c)right<A, B, C>(pab) -> ...Result<A, C>, Result<B, C>Same adaptation

§2.4 Closed

PureScriptRustNotes
closed :: p a b -> p (x -> a) (x -> b)closed<A, B, X>(pab) -> ...FunctionBrand::Of<X, A>, FunctionBrand::Of<X, B>Parameterized over FunctionBrand (see §3.2)

§2.5 Cochoice

PureScriptRustNotes
unleft :: p (Either a c) (Either b c) -> p a bunleft<A, B, C>(pab)Identical (with Either ->Result)
unright :: p (Either a b) (Either a c) -> p b cunright<A, B, C>(pab)Identical

§2.6 Costrong

PureScriptRustNotes
unfirst :: p (Tuple a c) (Tuple b c) -> p a bunfirst<A, B, C>(pab)Identical
unsecond :: p (Tuple a b) (Tuple a c) -> p b cunsecond<A, B, C>(pab)Identical

§2.7 Wander

PureScriptRustNotes
wander :: (forall f. Applicative f => (a -> f b) -> s -> f t) -> p a b -> p s twander<S, T, A, B, TFunc>(traversal, pab)Rank-2 type replaced by TFunc: TraversalFunc (see §3.3)

§3. Rust-Specific Adaptations

§3.1 Either ->Result

PureScript’s Either a b maps to Rust’s Result<B, A> with reversed parameter order:

PureScriptRustSemantic role
Left aErr(A)Active/focus variant
Right cOk(C)Passthrough variant

This means Choice::left acts on the Err variant and Choice::right acts on the Ok variant. The naming is faithful to PureScript’s conventions but may be unintuitive to Rust users who associate Ok with the “primary” case.

§3.2 Closed parameterization

PureScript’s Closed uses bare function types x -> a. Rust cannot express this directly -closures must be wrapped in Rc<dyn Fn> or Arc<dyn Fn>. The Rust Closed<FunctionBrand: LiftFn> trait takes an extra type parameter FunctionBrand to abstract over the function wrapping strategy.

§3.3 fan_out requires A: Clone

PureScript’s fanout duplicates the input with \a -> Tuple a a. Rust’s move semantics require A: Clone to achieve this. The same justification applies as for Closed::closed’s X: Clone.

§3.4 Wander and rank-2 types

PureScript’s wander uses a rank-2 type (forall f. Applicative f => ...). Rust lacks rank-2 polymorphism, so this is encoded via the TraversalFunc trait, which provides a concrete apply method that the Wander implementation calls with specific applicative functors.


§4. Free Function Comparison

§4.1 Present in both

PureScriptRustNotes
dimapdimapIdentical
lcmaplmapName difference (see §5.1)
rmaprmapIdentical
firstfirstIdentical
secondsecondIdentical
leftleftIdentical
rightrightIdentical
closedclosedIdentical
unleftunleftIdentical
unrightunrightIdentical
unfirstunfirstIdentical
unsecondunsecondIdentical
wanderwanderIdentical
arrarrowName difference; free function with Category + Profunctor bounds
splitStrong (***)split_strongsnake_case; Semigroupoid + Strong bounds
fanout (&&&)fan_outsnake_case; Semigroupoid + Strong bounds; A: Clone (see §3.4)
splitChoice (+++)split_choicesnake_case; Semigroupoid + Choice bounds
fanin (|||)fan_insnake_case; Semigroupoid + Choice bounds

§5. Naming Consistency

§5.1 lmap vs PureScript’s lcmap

PureScript renamed lmap to lcmap to avoid conflict with Data.Functor.Contravariant.cmap. The Rust library uses lmap, which matches the name used in Haskell’s profunctors package and in the profunctor literature. This is a deliberate and reasonable choice.

§5.2 Free function brand parameter naming

All profunctor free functions consistently use Brand for the profunctor type parameter:

pub fn dimap<'a, Brand: Profunctor, ...>(...) { ... }
pub fn first<'a, Brand: Strong, ...>(...) { ... }
pub fn closed<'a, Brand: Closed<FunctionBrand>, FunctionBrand: LiftFn, ...>(...) { ... }

This is consistent within the profunctor module. However, the optics code uses P for profunctor parameters (e.g., Optic::evaluate<P: Profunctor>). This is a minor naming difference between the two modules -Brand in profunctor classes vs P in optic traits -but both are single-concept identifiers and the bounds disambiguate.

§5.3 Type parameter conventions summary

ConceptIn profunctor classesIn optic traits/structsIn optic free functions
Profunctor brandSelf (trait) / Brand (free fn)P (method-level)P
Pointer brand-PointerBrandPointerBrand
Function brandFunctionBrand (Closed only)FunctionBrandFunctionBrand
Focus typesA, BA, BA, B
Structure types-S, TS, T
Auxiliary typesC (passthrough), X (input)R (result/monoid)R

§6. Summary

§What matches PureScript

  • All 7 profunctor classes present with correct superclass hierarchy
  • All method names identical (except lcmap ->lmap, which matches Haskell convention)
  • Type parameter ordering within methods matches PureScript
  • lmap/rmap default implementations match PureScript’s free function definitions

§Rust-specific adaptations (justified)

  • Closed<FunctionBrand> parameterization (Rust needs explicit function wrapping)
  • Result<C, A> instead of Either a c (Rust lacks a standard Either)
  • TFunc: TraversalFunc instead of rank-2 types in Wander
  • X: Clone on Closed::closed (Rust needs explicit cloning in nested closures)
  • A: Clone on fan_out (same justification -Rust needs explicit cloning to duplicate input)
  • arrow is a free function with Category + Profunctor bounds (no Arrow trait)
  • lmap/rmap as trait methods instead of free functions

§Naming inconsistencies to consider

  1. Brand in profunctor free functions vs P in optic traits -different naming for profunctor type parameter (minor, both are clear from context)