Skip to content

Conversation

@danicheg
Copy link
Contributor

@danicheg danicheg commented Aug 23, 2024

Motivation

This PR adds four methods Transformer#contramap, Transformer#map, PartialTransformer#contramap and PartialTransformer#map, to make transformers more composable and reusing existing transformers more concise and pleasant.

Examples

  • Transformer#contramap:
val stringTransformer: Transformer[String, Int] = _.length

case class Id(id: String)

implicit val idTransformer: Transformer[Id, Int] =
  stringTransformer.contramap(_.id)
  • PartialTransformer#map:
val stringTransformer: PartialTransformer[String, Int] =
  PartialTransformer.fromFunction(_.length)

case class Length(length: Int)

implicit val toLengthTransformer: PartialTransformer[String, Length] =
  stringTransformer.map(id => Length(id))

Why not use existing capabilities?

  • To emulate Transformer#contramap users might do
val stringTransformer: Transformer[String, Int] = _.length

case class Id(value: String)

implicit val idTransformer: Transformer[Id, Int] =
  id => stringTransformer.transform(id.value)
  • To emulate PartialTransformer#map users might do
val stringTransformer: PartialTransformer[String, Int] =
  PartialTransformer.fromFunction(_.length)

case class Length(length: Int)

implicit val toLengthTransformer: PartialTransformer[String, Length] =
  PartialTransformer.apply(id => stringTransformer.transform(id).map(Length(_)))

Well, the main reason is to be concise and to promote reusing existing transformers (at the user's site), keeping the DRY principle at its max. These combinators are also widely known in functional communities, making their usage transparent and pleasant. OTOH, there is the chimney-cats module, which provides the same functionalities. However, using it would require many redundant allocations (implicit syntaxes come with their costs), which might not be ideal for GCs, especially since Chimney is often used in hot-paths of applications (based on my experience).

I'd appreciate any feedback on that matter!

Copy link
Member

@MateuszKubuszok MateuszKubuszok left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, thanks for another PR!

I think we need a parity of features where possible, so both Transformer and PartialTransformer should have both map and contramap if we're adding them.

Additionally, we would prefer these operations to not live in the main typeclass but more in some dedicated trait:

// docs
trait Transformer[From, To] extends AutoDerived[From, To] {
  // docs
  def transform(src: From): To
}
object Transformer {

  trait AutoDerived[From, To] extends TransformerOps[From, To] {
    def transform(src: From): To
  }

  trait TransformerOps[From, To] {
    def transform(src: From): To

    def map[A](f: To => A): Transformer[From, A] = ...
    def contramap[A](f: A => From): Transformer[A, To] = ...
  }
}

so that they could be called and be present in intellisense but not distract from the main use case of Transformers (and PartialTransformers) when looking in the source code.

*
* @since 1.5.0
*/
final def map[A](f: To => A): PartialTransformer[From, A] = new PartialTransformer[From, A] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is map in partial, there could also be mapPartial[A](f: To => partial.Result[A]): PartialTransformer[From, A].

Since Transformer has contramap the PartialTransformer should also get one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this once again and don't see any issue with adding contramap to PartialTransformer and map to Transformer, respectively.

What about mapPartial, it might be useful indeed, but don't you mind if we would add it separately?

*/
final def contramap[A](f: A => From): Transformer[A, To] = new Transformer[A, To] {
override def transform(src: A): To = self.transform(f(src))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since PartialTransformer has map the Transformer should also get one.

@danicheg danicheg changed the title Add the Transformer#contramap and PartialTransformer#map Add contramap and map ops to the Transformer and PartialTransformer Aug 31, 2024
@MateuszKubuszok
Copy link
Member

If you are still interested in working on this I just merged #753, which should make it more doable on 2.0.0-development branch (summoning work in expected way without an extra type per type-class, no issues with deciding which one should be extended etc). It would be released as 2.0.0-M2 once 2.13.17 is released, so there is no rush.

@MateuszKubuszok MateuszKubuszok changed the base branch from master to 2.0.0-development July 23, 2025 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants