Skip to content

Commit

Permalink
Merge lenses into API refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
rmgk committed Mar 8, 2024
2 parents dcf3d56 + 3a12412 commit f8bf76c
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 13 deletions.
5 changes: 5 additions & 0 deletions Changelog.scim
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
= Changelog

== v0.35.1
date = 2024-03-08

• update Scala version to 3.3.3 due to incompatible release

== v0.35.0
date = 2024-02-24

Expand Down
30 changes: 30 additions & 0 deletions Modules/Example ReactiveLenses/concept.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Points:
1. Propagation richtung Root
2. How to do toModel()

Idee:
1. If LVar changed -> Propagate using toModel() Function -> where are the dependencies?
2. Propagate Signals & toViews() -> multiple Points of Change or just from root and evaluate vars multiple times

Signals vs Lenses

Signals Lenses
n -> 1 1 <-> 1
expr(Vars) -> Signal expr1(LVar) -> LVar
LVar <- expr2(LVar)
Signal = Signal {...} LVar[View] = LVar[Model].applyLens(...) / Lens(LVar) z.B. meters = yards.applyLens(new MulLens(0.9144))

What happens when an update occurs?

Signal => all dependencies are reevaluated
Var => inherits from Signal, but updates can be triggered manually
LVar => inherits from Var AND
all LVars connected via Lenses are updated using toView() OR toModel()

How to decide which method to call?

1. no change in dependency structure
toView() "trivially" as Signal expression
toModel() if "incoming" is connected via Lens, call toModel() -> How do we know that? & Do Vars have "incoming"?

2. extend dependencies bidirectional, if LVar in "incoming" && "outgoing" we have found a Lens -> What's the orientation?
20 changes: 20 additions & 0 deletions Modules/Example ReactiveLenses/index.template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ConversionTest</title>
</head>
<body>
<section class="conversions">
<noscript><div style="padding:2em;text-align:center;font-size:2em">
Please enable JavaScript!
</div></noscript>
</section>

<script type="text/javascript" src="JSPATH"></script>
<script type="text/javascript">
UnitConversion()
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package copl

import org.scalajs.dom.html.{Input, Select}
import org.scalajs.dom.document
import reactives.default.*
import reactives.extra.lenses.*
import reactives.extra.lenses.toSignalLens
import scalatags.JsDom
import scalatags.JsDom.all.*
import scalatags.JsDom.TypedTag

import scala.scalajs.js.annotation.JSExportTopLevel

object ConversionTest {

@JSExportTopLevel("UnitConversion")
def run(): Unit = main(Array.empty[String])

def main(args: Array[String]): Unit = {
val temperatureConverter = unitConverter()
document.body.replaceChild(temperatureConverter.render, document.body.firstChild)
()
}


/**
* Contains all supported units with their conversion from Celsius. To add support for a unit, simply add it to the
* enum with the lens representing the corresponding conversion
* @param lens the conversion lens from Celsius
*/
enum TempConversion(val lens: BijectiveLens[Double, Double]):
//All Conversions are given from Celsius
case C extends TempConversion(new NeutralLens)
case K extends TempConversion(new AddLens(274.15))
case L extends TempConversion(new AddLens(253))
case F extends TempConversion(new MulLens(1.8).compose(new AddLens(32.0)))
end TempConversion

/**
* Creates a lens which converts between any supported units
*/
def conversionLens(from : TempConversion, to : TempConversion): BijectiveLens[Double, Double] = from.lens.inverse.compose(to.lens)

/**
* A demonstration of reactive lenses using a simple unit converter for temperature units
*/
def unitConverter() = {

//Create selection for units and convert selected units to signals
val leftUnitInput: TypedTag[Select] = select(TempConversion.values.map{ unit => option(unit.toString) })
val (leftUnitEvent: Event[String], renderedLeftUnit: Select) = RenderUtil.dropDownHandler(leftUnitInput, oninput, clear = false)
val leftUnitSignal: Signal[TempConversion] = leftUnitEvent.hold(init = renderedLeftUnit.value).map{TempConversion.valueOf(_)}

val rightUnitInput: TypedTag[Select] = select(TempConversion.values.map { unit => option(unit.toString) })
val (rightUnitEvent: Event[String], renderedRightUnit: Select) = RenderUtil.dropDownHandler(rightUnitInput, oninput, clear = false)
val rightUnitSignal: Signal[TempConversion] = rightUnitEvent.hold(init = renderedRightUnit.value).map{TempConversion.valueOf(_)}

//Create the two LVars containing the left and right value using reactive lenses.
val leftVar = LVar(0.0)
val rightVar = leftVar.applyLens(SignalLens(Signal{conversionLens(leftUnitSignal.value, rightUnitSignal.value)}))

//Create text fields and input events for the values
val leftValueInput: TypedTag[Input] = input(value := leftVar.now)
val (leftValueEvent: Event[String], renderedLeftValue: Input) = RenderUtil.inputFieldHandler(leftValueInput, oninput, clear = false)

val rightValueInput: TypedTag[Input] = input(value := rightVar.now)
val (rightValueEvent: Event[String], renderedRightValue: Input) = RenderUtil.inputFieldHandler(rightValueInput, oninput, clear = false)

//Register input events as source of change for LVars
leftVar.fire(leftValueEvent.map{toDoubleOr0(_)})
rightVar.fire(rightValueEvent.map{toDoubleOr0(_)})

//Observe LVars to update UI
leftVar.observe{ value => RenderUtil.setInputDisplay(renderedLeftValue, value.toString) }
rightVar.observe{ value => RenderUtil.setInputDisplay(renderedRightValue, value.toString) }

//Combine all UI elements
div(p("Unit Conversion with Lenses"), renderedLeftUnit, renderedRightUnit, br , renderedLeftValue, renderedRightValue)
}

/**
* Returns the double represented by the string or 0 if no double is represented
*/
def toDoubleOr0(str : String): Double = {
try {
{str.toDouble}
} catch {
case _ => {0.0}
}
}

/**
* A demonstration of the effect of declaration order on event execution if an event effects multiple LVars in the same
* cluster. When inverting the definition of b and c, the output changes.
*/
def raceConditionTest(): Unit = {
val a = LVar(0)
val b = a.applyLens(new AddLens(1))
val c = a.applyLens(new AddLens(2))

println("a is " + a.now.toString)
println("b is " + b.now.toString)
println("c is " + c.now.toString)

val e = Evt[Int]()
b.fire(e)
c.fire(e)
e.fire(3)
println("\n Fire event...")
println("a is " + a.now.toString)
println("b is " + b.now.toString)
println("c is " + c.now.toString)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package copl

import org.scalajs.dom.UIEvent
import org.scalajs.dom.html.{Input, Select}
import scalatags.JsDom.all.*
import reactives.operator.*
import scalatags.JsDom.{Attr, TypedTag}

object RenderUtil {
def inputFieldHandler(tag: TypedTag[Input], attr: Attr, clear: Boolean = true): (Event[String], Input) = {
val handler = Event.fromCallback(tag(attr := Event.handle[UIEvent]))
val todoInputField: Input = handler.data.render

// observer to prevent form submit and empty content
handler.event.observe { (e: UIEvent) =>
e.preventDefault()
if (clear) todoInputField.value = ""
}

// note that the accessed value is NOT a reactive, there is a name clash with the JS library :-)
val inputFieldText = handler.event.map { _ => todoInputField.value.trim }

(inputFieldText, todoInputField)
}

def dropDownHandler(tag: TypedTag[Select], attr: Attr, clear: Boolean = true): (Event[String], Select) = {
val handler = Event.fromCallback(tag(attr := Event.handle[UIEvent]))
val todoInputField: Select = handler.data.render

// // observer to prevent form submit and empty content
// handler.event.observe { (e: UIEvent) =>
// e.preventDefault()
// if (clear) todoInputField.value = ""
// }
//
// // note that the accessed value is NOT a reactive, there is a name clash with the JS library :-)
val inputFieldText = handler.event.map { _ => todoInputField.value.trim }

(inputFieldText, todoInputField)
}

def setInputDisplay(in : Input, text : String): Unit = {
in.value = text
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ object CreationTicket {
info: ReInfo
): CreationTicket[State] =
new CreationTicket(scopeSearch, info.derive(str))
}

final class CreationTicketCont[State[_]](ct: CreationTicket[State])
object CreationTicketCont {
given fromTicket[State[_]](using ct: CreationTicket[State]): CreationTicketCont[State] = CreationTicketCont(ct)
}

/** Essentially a kill switch, that will remove the reactive at some point. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ trait SchedulerWithDynamicScope[State[_], Tx <: Transaction[State]] extends Sche

/** Provides the capability to look up transactions in the dynamic scope. */
trait DynamicScope[State[_]] {
private[reactives] def dynamicTransaction[T](f: Transaction[State] => T): T
private[reactives] def dynamicTransaction[T](f: Transaction[State] ?=> T): T
def maybeTransaction: Option[Transaction[State]]
}

class DynamicScopeImpl[State[_], Tx <: Transaction[State]](scheduler: SchedulerWithDynamicScope[State, Tx])
extends DynamicScope[State] {

final private[reactives] def dynamicTransaction[T](f: Transaction[State] => T): T = {
final private[reactives] def dynamicTransaction[T](f: Transaction[State] ?=> T): T = {
_currentTransaction.value match {
case Some(transaction) => f(transaction)
case None => scheduler.forceNewTransaction(Set.empty, ticket => f(ticket.tx))
case Some(transaction) => f(using transaction)
case None => scheduler.forceNewTransaction(Set.empty, ticket => f(using ticket.tx))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,31 @@ trait LowPrioTransactionScope {
}

trait CreationScope[State[_]] {
def embedCreation[T](f: Transaction[State] => T): T
def embedCreation[T](f: Transaction[State] ?=> T): T

private[reactives] def create[V, T <: Derived.of[State]](
incoming: Set[ReSource.of[State]],
initValue: V,
needsReevaluation: Boolean
)(instantiateReactive: State[V] => T): T = {
embedCreation { tx =>
embedCreation { tx ?=>
val init: Initializer[State] = tx.initializer
init.create(incoming, initValue, needsReevaluation)(instantiateReactive)
}
}

private[reactives] def createSource[V, T <: ReSource.of[State]](intv: V)(instantiateReactive: State[V] => T): T = {
embedCreation(_.initializer.createSource(intv)(instantiateReactive))
embedCreation(tx ?=> tx.initializer.createSource(intv)(instantiateReactive))
}
}

object CreationScope {

case class StaticCreationScope[State[_]](tx: Transaction[State]) extends CreationScope[State] {
override def embedCreation[T](f: Transaction[State] => T): T = f(tx)
override def embedCreation[T](f: Transaction[State] ?=> T): T = f(using tx)
}
case class DynamicCreationScope[State[_]](ds: DynamicScope[State]) extends CreationScope[State] {
override def embedCreation[T](f: Transaction[State] => T): T = ds.dynamicTransaction(f)
override def embedCreation[T](f: Transaction[State] ?=> T): T = ds.dynamicTransaction(f)
}

inline given search(using ts: TransactionSearch[Interface.State]): CreationScope[Interface.State] = ts.static match
Expand Down
Loading

0 comments on commit f8bf76c

Please sign in to comment.