Skip to content

Commit

Permalink
Merge 9f22c6d into 1a388f2
Browse files Browse the repository at this point in the history
  • Loading branch information
ryan-williams committed Nov 17, 2017
2 parents 1a388f2 + 9f22c6d commit 633a222
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 21 deletions.
79 changes: 72 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ shapeless-style type-classes for structural manipulation of algebraic data types

[![Build Status](https://travis-ci.org/hammerlab/shapeless-utils.svg?branch=master)](https://travis-ci.org/hammerlab/shapeless-utils)
[![Coverage Status](https://coveralls.io/repos/github/hammerlab/shapeless-utils/badge.svg?branch=master)](https://coveralls.io/github/hammerlab/shapeless-utils?branch=master)
[![Maven Central](https://img.shields.io/maven-central/v/org.hammerlab/shapeless-utils_2.11.svg?maxAge=600)](http://search.maven.org/#search%7Cga%7C1%7Corg.hammerlab%20shapeless-utils)
[![org.hammerlab:shapeless-utils_2.1[12] on Maven Central](https://img.shields.io/maven-central/v/org.hammerlab/shapeless-utils_2.11.svg?maxAge=600&label=org.hammerlab:shapeless-utils_2[12])](http://search.maven.org/#search%7Cga%7C1%7Corg.hammerlab%20shapeless-utils)

- `Find`: recursively find fields by type and/or name
- [`hlist.Find`](src/main/scala/org/hammerlab/shapeless/hlist/Find.scala): recursively find field by type
- [`record.Find`](src/main/scala/org/hammerlab/shapeless/record/Find.scala): recursively find field by name
- [`record.Field`](src/main/scala/org/hammerlab/shapeless/record/Field.scala): recursively find field by name and type
- [`Flatten`](src/main/scala/org/hammerlab/shapeless/hlist/Flatten.scala): recursively flatten an `HList` or `case class` into an `HList`
- [`Select`](src/main/scala/org/hammerlab/shapeless/hlist/Select.scala): covariant version of [`shapeless.ops.hlists.Selector`](https://github.com/milessabin/shapeless/blob/shapeless-2.3.2/core/src/main/scala/shapeless/ops/hlists.scala#L842-L865)
- [`Cast`](src/main/scala/org/hammerlab/shapeless/coproduct/Cast.scala): evidence that a product – or all branches of a coproduct – matches a given HList structure
- [`Singleton`](src/main/scala/org/hammerlab/shapeless/coproduct/Singleton.scala): above when the HList contains one element

## Examples

Expand All @@ -35,12 +37,12 @@ val f = F(e)
Import syntax:

```scala
import org.hammerlab.shapeless._
import hammerlab.shapeless._
```

### `findt`: find field by type
### `findt`

Adapted from [hlist.FindTest](src/test/scala/org/hammerlab/shapeless/hlist/FindTest.scala):
Find field by type

```scala
a.findt[Int] // 123
Expand All @@ -61,9 +63,11 @@ e.findt[A] // doesn't compile: E.a, E.a2, and E.c.a both match
e.findt[Int] // doesn't compile: E.a.n, E.a2.n, and E.c.a.n both match
```

### `find`: find field by name
(adapted from [hlist.FindTest](src/test/scala/org/hammerlab/shapeless/hlist/FindTest.scala))

Adapted from [record.FindTest](src/test/scala/org/hammerlab/shapeless/record/FindTest.scala):
### `find`

Find field by name:

```scala
a.find('n) // 123
Expand All @@ -85,9 +89,70 @@ e.find('a) // doesn't compile: E.c.a and E.a both match
e.find('n) // doesn't compile: E.a.n, E.a2.n, and E.c.a.n both match
```

### `field`: find field by name and type
(adapted from [record.FindTest](src/test/scala/org/hammerlab/shapeless/record/FindTest.scala))

### `field`

Find field by name and type:

```scala
e.field[Boolean]('b) // true
e.field[B]('b) // B("abc")
```

### `Singleton` / `.map`

Manipulate a generic hierarchy that always wraps one element of a given type.

Example hierarchy:

```scala
sealed trait Foo

case class A(n: Int) extends Foo

sealed trait Bar extends Foo
case class B(n: Int) extends Bar
case class C(n: Int) extends Bar
case class D(n: Int) extends Bar
```

Transform the enclosed `Int`, preserving concrete type:

```scala
implicit val singleton = Singleton[Foo, Int]

val foos = Seq[Foo](A(1), B(2), C(3), D(4))

foos.map(_.map(_ * 10))
// Seq(A(10), B(20), C(30), D(40))
```

#### `Cast`

Generalization of the above, providing evidence that a type is structured like a given `HList`.

Given a hierarchy with all leafs matching `Int :: String :: HNil`:

```scala
sealed trait X
case class Y(n: Int, s: String) extends X
case class Z(n: Int, s: String) extends X

val y = Y(111, "abc")
val z = Z(222, "def")
val xs = Seq[X](y, z)
```

Expose a `map` operation that transforms the HList representation, preserving the container type:

```scala
import shapeless._
xs map {
_ map {
case n :: s ::
2*n :: s.reverse ::
}
}
// Seq(Y(222, cba), Z(444, fed))
```
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name := "shapeless-utils"
version := "1.0.1"
version := "1.1.0-SNAPSHOT"
deps += shapeless
addScala212
5 changes: 5 additions & 0 deletions src/main/scala/hammerlab/shapeless.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package hammerlab

import org.hammerlab.shapeless.all

object shapeless extends all
18 changes: 18 additions & 0 deletions src/main/scala/org/hammerlab/shapeless/all.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.hammerlab.shapeless

import org.hammerlab.shapeless.coproduct.{ cast, singleton }
import org.hammerlab.shapeless.hlist.{ HasFlattenedOps, HasSelectOps }
import org.hammerlab.shapeless.record.HasFieldOps
import shapeless.HNil

trait all
extends record.HasFindOps
with HasFieldOps
with hlist.HasFindOps
with HasFlattenedOps
with HasSelectOps
with cast
with singleton {
val = HNil
type = HNil
}
20 changes: 20 additions & 0 deletions src/main/scala/org/hammerlab/shapeless/coproduct/Iso.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.hammerlab.shapeless.coproduct

import shapeless.Generic

abstract class Iso[From, To](f: From, val t: To) {
def from(to: To): From
def apply(fn: To To): From =
from(
fn(
t
)
)
}

object Iso {
implicit def apply[T, L](t: T)(implicit gen: Generic.Aux[T, L]): Iso[T, L] =
new Iso[T, L](t, gen.to(t)) {
override def from(to: L) = gen.from(to)
}
}
79 changes: 79 additions & 0 deletions src/main/scala/org/hammerlab/shapeless/coproduct/cast.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.hammerlab.shapeless.coproduct;

import shapeless._

trait cast {
/**
* Type-class providing evidence that type [[T]] is structurally equivalent to an [[HList]]-type `L`
*
* Additionally provides a round-trip into and out of `L` for a given instance of [[T]]
*/
trait Cast[T] {
type L <: HList
def iso(t: T): Iso[T, L]
def apply(t: T)(fn: L L): T = iso(t)(fn)
}

object Cast {

type Aux[T, L0 <: HList] = Cast[T] { type L = L0 }

implicit def cnil[L0 <: HList]: Aux[CNil, L0] =
new Cast[CNil] {
type L = L0
override def iso(t: CNil) = ???
}

implicit def coproduct[H, T <: Coproduct, L0 <: HList](implicit
genH: Generic.Aux[H, L0],
lt: Aux[T, L0]): Aux[H :+: T, L0] =
new Cast[H :+: T] {
type L = L0
override def iso(t: H :+: T) =
t match {
case Inl(h)
new Iso[H :+: T, L](
Inl(h),
genH.to(h)
) {
override def from(to: L) = Inl(genH.from(to))
}
case Inr(t)
val iso = lt.iso(t)
new Iso[H :+: T, L](
Inr(t),
iso.t
) {
override def from(to: L) = Inr(iso.from(to))
}
}
}

implicit def sealedTrait[T, C <: Coproduct, L0 <: HList](implicit
genT: Generic.Aux[T, C],
l: Aux[C, L0]): Aux[T, L0] =
new Cast[T] {
type L = L0
override def iso(t: T) = {
val c = genT.to(t)
val iso = l.iso(c)
new Iso[T, L](t, iso.t) {
override def from(to: L) = genT.from(iso.from(to))
}
}
}

implicit def caseClass[T, L0 <: HList](implicit genT: Generic.Aux[T, L0]): Aux[T, L0] =
new Cast[T] {
type L = L0
override def iso(t: T) = Iso[T, L0](t)
}

}

implicit class CastOps[T, L <: HList](t: T)(implicit c: Cast.Aux[T, L]) {
def map(fn: L L): T = c(t)(fn)
}
}

object cast extends cast with Serializable
30 changes: 30 additions & 0 deletions src/main/scala/org/hammerlab/shapeless/coproduct/singleton.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.hammerlab.shapeless.coproduct

import cast._
import shapeless._

trait singleton {
/**
* Type-class representing evidence that a type [[T]] wraps a single value of type [[V]].
*
* [[T]] may be a sealed hierarchy as long as all concrete implementations are case-classes with a single [[V]]
* parameter.
*/
trait Singleton[T, V] {
def apply(t: T)(fn: V V): T
}

object Singleton {
def apply[T, V](implicit l: Cast.Aux[T, V :: HNil]): Singleton[T, V] = fromLooksLike[T, V]

implicit def fromLooksLike[T, V](implicit l: Cast.Aux[T, V :: HNil]): Singleton[T, V] =
new Singleton[T, V] {
override def apply(t: T)(fn: V V) =
l(t)(l fn(l.head) :: HNil)
}
}

implicit class SingletonOps[T, V](t: T)(implicit e: Singleton[T, V]) {
def map(fn: V V) = e(t)(fn)
}
}
14 changes: 1 addition & 13 deletions src/main/scala/org/hammerlab/shapeless/package.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
package org.hammerlab

import _root_.shapeless.HNil
import org.hammerlab.shapeless.hlist.{ HasFlattenedOps, HasSelectOps }
import org.hammerlab.shapeless.record.HasFieldOps

package object shapeless
extends record.HasFindOps
with HasFieldOps
with hlist.HasFindOps
with HasFlattenedOps
with HasSelectOps {
val = HNil
type = HNil
}
package object shapeless extends all
43 changes: 43 additions & 0 deletions src/test/scala/org/hammerlab/shapeless/coproduct/CastTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.hammerlab.shapeless.coproduct

import org.hammerlab.test.Suite

object CastTest {
sealed trait X
case class Y(n: Int, s: String) extends X
case class Z(n: Int, s: String) extends X
}

class CastTest
extends Suite {

import CastTest._
import hammerlab.shapeless._
import shapeless._

test("apply") {
val y = Y(111, "abc")
val z = Z(222, "def")

val xs = Seq[X](y, z)

y map {
case n :: s ::
2*n :: s.reverse ::
} should be(
Y(222, "cba")
)

xs map {
_ map {
case n :: s ::
10*n :: s*2 ::
}
} should be(
Seq(
Y(1110, "abcabc"),
Z(2220, "defdef")
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.hammerlab.shapeless.coproduct

import hammerlab.shapeless._
import org.hammerlab.test.Suite

object SingletonTest {

/**
* Sample sealed hierarchy where all implementations are [[Int]] [[Singleton]]s
*/
sealed trait Foo

case class A(n: Int) extends Foo

sealed trait Bar extends Foo
case class B(n: Int) extends Bar
case class C(n: Int) extends Bar
case class D(n: Int) extends Bar

/**
* Hang a multiplication ([[*]]) operator off any [[Int]]-[[Singleton]], in this case [[Bar]] and its subclasses.
*/
implicit class Ops[U](val t: U) extends AnyVal {
def *(n: Int)(implicit e: Singleton[U, Int]): U = t map(_ * n)
}
}

class SingletonTest
extends Suite {

import SingletonTest._

test("coproduct") {
// mapping preserves type
val a: A = A(2) * 10

A(2) * 10 should be(A(20))
B(2) * 10 should be(B(20))
C(2) * 10 should be(C(20))
D(2) * 10 should be(D(20))

val foo: Foo = A(2)
foo * (10) should be(A(20))

val bar: Bar = B(2)
bar * (10) should be(B(20))

// test summoning
val _: Singleton[Foo, Int] = Singleton[Foo, Int]
}
}

0 comments on commit 633a222

Please sign in to comment.