Eliminating identity wrapper types from Scala APIs

Viewed 130

Suppose I am trying to "abstract over execution":

import scala.language.higherKinds

class Operator[W[_]]( f : Int => W[Int] ) {
  def operate( i : Int ) : W[Int] = f(i)
}

Now I can define an Operator[Future] or Operator[Task] etc. For example...

import scala.concurrent.{ExecutionContext,Future}
def futureSquared( i : Int ) = Future( i * i )( ExecutionContext.global )

In REPL-style...

scala> val fop = new Operator( futureSquared )
fop: Operator[scala.concurrent.Future] = Operator@105c54cb

scala> fop.operate(4)
res0: scala.concurrent.Future[Int] = Future(<not completed>)

scala> res0
res1: scala.concurrent.Future[Int] = Future(Success(16))

Hooray!

But I also might want a straightforward synchronous version, so I define somewhere

type Identity[T] = T

And I can define a synchronous operator...

scala> def square( i : Int ) : Identity[Int] = i * i
square: (i: Int)Identity[Int]

scala> val sop = new Operator( square )
sop: Operator[Identity] = Operator@18f2960b

scala> sop.operate(9)
res2: Identity[Int] = 81

Sweet.

But, it's awkward that the inferred type of the result is Identity[Int], rather than the simpler, straightforward Int. Of course the two types are really the same, and so are identical in every way. But I'd like clients of my library who don't know anything about this abstracting-over-execution stuff not to be confused.

I could write a wrapper by hand...

class SimpleOperator( inner : Operator[Identity] ) extends Operator[Identity]( inner.operate ) {
  override def operate( i : Int ) : Int = super.operate(i)
}

which does work...

scala> val simple = new SimpleOperator( sop )
simple: SimpleOperator = SimpleOperator@345c744e

scala> simple.operate(7)
res3: Int = 49

But this feels very boiler-platey, especially if my abstracted-over-execution class has lots of methods rather than just one. And I'd have to remember to keep the wrapper in sync as the generic class evolves.

Is there some more generic, maintainable way to get a version of Operator[Identity] that makes the containing type disappear from the type inference and API docs?

3 Answers

This more of long comment rather than an answer...

But, it's awkward that the inferred type of the result is Identity[Int], rather than the simpler, straightforward Int. Of course the two types apparent types are really the same, and so are identical in every way. But I'd like clients of my library who don't know anything about this abstracting-over-execution stuff not to be confused.

This sounds like you want to convert Indentity[T] back to T... Have you considered type ascription?

scala>def f[T](t: T): Identity[T] = t

scala>f(3)
// res11: Identity[Int] = 3

scala>f(3): Int
// res12: Int = 3

// So in your case
scala>sop.operate(9): Int
// res18: Int = 81

As Steve Waldman suggested in comments given type Identity[T] = T, the types T and Identity[T] really are identical without any ceremony, substitutable and transparent at call sites or anywhere else. For example, following works fine out-of-the-box

sop.operate(9) // res2: cats.Id[Int] = 81
def foo(i: Int) = i
foo(sop.operate(9)) // res3: Int = 81

extract from Cats is the dual of pure and extracts the value from its context, so perhaps we could provide similar methods for users not familiar with the above equivalence (like myself if you see my previous edit).

Can be done by providing types explicitly, but still looks magical for external users investigating method signature.

type Identity[T] = T

def square( i : Int ):Int = i * i

class Operator[W[_], T <: W[Int] ]( f : Int => T ) {
  def operate(i : Int):T = f(i)
}

val op = new Operator[Identity,Int](square)

op.operate(5)
//res0: Int = 25

Works for new Operator[Future,Future[Int]] as well.

Related