This is a small proof of concept that demonstrates how to implement a Scala macro that allows us to "forget" all of a value's methods and only use enrichment methods (which will usually be provided via a type class).
The idea behind abstracted
was originally (as far as I know) suggested by
Michael Pilquist in the
cats room on
Gitter, although the
approach he suggests is different from the one I've used here.
As an example, suppose we've got a Box
type:
case class Box[A](val a: A) {
def map[B](f: A => B): Box[B] = {
println("Box's map")
Box(f(a))
}
def flatMap[B](f: A => Box[B]): Box[B] = {
println("Box's flatMap")
f(a)
}
}
And a monad instance for it:
import cats.Monad
implicit val boxMonad: Monad[Box] = new Monad[Box] {
override def map[A, B](fa: Box[A])(f: A => B): Box[B] = {
println("Box's functor's map")
Box(f(fa.a))
}
def flatMap[A, B](fa: Box[A])(f: A => Box[B]): Box[B] = {
println("Box's monad's flatMap")
f(fa.a)
}
def pure[A](a: A): Box[A] = Box(a)
}
Now if we use Box
in a for
-comprehension, for example, the monad instance
won't get used:
scala> import io.travisbrown.abstracted.demo._
import io.travisbrown.abstracted.demo._
scala> import cats.syntax.all._
import cats.syntax.all._
scala> for { foo <- Box("foo"); howMany <- Box(3) } yield foo * howMany
Box's flatMap
Box's map
res0: io.travisbrown.abstracted.demo.Box[String] = Box(foofoofoo)
Our abstracted
macro allows us to change this:
scala> import io.travisbrown.abstracted._
import io.travisbrown.abstracted._
scala> for { foo <- Box("foo").abstracted; howMany <- Box(3) } yield foo * howMany
Box's monad's flatMap
Box's map
res1: io.travisbrown.abstracted.demo.Box[String] = Box(foofoofoo)
I decided to take a stab at implementing abstracted
tonight because of
a conversation about how Finagle services compose
this afternoon. Finagle services are morally more or less Kleisli arrows over
Twitter futures, but for whatever reason Service
extends I => Future[O]
,
which means that they have totally useless compose
and andThen
methods. In
another project I provide category
and profunctor instances for Service
, but the compose
and andThen
enrichment methods provided by cats for things with Compose
instances are
blocked by the stupid methods that Service
inherits from Function1
.
For example, if we've got these services:
import cats.syntax.compose._
import com.twitter.util.Future
import com.twitter.finagle.Service
import io.catbird.finagle._
val is = Service.mk[Int, String](i => Future.value(i.toString))
val si = Service.mk[String, Int](s => Future(s.toInt))
We get an error when we try to compose them:
scala> val ss = si andThen is
<console>:22: error: type mismatch;
found : com.twitter.finagle.Service[Int,String]
required: com.twitter.util.Future[Int] => ?
si andThen is
^
Our abstracted
macro fixes this problem:
scala> import io.travisbrown.abstracted._
import io.travisbrown.abstracted._
scala> val ss = si.abstracted andThen is
ss: com.twitter.finagle.Service[String,String] = <function1>
The implementation is pretty straightforward. First we've got an implicit class
that provides a def abstracted: Empty[A]
method for any A
, where our Empty
type is a case class that wraps an A
and provides access to the wrapped value,
but doesn't have any other methods.
We also have a Converter[A, B]
type that represents a conversion from
Empty[A]
to B
(I ran into problems trying to use Empty[A] => B
directly),
and an implicit method that will apply the conversion automatically to any
Empty[A]
for any appropriately-typed Converter
instance.
The interesting part is how we make Converter
instances. We use the Scala
macro system's fundep materialization, which allows us to determine in the body of the macro what the output
type of the Converter
will be. We look at the open implicits and find one that
looks like the compiler is fishing for a WhateverOps
enrichment class for our
Empty[A]
. We then ask for a view from A
(our real type) to the target of
that view. We read the return type off the view from A
, and from there the
implementation is pretty trivial.
It seems like it works. The examples above can be run by opening up a REPL with
sbt demo/console
. If other people think it looks useful I guess it could end
up in cats, although there's nothing cats-specific about the macro itself or the
surrounding machinery.
abstracted is licensed under the Apache License, Version 2.0 (the "License"); you may not use this software except in compliance with the License.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.