4

I need to implement a transformation from one data structure to another:

A[B] => C[D]

I could just implement it as a method:

def transform(in: A[B]): C[D] = ???

But I would like to do it in a type-driven development way, the code being scalable, loosely-coupled, and ready to be extended for new absurd requirements from business. So here's what I got:

type AB = A[B]
type CD = C[D]
trait Transformer[I,O] {
  def transform(in:I): O
}
implicit val abcdTransformer: Transformer[AB,CD] = 
  (in: AB) => ???

def transform[I,O](in: I)(implicit t: Transformer[I,O]): O = t.transform(in)

Not sure what exactly I'm gaining from it, and feels like an overkill. Is it really a good way to implement this transformation? Am I missing out on some library (cats) that already provides such boilerplate and more?

Vasily802
  • 1,703
  • 2
  • 18
  • 35
  • 2
    What you have written is a standard typeclass which is a good way to go. But this is not using parameterised types so it doesn't seem to be answering your question. What is the relevance of the fact that these are parameterised types? – Tim May 07 '20 at 06:54
  • 1
    I only just found twitter-bijection yesterday. not sure it's exactly what you are looking for, but worth having a look at it either way i think. – Saskia May 07 '20 at 08:36
  • @Tim no relevance really, in reality it's `ConsumerRecord[_, SpecificRecord]` (kafka) and `Seq[WriteMessage[SolrInputDocument, NotUsed]]` (alpakka-solr) I guess my question was more about library >.<. Sorry – Vasily802 May 07 '20 at 19:45

2 Answers2

3

When there is single instance of a type class, there is not much difference between approaches based on type class and method.

With a type class you can define that you work with diffeferent types differently (type classes are type-level, compile-time "pattern-matching")

trait Transformer[I,O] {
  def transform(in:I): O
}
object Transformer {
  implicit val abcdTransformer: Transformer[AB,CD] = (in: AB) => ???
  implicit val efghTransformer: Transformer[EF,GH] = (in: EF) => ???
}

If your types "intersect", with a type class you can prioritize instances

trait Transformer[I,O] {
  def transform(in:I): O
}
trait LowPriorityTransformer {
  implicit val efghTransformer: Transformer[EF,GH] = (in: EF) => ???
}
object Transformer extends LowPriorityTransformer {
  implicit val abcdTransformer: Transformer[AB,CD] = (in: AB) => ???
}

With a type class you can define your logic inductively

trait Transformer[I,O] {
  def transform(in:I): O
}
object Transformer {  
  implicit def recurse(implicit t: Transformer[...]): Transformer[...] = ???
  implicit val base: Transformer[...] = ???
}

With a type class you can perform type-level calculations

trait Transformer[I] {
  type O
  def transform(in:I): O
}
object Transformer {
  implicit val abcdTransformer: Transformer[AB] { type O = CD } = ???
  implicit val efghTransformer: Transformer[EF] { type O = GH } = ???
}

def transform[I](in: I)(implicit t: Transformer[I]): t.O = t.transform(in)

Here are examples where replacing method with a type class makes the job

shapeless filter a list of options

How to overload generic method with different evidence without ambiguity?

When using HList with GADTs I am having to cast using asInstanceOf[H]. Is there a way to avoid the cast?

Also with a type class you can hide several implicit parameter in a single one encapsulating your logic in a type class

How to wrap a method having implicits with another method in Scala?

Implicit Encoder for TypedDataset and Type Bounds in Scala

Parameterized folding on a shapeless HList

Regarding hiding boilerplate, some boilerplate will be hidden in Dotty (Scala 3). There will not be much need in

def transform[I,O](in: I)(implicit t: Transformer[I,O]): O = t.transform(in) // (*)

any more. We can directly define type classes with extension methods

trait Transformer[I,O] {
  def (in:I) transform: O
}
object Transformer {
  given as Transformer[AB,CD] = (in: AB) => ??? // given is instead of implicit
}

import Transformer.{ given _}
ab.transform

In Scala 2 I have small library AUXify (not production-ready) to generate boilerplate like (*)

import com.github.dmytromitin.auxify.macros.delegated

@delegated
trait Transformer[I,O] {
  def transform(in:I): O
}
object Transformer {
  implicit val abcdTransformer: Transformer[AB,CD] = (in: AB) => ???
}

Transformer.transform(ab)

      // scalacOptions += "-Ymacro-debug-lite"
//Warning:scalac: {
//  abstract trait Transformer[I, O] extends scala.AnyRef {
//    def transform(in: I): O
//  };
//  object Transformer extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    def transform[I, O](in: I)(implicit inst$macro$1: Transformer[I, O]): O = inst$macro$1.transform(in);
//    implicit val abcdTransformer: Transformer[AB, CD] = ((in: AB) => $qmark$qmark$qmark)
//  };
//  ()
//}

or to generate extension methods (syntax)

import com.github.dmytromitin.auxify.macros.syntax

@syntax
trait Transformer[I,O] {
  def transform(in:I): O
}
object Transformer {
  implicit val abcdTransformer: Transformer[AB,CD] = (in: AB) => ???
}

import Transformer.syntax._
ab.transform[CD]

//Warning:scalac: {
//  abstract trait Transformer[I, O] extends scala.AnyRef {
//    def transform(in: I): O
//  };
//  object Transformer extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    object syntax extends scala.AnyRef {
//      def <init>() = {
//        super.<init>();
//        ()
//      };
//      implicit class Ops$macro$1[I] extends scala.AnyRef {
//        <paramaccessor> val in: I = _;
//        def <init>(in: I) = {
//          super.<init>();
//          ()
//        };
//        def transform[O]()(implicit inst$macro$2: Transformer[I, O]): O = inst$macro$2.transform(in)
//      }
//    };
//    implicit val abcdTransformer: Transformer[AB, CD] = ((in: AB) => $qmark$qmark$qmark)
//  };
//  ()
//}

or to generate materializer etc.

import com.github.dmytromitin.auxify.macros.apply

@apply
trait Transformer[I, O] {
  def transform(in:I): O
}
object Transformer {
  implicit val abcdTransformer: Transformer[AB, CD] = ???
}

Transformer[AB, CD].transform(ab)

//Warning:scalac: {
//  abstract trait Transformer[I, O] extends scala.AnyRef {
//    def transform(in: I): O
//  };
//  object Transformer extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    def apply[I, O](implicit inst: Transformer[I, O]): Transformer[I, O] = inst;
//    implicit val abcdTransformer: Transformer[AB, CD] = $qmark$qmark$qmark
//  };
//  ()
//}

Also extension methods (and materializer) for single-parameter type classes can be generated with Simulacrum

import simulacrum.typeclass

@typeclass
trait Transformer[I] {
  type O
  def transform(in:I): O
}
object Transformer {
  implicit val abcdTransformer: Transformer[AB] { type O = CD } = ???
}

Transformer[AB].transform(ab)

import Transformer.ops._
ab.transform

//Warning:scalac: {
//  @new _root_.scala.annotation.implicitNotFound("Could not find an instance of Transformer for ${I}") abstract trait Transformer[I] extends _root_.scala.Any with _root_.scala.Serializable {
//    type O;
//    def transform(in: I): O
//  };
//  object Transformer extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    implicit val abcdTransformer: Transformer[AB] {
//      type O = CD
//    } = $qmark$qmark$qmark;
//    @new scala.inline() def apply[I](implicit instance: Transformer[I]): Transformer[I] {
//      type O = instance.O
//    } = instance;
//    abstract trait Ops[I] extends scala.AnyRef {
//      def $init$() = {
//        ()
//      };
//      type TypeClassType <: Transformer[I];
//      val typeClassInstance: TypeClassType;
//      import typeClassInstance._;
//      def self: I;
//      def transform: O = typeClassInstance.transform(self)
//    };
//    abstract trait ToTransformerOps extends scala.AnyRef {
//      def $init$() = {
//        ()
//      };
//      @new java.lang.SuppressWarnings(scala.Array("org.wartremover.warts.ExplicitImplicitTypes", "org.wartremover.warts.ImplicitConversion")) implicit def toTransformerOps[I](target: I)(implicit tc: Transformer[I]): Ops[I] {
//        type TypeClassType = Transformer[I] {
//          type O = tc.O
//        }
//      } = {
//        final class $anon extends Ops[I] {
//          def <init>() = {
//            super.<init>();
//            ()
//          };
//          type TypeClassType = Transformer[I] {
//            type O = tc.O
//          };
//          val self = target;
//          val typeClassInstance: TypeClassType = tc
//        };
//        new $anon()
//      }
//    };
//    object nonInheritedOps extends ToTransformerOps {
//      def <init>() = {
//        super.<init>();
//        ()
//      }
//    };
//    abstract trait AllOps[I] extends Ops[I] {
//      type TypeClassType <: Transformer[I];
//      val typeClassInstance: TypeClassType
//    };
//    object ops extends scala.AnyRef {
//      def <init>() = {
//        super.<init>();
//        ()
//      };
//      @new java.lang.SuppressWarnings(scala.Array("org.wartremover.warts.ExplicitImplicitTypes", "org.wartremover.warts.ImplicitConversion")) implicit def toAllTransformerOps[I](target: I)(implicit tc: Transformer[I]): AllOps[I] {
//        type TypeClassType = Transformer[I] {
//          type O = tc.O
//        }
//      } = {
//        final class $anon extends AllOps[I] {
//          def <init>() = {
//            super.<init>();
//            ()
//          };
//          type TypeClassType = Transformer[I] {
//            type O = tc.O
//          };
//          val self = target;
//          val typeClassInstance: TypeClassType = tc
//        };
//        new $anon()
//      }
//    }
//  };
//  ()
//}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
1

The Standard Library (2.13.x) comes pretty close to what you need, depending on what, exactly, you need.

import scala.collection.Factory

implicit class AB2CD[A,B](from :IterableOnce[A]) {
  def transit[CC[_]](f :A => B
                   )(implicit fctry: Factory[B, CC[B]]
                   ) :CC[B] = {
    val bs = LazyList.unfold(from.iterator) { itr =>
               Option.when(itr.hasNext) (f(itr.next()), itr)
             }
    fctry.fromSpecific(bs)
  }
}

testing:

Option(88).transit[Vector](_.toString)//res0: Vector[String] = Vector(88)
Seq('c','x').transit[Set](_.asDigit)  //res1: Set[Int] = Set(12, 33)
List(1.1,2.2).transit[Array](_ < 2)   //res2: Array[Boolean] = Array(true, false)

Due to the limitations of IterableOnce, this won't transit from Array and it won't transit to or from String. There's a workaround for that, but I wanted to go only so far with it.

jwvh
  • 50,871
  • 7
  • 38
  • 64