Skip to content

Commit

Permalink
#25, #38 - introduced "high-level" arrows and syntax for them
Browse files Browse the repository at this point in the history
- ScalaExpr: Id, SelectField, FMap, MBind, SelectIn (TODO);
  • Loading branch information
fehu committed Jul 11, 2018
1 parent cecafbd commit 4bc97c1
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 76 deletions.
9 changes: 8 additions & 1 deletion src/main/scala/com/abraxas/slothql/mapper/Arrow.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ object Arrow {


// TODO: `Arrow.Id[Int] == Arrow.Id[String]` complies
trait Id[A] extends Arrow { type Source = A; type Target = A }
trait Id[A] extends Arrow {
type Source = A
type Target = A

override def toString: String = "Id"
}
private object Id extends Id[Any]
def Id[A]: Id[A] = Id.asInstanceOf[Id[A]]

Expand All @@ -33,6 +38,8 @@ object Arrow {
override def equals(o: scala.Any): Boolean = PartialFunction.cond(o) {
case that: Composition[_, _] => this.F == that.F && this.G == that.G
}

override def toString: String = s"$F$G"
}
object Composition {
type Aux[F <: Arrow, G <: Arrow, S, T] = Composition[F, G] { type Source = S; type Target = T }
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/com/abraxas/slothql/mapper/GraphPath.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ sealed trait GraphPath extends Arrow {

object GraphPath {
case class Initial[N <: GraphRepr.Node](node: N)
extends GraphPath { type Source = N; type Target = N }
extends GraphPath { type Source = N; type Target = N } // TODO: extends GraphPath with Arrow.Id[N]
case class InitialUnique[N <: GraphRepr.Node with GraphRepr.Identifiable.Aux[Id], Id](node: N, id: Id)
extends GraphPath { type Source = N; type Target = N }

Expand Down
105 changes: 105 additions & 0 deletions src/main/scala/com/abraxas/slothql/mapper/ScalaExpr.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.abraxas.slothql.mapper

import scala.language.{ dynamics, higherKinds }

import shapeless.tag.@@
import shapeless.{ HList, LabelledGeneric, ops }

import com.abraxas.slothql.mapper.Arrow.Types


sealed trait ScalaExpr extends Arrow with ScalaExpr.FieldSelectionOps

object ScalaExpr {
type Aux[-S, +T] = ScalaExpr { type Source >: S; type Target <: T }

def apply[A]: Id[A] = Id[A]


/** Identity arrow. (self selection) */
sealed trait Id[A] extends ScalaExpr with Arrow.Id[A]
object Id {
def apply[A]: Id[A] = instance.asInstanceOf[Id[A]]
private lazy val instance = new Id[Any]{}
}

/** Expression representing selection of a field of an ADT (case class). */
case class SelectField[Obj, K <: String, V](field: K)
extends ScalaExpr { type Source = Obj; type Target = V }

/** Expression representing functor `map` operation. */
case class FMap[F[_], E <: ScalaExpr](expr: E)(implicit val F: cats.Functor[F])
extends ScalaExpr { type Source = F[expr.Source]; type Target = F[expr.Target] }
object FMap {
def apply[F[_]]: Builder[F] = Builder.asInstanceOf[Builder[F]]

protected class Builder[F[_]] {
def apply[E <: ScalaExpr](expr: E)(implicit F: cats.Functor[F]): FMap[F, E] = FMap[F, E](expr)
}
private object Builder extends Builder
}

/** Expression representing monadic bind / `flatMap` operation. */
case class MBind[F[_], E <: ScalaExpr](expr: E)(implicit val M: cats.Monad[F])
extends ScalaExpr { type Source = F[expr.Source]; type Target = expr.Target }
object MBind {
def apply[F[_]]: Builder[F] = Builder.asInstanceOf[Builder[F]]

protected class Builder[F[_]] {
def apply[E <: ScalaExpr](expr: E)(implicit M: cats.Monad[F]): MBind[F, E] = MBind[F, E](expr)
}
private object Builder extends Builder
}

// TODO
case class SelectIn[F[_], Sel, V](sel: Sel)
extends ScalaExpr { type Source = F[V]; type Target = V }


// // // // // // Syntax Ops // // // // // //


implicit class ScalaExprFMapOps[A <: ScalaExpr, F[_], TA, S0](a: A)(
implicit
targetA: Types.Aux[A, _, TA],
source0: TA <:< F[S0],
functor: cats.Functor[F]
) {

def map[B <: ScalaExpr](b: B)(implicit compose: Arrow.Compose[FMap[F, B], A]): compose.Out = compose(new FMap[F, B](b), a)
def map[B <: ScalaExpr](fb: Id[S0] => B)(implicit compose: Arrow.Compose[FMap[F, B], A]): compose.Out = compose(new FMap[F, B](fb(Id[S0])), a)
}

implicit class ScalaExprMBindOps[A <: ScalaExpr, F[_], TA, S0](a: A)(
implicit
targetA: Types.Aux[A, _, TA],
source0: TA <:< F[S0],
monad: cats.Monad[F]
) {

def flatMap[B <: ScalaExpr.Aux[_, F[_]]](b: B)(implicit compose: Arrow.Compose[MBind[F, B], A]): compose.Out = compose(new MBind[F, B](b), a)
def flatMap[B <: ScalaExpr.Aux[_, F[_]]](fb: Id[S0] => B)(implicit compose: Arrow.Compose[MBind[F, B], A]): compose.Out = compose(new MBind[F, B](fb(Id[S0])), a)
}

protected trait FieldSelectionOps extends Dynamic {
expr: ScalaExpr =>

def selectDynamic(k: String)(implicit ev: Syntax.HasField[Source, Symbol @@ k.type]): SelectField[Source, k.type, ev.Value] = SelectField(k)
}

object Syntax {
/** Evidence that `Obj` has a field of type `V` with name `K` */
@annotation.implicitNotFound(msg = "${Obj} doesn't have field ${K}")
trait HasField[Obj, K] { type Value }
object HasField {
type Aux[Obj, K, V] = HasField[Obj, K] { type Value = V }
implicit def evidence[Obj, K, V, Repr <: HList](
implicit
generic: LabelledGeneric.Aux[Obj, Repr], select: ops.record.Selector.Aux[Repr, K, V]
): HasField.Aux[Obj, K, V] = instance.asInstanceOf[HasField.Aux[Obj, K, V]]
private lazy val instance = new HasField[Any, Any]{}
}

}

}
12 changes: 12 additions & 0 deletions src/main/scala/com/abraxas/slothql/mapper/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ object GraphRepr {
Node { type Labels = Labels0; type Fields = Fields0; type Outgoing = Outgoing0 }
def unapply(repr: GraphRepr): Option[(List[String], Map[String, Property], Map[String, Relation])] =
PartialFunction.condOpt(repr) { case node: Node => (node.labels, node.fields, node.outgoing) }

case class Optional[N <: Node](node: N) extends Node {
type Labels = node.Labels
type Fields = node.Fields
type Outgoing = node.Outgoing
val Labels: Labels = node.Labels
val Fields: Fields = node.Fields
val Outgoing: Outgoing = node.Outgoing
val labels: List[String] = node.labels
val fields: Map[String, Property] = node.fields
val outgoing: Map[String, Relation] = node.outgoing
}
}

object Relation {
Expand Down
111 changes: 47 additions & 64 deletions src/test/scala/com/abraxas/slothql/TestArrows.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,13 @@ package com.abraxas.slothql

import scala.language.higherKinds

import shapeless.tag.@@
import shapeless.{ HList, HNil, Witness }

import com.abraxas.slothql.mapper.Arrow.{ Compose, Types }
import com.abraxas.slothql.mapper.GraphPath._
import com.abraxas.slothql.mapper._


trait CodeArrow extends Arrow
object CodeArrow {
/** Arrow representing object field selection. */
trait FieldFocus[Obj, K <: Symbol, V] extends CodeArrow {
type Source = Obj
type Target = V
val field: K
}
object FieldFocus {
def stub[Obj, V](k: String)(implicit w: Witness.Aux[k.type]): FieldFocus[Obj, Symbol @@ w.T, V] =
new FieldFocus[Obj, Symbol @@ w.T, V] { val field = Symbol(w.value).asInstanceOf[Symbol @@ w.T] }
}

// TODO
/** Arrow representing ??? inside a sequence. */
trait SeqFocus[CC[x] <: Seq[x], A] extends CodeArrow {
type Source = CC[A]
type Target = A
}
object SeqFocus {
def stub[CC[x] <: Seq[x], A]: SeqFocus[CC, A] = new SeqFocus[CC, A] {}
}

/** Arrow representing mapping a sequence with arrow `F`. */
trait SeqMapper[F <: Arrow, CC[x] <: Seq[x]] extends CodeArrow {
type Source <: CC[_]
type Target <: CC[_]
}
object SeqMapper {
type Aux[F <: Arrow, CC[x] <: Seq[x], S, T] =
SeqMapper[F, CC] { type Source = CC[S]; type Target = CC[T] }
def stub[F <: Arrow, CC[x] <: Seq[x]](implicit t: Types[F]): Aux[F, CC, t.Source, t.Target] =
new SeqMapper[F, CC] {
type Source = CC[t.Source]
type Target = CC[t.Target]
}
}
}


object Functors {
import CodeArrow._

/*
implicit def fieldFocusToDBArrowLeaf[
A <: Arrow, Obj, K <: Symbol, V, Repr <: GraphRepr.Element, Fields <: HList, Field <: GraphRepr.Property, V0
](
Expand Down Expand Up @@ -92,34 +48,61 @@ object Functors {
val rel = select(schema.repr.Outgoing.asInstanceOf[Rels])
compose(RelationTarget(rel, rel.To.asInstanceOf[RelTarget]), OutgoingRelation(schema.repr, rel))
}
*/
}



object FunctorsTest {
import CodeArrow._
import Functors._
import cats.instances.list._
import cats.instances.option._

import com.abraxas.slothql.test.models.{ Book, Page }

val sel1 = FieldFocus.stub[Book, List[Page]]("pages")
val sel2 = SeqFocus.stub[List, Page]
val sel3 = FieldFocus.stub[Page, String]("text")
val selPages = ScalaExpr[Book].pages
// ScalaExpr.SelectField[Book, pages, List[Page]]
// = SelectField("pages")

val mapped0 = Functor.map(sel2 ∘ sel1).to[GraphPath]
val selText = ScalaExpr[Page].text
// ScalaExpr.SelectField[Page, text, String]
// = SelectField("text")

val mapPagesText1 = selPages.map(selText)
// Arrow.Composition[
// GraphPath.RelationTarget[Book.PageListRepr.type, Page.PageRepr.type],
// GraphPath.OutgoingRelation[Book.BookRepr.type, Book.PageListRepr.type]
// ]{type Source = Book.BookRepr.type;type Target = Page.PageRepr.type}
// ScalaExpr.FMap[List, ScalaExpr.SelectField[Page, text, String]],
// ScalaExpr.SelectField[Book, pages, List[Page]]
// ]{type Source = Book;type Target = List[String]}
// = FMap(SelectField(text)) ∘ SelectField(pages)

val mapped1 = Functor.map(sel3 ∘ (sel2 ∘ sel1)).to[GraphPath]
val mapPagesText2 = selPages.map(_.text)
// Arrow.Composition[
// GraphPath.PropSelection[Page.PageRepr.type, GraphRepr.Property{type Type = String}],
// Arrow.Composition[
// GraphPath.RelationTarget[Book.PageListRepr.type, Page.PageRepr.type],
// GraphPath.OutgoingRelation[Book.BookRepr.type, Book.PageListRepr.type]
// ]{type Source = Book.BookRepr.type;type Target = Page.PageRepr.type}
// ]{type Source = Book.BookRepr.type;type Target = GraphRepr.Property{type Type = String}}

val m = Functor.map(sel3).to[GraphPath] ∘ Functor.map(sel2 ∘ sel1).to[GraphPath]
assert(mapped1 == m)
// ScalaExpr.FMap[List, ScalaExpr.SelectField[Page, text, String]],
// ScalaExpr.SelectField[Book, pages, List[Page]]
// ]{type Source = Book;type Target = List[String]}
// = FMap(SelectField(text)) ∘ SelectField(pages)

val selAuthor = ScalaExpr[Book].selectDynamic("author") // same as `.author`
// ScalaExpr.SelectField[Book, author, Option[Author]]
// = SelectField("author")

val mapAuthorName = selAuthor.map(_.name)
// Arrow.Composition[
// ScalaExpr.FMap[Option, ScalaExpr.SelectField[Author, name, String]],
// ScalaExpr.SelectField[Book, author, Option[Author]]
// ]{type Source = Book;type Target = Option[String]}
// = FMap(SelectField(name)) ∘ SelectField(author)

val mapAuthorPseudonym = selAuthor.flatMap(_.pseudonym)
// Arrow.Composition[
// ScalaExpr.MBind[Option, ScalaExpr.SelectField[Author, pseudonym, Option[String]]],
// ScalaExpr.SelectField[Book, author, Option[Author]]
// ]{type Source = Book;type Target = Option[String]}
// = MBind(SelectField(pseudonym)) ∘ SelectField(author)


// val mapped0 = Functor.map(sel2 ∘ sel1).to[GraphPath]
// val mapped1 = Functor.map(sel3 ∘ (sel2 ∘ sel1)).to[GraphPath]

// val m = Functor.map(sel3).to[GraphPath] ∘ Functor.map(sel2 ∘ sel1).to[GraphPath]
// assert(mapped1 == m)
}
34 changes: 24 additions & 10 deletions src/test/scala/com/abraxas/slothql/test/models/Book.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import shapeless.syntax.singleton._

import com.abraxas.slothql.mapper.{ GraphRepr, Schema }

trait Book {
def author: String
def pages: List[Page]
}
case class Book(author: Option[Author], pages: List[Page])
case class Author(name: String, pseudonym: Option[String])
case class Page(text: String)

object Book {
object PageListRepr extends GraphRepr.Relation {
Expand All @@ -30,12 +29,14 @@ object Book {

object BookRepr extends GraphRepr.Node {
type Labels = Witness.`"Book"`.T :: HNil
type Fields = Witness.`'author`.Field[GraphRepr.Property.Aux[String]] :: HNil
type Outgoing = Witness.`'pages`.Field[PageListRepr.type] :: HNil
type Fields = HNil
type Outgoing = Witness.`'author`.Field[GraphRepr.Node.Optional[Author.AuthorRepr.type]] ::
Witness.`'pages` .Field[PageListRepr.type] :: HNil

lazy val Labels: Labels = "Book".narrow :: HNil
lazy val Fields: Fields = 'author ->> GraphRepr.Property[String] :: HNil
lazy val Outgoing: Outgoing = 'pages ->> PageListRepr :: HNil
lazy val Fields: Fields = HNil
lazy val Outgoing: Outgoing = 'author ->> GraphRepr.Node.Optional(Author.AuthorRepr) ::
'pages ->> PageListRepr :: HNil

lazy val labels: List[String] = List("Book")
lazy val fields: Map[String, GraphRepr.Property] = Map("author" -> GraphRepr.Property[String])
Expand All @@ -46,8 +47,21 @@ object Book {
implicit def bookSchema: Schema.Aux[Book, BookRepr.type] = Schema.defineFor[Book](BookRepr)
}

trait Page {
def text: String
object Author {
object AuthorRepr extends GraphRepr.Node {
type Labels = Witness.`"Author"`.T :: HNil
type Fields = Witness.`'name` .Field[GraphRepr.Property.Aux[String]] ::
Witness.`'pseudonym`.Field[GraphRepr.Property.Aux[Option[String]]] :: HNil
type Outgoing = HNil
val Labels: Labels = "Author".narrow :: HNil
val Fields: Fields = 'name ->> GraphRepr.Property[String] ::
'pseudonym ->> GraphRepr.Property[Option[String]] :: HNil
val Outgoing: HNil = HNil
val labels: List[String] = List("Author")
val fields: Map[String, GraphRepr.Property] = Map("name" -> GraphRepr.Property[String],
"pseudonym" -> GraphRepr.Property[Option[String]])
val outgoing: Map[String, GraphRepr.Relation] = Map()
}
}

object Page {
Expand Down

0 comments on commit 4bc97c1

Please sign in to comment.