Skip to content

Instantly share code, notes, and snippets.

@fkowal
Forked from vil1/REPLesent.scala
Created April 27, 2017 07:50
Show Gist options
  • Save fkowal/510ae82b3251811deb535a213ec84138 to your computer and use it in GitHub Desktop.
Save fkowal/510ae82b3251811deb535a213ec84138 to your computer and use it in GitHub Desktop.
slides of my presentation "Shapeless ? Easy !" at ScalarConf 2016

Shapeless ? Easy !

Load the REPLesent.scala file in a scala REPL (it's a slightly modified version, the original can be found here).

Then create the presentation with :

scala> val presentation = REPLesent(input = "shapeless_easy.txt", intp = $intp)
scala> import presentation._

You can run the code on a slide using !!!

/*
* Copyright 2015 Marconi Lanna
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
case class REPLesent(
width: Int = 0
, height: Int = 0
, input: String = "REPLesent.txt"
, slideCounter: Boolean = false
, slideTotal: Boolean = false
, intp: scala.tools.nsc.interpreter.IMain = null
) {
import scala.util.Try
private case class Config(
top: String = "*"
, bottom: String = "*"
, sinistral: String = "* "
, dextral: String = " *"
, newline: String = System.lineSeparator
, whiteSpace: String = " "
, private val width: Int
, private val height: Int
) {
val (screenWidth, screenHeight): (Int, Int) = {
val defaultWidth = 80
val defaultHeight = 25
if (width > 0 && height > 0) (width, height) else {
// Experimental support for screen size auto-detection.
// Supports only Unix-like systems, including Mac OS X and Linux.
// Does not work with Microsoft Windows.
val Array(h, w) = Try {
import scala.sys.process._
val stty = Seq("sh", "-c", "stty size < /dev/tty").!!
stty.trim.split(' ') map (_.toInt)
} getOrElse Array(0, 0)
val screenWidth = Seq(width, w) find (_ > 0) getOrElse defaultWidth
val screenHeight = Seq(height, h) find (_ > 0) getOrElse defaultHeight
(screenWidth, screenHeight)
}
}
private def fill(s: String): String = if (s.isEmpty) s else {
val t = s * (screenWidth / s.length)
t + s.take(screenWidth - t.length)
}
val topRow = fill(top) + newline
val bottomRow = fill(bottom)
val verticalSpace = screenHeight - 3 // accounts for header, footer, and REPL prompt
val horizontalSpace = screenWidth - sinistral.length - dextral.length
val blankLine = {
val padding = if (dextral.isEmpty) "" else whiteSpace * horizontalSpace + dextral
sinistral + padding + newline
}
}
private val config = Config(width = width, height = height)
private case class Line(content: String, length: Int, private val style: Line.Style) {
override def toString: String = content
def isEmpty: Boolean = content.isEmpty
def render(margin: Int): String = style(this, margin)
}
private object Line {
import scala.io.AnsiColor._
protected sealed trait Style {
import config.whiteSpace
protected def horizontalSpace = config.horizontalSpace
protected def fill(line: Line, left: Int, right: Int): String = {
whiteSpace * left + line + whiteSpace * right
}
def apply(line: Line, margin: Int): String
}
private object HorizontalRuler extends Style {
private val ansiBegin = RESET.head
private val ansiEnd = RESET.last
private val defaultPattern = Line("-")
def apply(line: Line, margin: Int): String = {
// Provides a default pattern if none was specified
val pattern = if (line.isEmpty) defaultPattern else line
val width = horizontalSpace - margin
val repeats = width / pattern.length
val content = pattern.toString * repeats
var remaining = width - repeats * pattern.length
var ansi = false
var reset = ""
val padding = pattern.toString takeWhile { c =>
val continue = remaining > 0
if (continue) c match {
case `ansiEnd` if ansi => ansi = false
case _ if ansi => // no-op
case `ansiBegin` => ansi = true; reset = RESET
case c if Character.isHighSurrogate(c) => // no-op
case _ => remaining -= 1
}
continue
}
val left = margin / 2
val right = margin - left
val l = Line(content + padding + reset, width, LeftAligned)
fill(l, left, right)
}
}
private object FullScreenHorizontalRuler extends Style {
def apply(line: Line, ignored: Int): String = HorizontalRuler(line, 0)
}
private object LeftFlushed extends Style {
def apply(line: Line, ignored: Int): String = {
val left = 0
val right = horizontalSpace - line.length
fill(line, left, right)
}
}
private object LeftAligned extends Style {
def apply(line: Line, margin: Int): String = {
val left = margin / 2
val right = horizontalSpace - left - line.length
fill(line, left, right)
}
}
private object Centered extends Style {
def apply(line: Line, ignored: Int): String = {
val margin = horizontalSpace - line.length
val left = margin / 2
val right = margin - left
fill(line, left, right)
}
}
private object RightAligned extends Style {
def apply(line: Line, margin: Int): String = {
val right = (margin + 1) / 2
val left = horizontalSpace - right - line.length
fill(line, left, right)
}
}
private object RightFlushed extends Style {
def apply(line: Line, ignored: Int): String = {
val left = horizontalSpace - line.length
val right = 0
fill(line, left, right)
}
}
private def style(line: String): (String, Style) = line match {
case s if s startsWith "<< " => (s.drop(3), LeftFlushed)
case s if s startsWith "< " => (s.drop(2), LeftAligned)
case s if s startsWith "| " => (s.drop(2), Centered)
case s if s startsWith "> " => (s.drop(2), RightAligned)
case s if s startsWith ">> " => (s.drop(3), RightFlushed)
case s if s startsWith "//" => (s.drop(2), FullScreenHorizontalRuler)
case s if s startsWith "/" => (s.drop(1), HorizontalRuler)
case s: String => (s, LeftAligned)
}
private val ansiEscape = """\\.""".r
private val ansiColor = Map(
'b' -> BLUE,
'c' -> CYAN,
'g' -> GREEN,
'k' -> BLACK,
'm' -> MAGENTA,
'r' -> RED,
'w' -> WHITE,
'y' -> YELLOW,
'B' -> BLUE_B,
'C' -> CYAN_B,
'G' -> GREEN_B,
'K' -> BLACK_B,
'M' -> MAGENTA_B,
'R' -> RED_B,
'W' -> WHITE_B,
'Y' -> YELLOW_B,
'!' -> REVERSED,
'*' -> BOLD,
'_' -> UNDERLINED
)
private def ansi(line: String): (String, Int) = {
var drop = 0
var reset = ""
val content: String = ansiEscape.replaceAllIn(line, m =>
m.matched(1) match {
case c if ansiColor.contains(c) => drop += 2; reset = RESET; ansiColor(c)
case 's' => drop += 2; RESET
case '\\' => drop += 1; "\\\\"
case c: Char => "\\\\" + c
}
)
(content + reset, drop)
}
private val emojiEscape = """:([\w+\-]+):""".r
private lazy val emojis: Map[String, String] = {
Try {
val input = io.Source.fromFile("emoji.txt").getLines
input.map { l =>
val a = l.split(' ')
(a(1), a(0))
}.toMap
} getOrElse Map.empty
}
private def emoji(line: String): (String, Int) = {
var drop = 0
val content: String = emojiEscape.replaceAllIn(line, m => {
m.group(1) match {
case e if emojis.contains(e) => drop += m.matched.length - 1; emojis(e)
case _ => m.matched
}
})
(content, drop)
}
def apply(line: String): Line = {
val (l1, lineStyle) = style(line)
val (l2, ansiDrop) = ansi(l1)
val (content, emojiDrop) = emoji(l2)
val length = l1.codePointCount(0, l1.length) - ansiDrop - emojiDrop
Line(content = content, length = length, style = lineStyle)
}
}
// `size` and `maxLength` refer to the dimensions of the slide's last build
private case class Build(content: IndexedSeq[Line], size: Int, maxLength: Int, footer: Line)
private case class Slide(content: IndexedSeq[Line], builds: IndexedSeq[Int], code: IndexedSeq[String]) {
private val maxLength = content.maxBy(_.length).length
def lastBuild: Int = builds.size - 1
def hasBuild(n: Int): Boolean = builds.isDefinedAt(n)
def build(n: Int, footer: Line): Build = Build(content.take(builds(n)), content.size, maxLength, footer)
}
private case class Deck(slides: IndexedSeq[Slide]) {
private var slideCursor = -1
private var buildCursor = 0
private def currentSlideIsDefined: Boolean = slides.isDefinedAt(slideCursor)
private def currentSlide: Slide = slides(slideCursor)
private def footer: Line = {
val sb = StringBuilder.newBuilder
if (slideCounter) {
sb ++= ">> " + (slideCursor + 1)
if (slideTotal) sb ++= "/" + slides.size
sb ++= " "
}
Line(sb.mkString)
}
private def select(slide: Int = slideCursor, build: Int = 0): Option[Build] = {
// "Stops" the cursor one position after/before the last/first slide to avoid
// multiple next/previous calls taking it indefinitely away from the deck
slideCursor = slide.min(slides.size).max(-1)
buildCursor = build
if (currentSlideIsDefined && currentSlide.hasBuild(buildCursor)) {
Some(currentSlide.build(buildCursor, footer))
} else None
}
def jumpTo(n: Int): Option[Build] = select(slide = n)
def jump(n: Int): Option[Build] = jumpTo(slideCursor + n)
def nextBuild: Option[Build] = select(build = buildCursor + 1) orElse jump(1)
def redrawBuild: Option[Build] = select(build = buildCursor)
def previousBuild: Option[Build] = select(build = buildCursor - 1) orElse {
jump(-1) flatMap { _ =>
select(build = currentSlide.lastBuild)
}
}
def lastSlide: Option[Build] = jumpTo(slides.size - 1)
def lastBuild: Option[Build] = jumpTo(slides.size) orElse previousBuild
def runAll: Unit = {
val code = currentSlide.code.mkString("\n")
if (repl.isEmpty) {
Console.err.print(s"No reference to REPL found. Please call with parameter intp=$$intp")
} else if (code.isEmpty) {
Console.err.print("No code for you")
} else {
repl foreach (_.interpret(code))
}
}
def runCode: Unit = {
val code = currentSlide.code(buildCursor)
if (repl.isEmpty) {
Console.err.print(s"No reference to REPL found. Please call with parameter intp=$$intp")
} else if (code.isEmpty) {
Console.err.print("No code for you")
} else {
repl foreach (_.interpret(code))
}
}
}
private val helpMessage = """Usage:
| next n > go to next build/slide
| previous p < go back to previous build/slide
| redraw z redraw the current build/slide
| Next N >> go to next slide
| Previous P << go back to previous slide
| i next i n advance i slides
| i previous i p go back i slides
| i go i g go to slide i
| first f |< go to first slide
| last l >| go to last slide
| Last L >>| go to last build of last slide
| run r !! execute code that appears on slide
| blank b blank screen
| help h ? print this help message""".stripMargin
private val repl = Option(intp)
private val deck = Deck(parseFile(input))
private def parseFile(file: String): IndexedSeq[Slide] = {
Try {
val input = io.Source.fromFile(file).getLines
parse(input)
} getOrElse {
Console.err.print(s"Sorry, could not parse file $file. Quick, say something funny before anyone notices!")
IndexedSeq.empty
}
}
private def parse(input: Iterator[String]): IndexedSeq[Slide] = {
sealed trait Parser {
def switch: Parser
def apply(line: String): (Line, Option[String])
}
object LineParser extends Parser {
def switch: Parser = CodeParser
def apply(line: String): (Line, Option[String]) = (Line(line), None)
}
object CodeParser extends Parser {
private val regex = {
val wb = "\\b"
val colors = Seq("m", "b", "c", "g", "y")
Seq(
"""(?:true|false|null|this)"""
, """[$_]*[A-Z][_$A-Z0-9]*[\w$]*"""
, """(?:contains|exists|filter|filterNot|find|flatMap|flatten|fold|""" +
"""forall|foreach|getOrElse|map|orElse)"""
, """(?i)(?:(?:0(?:[0-7]+|X[0-9A-F]+))L?|(?:(?:0|[1-9][0-9]*)""" +
"""(?:(?:\.[0-9]+)?(?:E[+\-]?[0-9]+)?F?|L?))|\\.[0-9]+(?:E[+\-]?[0-9]+)?F?)"""
, """(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|""" +
"""forSome|if|implicit|import|lazy|match|new|object|override|package|private|""" +
"""protected|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)"""
) map { s =>
(wb + s + wb).r
} zip colors
}
def switch: Parser = LineParser
def apply(line: String): (Line, Option[String]) = {
val l = Line("< " + (line /: regex) { case (line, (regex, color)) =>
regex.replaceAllIn(line, m =>
s"\\\\$color$m\\\\s"
)
})
(l, Option(line))
}
}
case class Acc(
content: IndexedSeq[Line] = IndexedSeq.empty
, builds: IndexedSeq[Int] = IndexedSeq.empty
, deck: IndexedSeq[Slide] = IndexedSeq.empty
, code: IndexedSeq[String] = IndexedSeq.empty
, codeAcc: IndexedSeq[String] = IndexedSeq.empty
, parser: Parser = LineParser
) {
import config.newline
def switchParser: Acc = copy(parser = parser.switch)
def append(line: String): Acc = {
val (l, c) = parser(line)
copy(content = content :+ l, codeAcc = c.fold(codeAcc)(codeAcc :+ _))
}
def pushBuild: Acc = copy(
builds = builds :+ content.size
, code = code :+ codeAcc.mkString(newline)
, codeAcc = IndexedSeq.empty
)
def pushSlide: Acc = {
if (content.isEmpty) {
append("").pushSlide
} else {
val finalBuild = pushBuild
val slide = Slide(content, finalBuild.builds, finalBuild.code)
Acc(deck = deck :+ slide)
}
}
}
val slideSeparator = "---"
val buildSeparator = "--"
val codeDelimiter = "```"
val acc = (Acc() /: input) { (acc, line) =>
line match {
case `slideSeparator` => acc.pushSlide
case `buildSeparator` => acc.pushBuild
case `codeDelimiter` => acc.switchParser
case _ => acc.append(line)
}
}.pushSlide
acc.deck
}
private def render(build: Build): String = {
import config._
val topPadding = (verticalSpace - build.size) / 2
val bottomPadding = verticalSpace - topPadding - build.content.size
val margin = horizontalSpace - build.maxLength
val sb = StringBuilder.newBuilder
def render(line: Line): StringBuilder = {
sb ++= sinistral
sb ++= line.render(margin)
sb ++= dextral
sb ++= newline
}
sb ++= topRow
sb ++= blankLine * topPadding
build.content foreach render
if (slideCounter && bottomPadding > 0) {
sb ++= blankLine * (bottomPadding - 1)
render(build.footer)
} else {
sb ++= blankLine * bottomPadding
}
sb ++= bottomRow
sb.mkString
}
private def show(build: Option[Build]): Unit = {
if (build.isEmpty) Console.err.print("No slide for you")
build foreach { b =>
print(render(b))
}
}
implicit class Ops(val i: Int) {
def next: Unit = show(deck.jump(i))
def n: Unit = next
def previous: Unit = show(deck.jump(-i))
def p: Unit = previous
def go: Unit = show(deck.jumpTo(i - 1))
def g: Unit = go
}
def next: Unit = show(deck.nextBuild)
def n: Unit = next
def > : Unit = next
def previous: Unit = show(deck.previousBuild)
def p: Unit = previous
def < : Unit = previous
def redraw: Unit = show(deck.redrawBuild)
def z: Unit = redraw
def Next: Unit = 1.next
def N: Unit = Next
def >> : Unit = Next
def Previous: Unit = 1.previous
def P: Unit = Previous
def << : Unit = Previous
def first: Unit = 1.go
def f: Unit = first
def |< : Unit = first
def last: Unit = show(deck.lastSlide)
def l: Unit = last
def >| : Unit = last
def Last: Unit = show(deck.lastBuild)
def L: Unit = Last
def >>| : Unit = Last
def run: Unit = deck.runCode
def r: Unit = run
def !! : Unit = run
def !!! : Unit = deck.runAll
def blank: Unit = print(config.newline * config.screenHeight)
def b: Unit = blank
def help: Unit = print(helpMessage)
def h: Unit = help
def ? : Unit = help
}
< \r███████\m╗\r██\m╗ \r██\m╗ \r█████\m╗ \r██████\m╗ \r███████\m╗\r██\m╗ \r███████\m╗\r███████\m╗\r███████\m╗ \r██████\m╗
< \r██\m╔════╝\r██\m║ \r██\m║\r██\m╔══\r██\m╗\r██\m╔══\r██\m╗\r██\m╔════╝\r██\m║ \r██\m╔════╝\r██\m╔════╝\r██\m╔════╝ ╚════\r██\m╗
< \r███████\m╗\r███████\m║\r███████\m║\r██████\m╔╝\r█████\m╗ \r██\m║ \r█████\m╗ \r███████\m╗\r███████\m╗ ▄\r███\m╔╝
< \m╚════\r██\m║\r██\m╔══\r██\m║\r██\m╔══\r██\m║\r██\m╔═══╝ \r██\m╔══╝ \r██\m║ \r██\m╔══╝ ╚════\r██\m║╚════\r██\m║ ▀▀══╝
< \r███████\m║\r██\m║ \r██\m║\r██\m║ \r██\m║\r██\m║ \r███████\m╗\r███████\m╗\r███████\m╗\r███████\m║\r███████\m║ \r██\m╗
< \m╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝╚══════╝╚══════╝╚══════╝ ╚═╝
< \r███████\m╗ \r█████\m╗ \r███████\m╗\r██\m╗ \r██\m╗ \r██\m╗
< \r██\m╔════╝\r██\m╔══\r██\m╗\r██\m╔════╝╚\r██\m╗ \r██\m╔╝ \r██\m║
< \r█████\m╗ \r███████\m║\r███████\m╗ ╚\r████\m╔╝ \r██\m║
< \r██\m╔══╝ \r██\m╔══\r██\m║╚════\r██\m║ ╚\r██\m╔╝ ╚═╝
< \r███████\m╗\r██\m║ \r██\m║\r███████\m║ \r██\m║ \r██\m╗
< \m╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝
| Scalar conference - Warsaw - 2016/4/16
> \g@ValentinKasas \_\chttp://kanaka.io
---
| Who am I ?
* Freelance developer from \r█\w█\b█\s
* Using Scala since mid 2012
* (\_Extremely\s) modest Shapeless contributor (\g131\s loc)
---
| What is Shapeless ?
* A library for generic programming in Scala
* Started as an experiment back in 2011
* Now has \b67\s contributors and \g44k\s loc
---
| Generic programming
* Programming with types not known beforehand
* Goes beyond classical polymorphism
╔═ \gGeneric program\s ╗
║ ║
║ v
* \bType\s ══> \gGeneric representation\s ══> \bType\s
---
| Heterogeneous Lists
--
* A "List" containing elements of different types
--
* Retains the type of each element in its own type
--
```
sealed trait HList
case object HNil extends HList
case class ::[H, T <: HList](head: H, tail: T) extends HList
```
---
| HList examples
```
val intStringLong = 42 :: "Scalar" :: 1L :: HNil
type LSB = Long :: String :: Boolean :: HNil
val lsb: LSB = 1L :: "foo" :: false :: HNil
// this wouldn't compile
// val lsb2: LSB = intStringLong
```
---
| Manipulating HLists
--
* The HList trait defines no method
--
* Every operation is injected through typeclasses
---
| Counting the elements of a HList
--
```
case class Size[L <: HList](get: Int)
```
--
```
object Size {
```
--
```
implicit val hnilSize = Size[HNil](0)
```
--
```
implicit def hconsSize[H, T <: HList](implicit tailSize: Size[T]) =
Size[H :: T](1 + tailSize.get)
```
--
```
def apply[L <: HList](l: L)(implicit size: Size[L]): Int = size.get
}
```
---
| How does that work ?
--
< Calling
```
Size("foo" :: true :: HNil)
```
--
< Requires an implicit
```
Size[String :: Boolean :: HNil]
```
--
< It can be obtained by calling
```
hconsSize[String, Boolean :: HNil]
```
--
< Which requires an implicit
```
Size[Boolean :: HNil]
```
--
< Obtained via
```
hconsSize[Boolean, HNil]
```
--
< Which needs an implicit
```
Size[HNil]
```
--
< Which is the type of
```
hnilSize
```
--
< So in the end we have
```
Size("foo" :: true :: HNil) = hconsSize[String, Boolean :: HNil](
hconsSize[Boolean, HNil](
hnilSize))
= Size[String :: Boolean :: HNil](1 +
Size[Boolean :: HNil](1 +
Size[HNil](0).get).get)
```
---
| Manipulating HList (cont'd)
* Decompose the HList recursively
* Use implicit resolution to derive the whole traversal
---
| Ok, that's all fun and games, but it's pretty \ruseless\s so far
---
| Generic : making things useful
--
* We need a way to transform our classes to their generic representation
--
* \bGeneric\s does that for us
--
```
case class Person(name: String, age: Int)
val genPerson = Generic[Person]
val homer = Person("Homer Simpson", 42)
val repr = genPerson.to(homer)
val ned = genPerson.from("Ned Flanders" :: 42 :: HNil)
```
---
| Real life use case : computing deltas
* Imagine we want to be able to determine what are the difference between two objects of the same type
* For example, we need to know what have changed in our DB since the last backup
* We need to be able to compute such deltas over a wide variety of classes, that are unrelated
* Of course, doing this by hand for each and every class is not an option
---
| Our diff representation
```
sealed trait Diff[A]
final case class Identical[A](value: A) extends Diff[A]
final case class Different[A](left: A, right: A) extends Diff[A]
object Diff {
def apply[A](left: A, right: A): Diff[A] =
if (left == right) Identical(left)
else Different(left, right)
}
```
---
| A first Delta implementation
```
trait SimpleDelta[R <: HList] extends DepFn2[R, R] {
type Out <: HList
}
--
object SimpleDelta {
type Aux[I <: HList, O <: HList] = SimpleDelta[I]{ type Out = O }
--
implicit def hnilDelta: Aux[HNil, HNil] = ???
--
implicit def hconsDelta[H, T <: HList, DT <: HList]
(implicit tailDelta: Aux[T, DT])
: Aux[H::T, Diff[H] :: DT] = ???
}
```
---
| A first Delta implementation
```
trait SimpleDelta[R <: HList] extends DepFn2[R, R] {
type Out <: HList
}
object SimpleDelta {
type Aux[I <: HList, O <: HList] = SimpleDelta[I]{ type Out = O }
implicit def hnilDelta: Aux[HNil, HNil] = new SimpleDelta[HNil] {
type Out = HNil
def apply(l: HNil, r: HNil): Out = HNil
}
implicit def hconsDelta[H, T <: HList, DT <: HList]
(implicit tailDelta: Aux[T, DT])
: Aux[H::T, Diff[H] :: DT] = ???
}
```
---
| A first Delta implementation
```
trait SimpleDelta[R <: HList] extends DepFn2[R, R] {
type Out <: HList
}
object SimpleDelta {
type Aux[I <: HList, O <: HList] = SimpleDelta[I]{ type Out = O }
implicit def hnilDelta: Aux[HNil, HNil] = new SimpleDelta[HNil] {
type Out = HNil
def apply(l: HNil, r: HNil): Out = HNil
}
implicit def hconsDelta[H, T <: HList, DT <: HList]
(implicit tailDelta: Aux[T, DT])
: Aux[H::T, Diff[H] :: DT] = new SimpleDelta[H :: T]{
type Out = Diff[H] :: DT
def apply(l: H :: T, r: H :: T) : Out =
Diff(l.head, r.head) :: tailDelta(l.tail, r.tail)
}
}
```
---
| A first Delta implementation
```
trait SimpleDelta[R <: HList] extends DepFn2[R, R] {
type Out <: HList
}
object SimpleDelta {
type Aux[I <: HList, O <: HList] = SimpleDelta[I]{ type Out = O }
implicit def hnilDelta: Aux[HNil, HNil] = new SimpleDelta[HNil] {
type Out = HNil
def apply(l: HNil, r: HNil): Out = HNil
}
implicit def hconsDelta[H, T <: HList, DT <: HList]
(implicit tailDelta: Aux[T, DT])
: Aux[H::T, Diff[H] :: DT] = new SimpleDelta[H :: T]{
type Out = Diff[H] :: DT
def apply(l: H :: T, r: H :: T) : Out =
Diff(l.head, r.head) :: tailDelta(l.tail, r.tail)
}
def apply[A, R <: HList](l: A, r: A)
(implicit genA: Generic.Aux[A, R],
delta: SimpleDelta[R])
: delta.Out =
delta(genA.to(l), genA.to(r))
}
```
---
| Lets try it out
```
case class Address(number: Int, street: String, city: String)
case class Character(name: String, age: Int, address: Address)
val homer = Character("Homer Simpson", 42, Address(742, "Evergreen Terrace", "Springfield"))
val ned = Character("Ned Flanders", 42, Address(744, "Evergreen Terrace", "Springfield"))
```
---
| Going further
* That's quite nice, but still a bit coarse-grained
* SimpleDelta doesn't work on nested fields
---
```
trait Delta[R <: HList] extends DepFn2[R, R]{
type Out <: HList
}
object Delta {
type Aux[R <: HList, O <: HList] = Delta[R]{type Out = O}
implicit def hnilDelta: Aux[HNil, HNil] = new Delta[HNil] {
override type Out = HNil
override def apply(l: HNil, r: HNil): Out = HNil
}
implicit def hconsGenDelta[H, GH <: HList, DH <: HList, T <: HList, DT <: HList]
(implicit genH: Generic.Aux[H, GH],
nested: Delta.Aux[GH, DH],
tailDelta: Delta.Aux[T, DT])
: Aux[H :: T, DH :: DT] = new Delta[H :: T] {
override type Out = DH :: DT
override def apply(l: H :: T, r: H :: T): Out =
nested(genH.to(l.head), genH.to(r.head)) :: tailDelta(l.tail, r.tail)
}
}
```
---
```
trait Delta[R <: HList] extends DepFn2[R, R]{
type Out <: HList
}
object Delta extends LowPriorityDelta {
type Aux[R <: HList, O <: HList] = Delta[R]{type Out = O}
implicit def hnilDelta: Aux[HNil, HNil] = new Delta[HNil] {
override type Out = HNil
override def apply(l: HNil, r: HNil): Out = HNil
}
implicit def hconsGenDelta[H, GH <: HList, DH <: HList, T <: HList, DT <: HList]
(implicit genH: Generic.Aux[H, GH],
nested: Delta.Aux[GH, DH],
tailDelta: Delta.Aux[T, DT])
: Aux[H :: T, DH :: DT] = new Delta[H :: T] {
override type Out = DH :: DT
override def apply(l: H :: T, r: H :: T): Out =
nested(genH.to(l.head), genH.to(r.head)) :: tailDelta(l.tail, r.tail)
}
}
trait LowPriorityDelta {
implicit def hconsDelta[H, T <: HList, DT <: HList]
(implicit tailDelta: Delta.Aux[T, DT])
: Delta.Aux[H :: T, Diff[H] :: DT] = new Delta[H :: T] {
override type Out = Diff[H] :: DT
override def apply(l: H :: T, r: H :: T): Out =
Diff(l.head, r.head) :: tailDelta(l.tail, r.tail)
}
}
```
---
```
trait Delta[R <: HList] extends DepFn2[R, R]{
type Out <: HList
}
object Delta extends LowPriorityDelta {
type Aux[R <: HList, O <: HList] = Delta[R]{type Out = O}
implicit def hnilDelta: Aux[HNil, HNil] = new Delta[HNil] {
override type Out = HNil
override def apply(l: HNil, r: HNil): Out = HNil
}
implicit def hconsGenDelta[H, GH <: HList, DH <: HList, T <: HList, DT <: HList]
(implicit genH: Generic.Aux[H, GH],
nested: Delta.Aux[GH, DH],
tailDelta: Delta.Aux[T, DT])
: Aux[H :: T, DH :: DT] = new Delta[H :: T] {
override type Out = DH :: DT
override def apply(l: H :: T, r: H :: T): Out =
nested(genH.to(l.head), genH.to(r.head)) :: tailDelta(l.tail, r.tail)
}
def apply[A, R <: HList](l: A, r: A)
(implicit genA: Generic.Aux[A, R],
delta: Delta[R])
: delta.Out =
delta(genA.to(l), genA.to(r))
}
trait LowPriorityDelta {
implicit def hconsDelta[H, T <: HList, DT <: HList]
(implicit tailDelta: Delta.Aux[T, DT])
: Delta.Aux[H :: T, Diff[H] :: DT] = new Delta[H :: T] {
override type Out = Diff[H] :: DT
override def apply(l: H :: T, r: H :: T): Out =
Diff(l.head, r.head) :: tailDelta(l.tail, r.tail)
}
}
```
---
| There is so much more to explore !
* Coproduct
* Singleton types
* LabelledGeneric
* Extensible records
* Poly
* Typeclass derivation
* etc ...
---
| \gThanks
---
| Bonus : Patcher
```
trait Patcher[R <: HList, P <: HList] {
def apply(repr: R, patch: P): R
}
trait LowPriorityPatcher {
implicit def hconsPatcher[H, T <: HList, PT <: HList]
(implicit tailPatcher: Patcher[T, PT]): Patcher[H::T, Diff[H]::PT] = new Patcher[H::T, Diff[H]::PT] {
override def apply(repr: H :: T, patch: Diff[H] :: PT): H :: T = {
val head = patch.head match {
case Identical(_) => repr.head
case Different(_, x) => x
}
head :: tailPatcher(repr.tail, patch.tail)
}
}
}
object Patcher extends LowPriorityPatcher{
def apply[A, R <: HList, P <: HList](value: A, patch: P)
(implicit gen: Generic.Aux[A, R], patcher: Patcher[R, P]): A =
gen.from(patcher(gen.to(value), patch))
implicit def hnilPatcher: Patcher[HNil, HNil] = new Patcher[HNil, HNil] {
override def apply(repr: HNil, patch: HNil): HNil = HNil
}
implicit def hconsGenPatcher[H, GH <: HList, T <: HList, PH <: HList, PT <: HList]
(implicit genH: Generic.Aux[H, GH],
headPatcher: Patcher[GH, PH],
tailPatcher: Patcher[T, PT])
: Patcher[H::T, PH::PT] =
new Patcher[H::T, PH::PT] {
override def apply(repr: H :: T, patch: PH :: PT): H :: T =
genH.from(headPatcher(genH.to(repr.head), patch.head)) :: tailPatcher(repr.tail, patch.tail)
}
}
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment