Skip to content

Instantly share code, notes, and snippets.

@non
Last active April 9, 2020 00:26
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save non/51b83d0abc929cc4f0b153accf2bf02f to your computer and use it in GitHub Desktop.
Save non/51b83d0abc929cc4f0b153accf2bf02f to your computer and use it in GitHub Desktop.
Interesting trick to lift existentials into values, so we can later refer to them.
package demo
import cats.{Applicative, Functor, Id, Semigroupal, Traverse}
import cats.arrow.FunctionK
/**
* Type-lifting operation to replace the wildcard type (i.e. _).
*
* In some cases we end up with code like: List[Option[_]]. This is
* fine unless you later need to write code in terms of a particular
* Option's type paramter (i.e. the underscore).
*
* With this code, you'd instead write: List[Wild[Option]]. Then, you
* could say something like:
*
* val ws: List[Wild[Option]] =
* List(Wild(Option(3)), Wild(Option("hi")), Wild(None))
* val w: Wild[Option] = xs.head
* val o: Option[w.Type] = w.value
* val e: w.Type = o.getOrElse(sys.error("!!!"))
*
* and you can now refer to the specific type of `e`. This is useful
* when you may need to produce aligned values or have a "stable" way
* of referring to an unknown type.
*/
sealed trait Wild[F[_]] extends Serializable {
/**
* This is the stable type used in place of an underscore.
*
* It relates a type to the F[Type] value we are holding.
*/
type Type
/**
* This is the F[_] value we are holding.
*/
def value: F[Type]
/**
* Map Wild[F] into Wild[G] while preserving Type.
*
* For example:
*
* val w0: Wild[List] = ...
* val f: FunctionK[List, Option] =
* new FunctionK[List, Option] {
* def apply[A](as: List[A]): Option[A] = as.headOption
* }
* val w1: Wild[Option] = w0.mapK(f)
*/
def mapK[G[_]](f: FunctionK[F, G]): Wild.Aux[G, Type] =
Wild.typed(f(value))
/**
* Combine F[Type] and F[w.Type] into F[(Type, w.Type)] using an
* implicit Semigroupal[F].
*
* For example:
*
* import cats.implicits._
*
* val w0: Wild[Option] = Wild(Some(8))
* val w1: Wild[Option] = Wild(Some(true))
* val w2: Wild[Option] = (w0 product w1)
*
* println(w2) // Wild(Some((8, true)))
*/
def product(w: Wild[F])(implicit ev: Semigroupal[F]): Wild.Aux[F, (Type, w.Type)] =
Wild.typed(ev.product(value, w.value))
/**
* Proxy `toString` to the underlying `value`.
*/
override def toString: String =
s"Wild($value)"
/**
* Proxy `equals` to the underlying `value`.
*/
override def equals(that: Any): Boolean =
that match {
case w: Wild[_] => value == w.value
case _ => false
}
/**
* Proxy `hashCode` to the underlying `value`.
*/
override def hashCode: Int =
value.hashCode
}
object Wild {
/**
* When needed, we can use Wild.Aux[F, A] to refer to a Wild[F]
* parameterized on a specific, known Type (i.e. A).
*/
type Aux[F[_], A] = Wild[F] { type Type = A }
/**
* Construct a Wild[F] from an F[A] value.
*
* At the point of construction, the Type (A) is known. We expect to
* later "forget" this type by combining various Wild[F] values
* whose Type members are not known.
*
* Even though A is known, this method returns Wild[F] (i.e. it
* erases the A type). If you want the A type "remembered" you can
* use `typed` to get a `Wild.Aux[F, A]` back.
*/
def apply[F[_], A](fa: F[A]): Wild[F] =
typed(fa)
/**
* Construct a Wild.Aux[F, A] from an F[A] value.
*
* This method is similar to `apply` but it retains the A type in
* its return value. Sometimes this is the desired behavior but
* other times it will cause unification problems.
*/
def typed[F[_], A](fa: F[A]): Wild.Aux[F, A] =
new Wild[F] {
type Type = A
def value: F[A] = fa
}
/**
* Support pattern-matching on Wild as if it was a case class.
*
* For example:
*
* val w: Wild[Option] = Wild(Some(999))
* val Wild(o) = w
*
* This is a total pattern-match (it will never fail).
*/
def unapply[F[_]](w: Wild[F]): Some[F[w.Type]] =
Some(w.value)
/**
* If Wild is used with a pure `A` value, we can use `cats.Id` as
* our `F[_]` type. The `Identity` alias (and friends) help make
* this a bit more ergonomic.
*/
type Identity = Wild[Id]
/**
* Construct an Identity value (Wild[Id]) from a given `a` value.
*/
def identity[A](a: A): Identity = Wild[Id, A](a)
/**
* If `F` has a `Functor`, we can always push our unknown type
* "through" a `Wild[F]` by wrapping our inner values in `Identity`.
*
* To see this, we can represent a List[Int] as either:
*
* val before: Wild[List] = Wild(List(1, 2, 3))
* val after: List[Identity] = Wild.lower(before)
* // i.e. List(Wild.identity(1), Wild.identity(2), Wild.identity(3))
*
* However, we cannot go the other direction, since there is no one
* type that can necessarily represents all the disparate values in
* List[Identity].
*
* val before: List[Identity] = List(Wild(1), Wild("two"))
* val after: Wild[List] = <impossible>
*/
def lower[F[_]](w: Wild[F])(implicit ev: Functor[F]): F[Identity] =
ev.map(w.value)(Wild.identity(_))
/**
* You can imagine Wild.sequence as doing two things:
*
* (1) Pushing the "wildness" down (as per Wild.lower) to turn `F[Wild[G]]` into `F[G[Identity]]`
* (2) Turning `F[G[A]]` into `G[F[A]]` as per `sequence` provided by `cats.Traverse[F]`.
*
* This is equivalent to a single `traverse` as shown below.
*/
def sequence[F[_], G[_]](fwg: F[Wild[G]])(implicit fev: Traverse[F],
gev: Applicative[G]): G[F[Identity]] =
fev.traverse(fwg) { (w: Wild[G]) =>
lower(w)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment