Skip to content

Algebraic Effects for Context

Mikael Vejdemo-Johansson edited this page May 16, 2024 · 4 revisions

These are some notes towards using Algebraic Effects as a design paradigm for the TDA4j algebra interfaces.

References on Algebraic Effects

Our Effects

The effect choice we will work with is the choice of field for the computation. Picking a different field changes the results of a computation with chains and cellular complexes -- but does not necessarily change the computation itself (especially for the simplicial boundary operator, which is fully describeable using only the Fractional[T] interface without any numeric primitive values.

A Code Example

Building off of the Direct-style Effects Explained blog post, here is one example of how this design could work out:

Library Code

Declare Type for Effectful Results and a Trait for the Effect Handler

type FractionalComputable[A] = CoefficientIsFractional ?=> A
trait CoefficientIsFractional {
  type CoefficientT
  val coefficientIsFractional : Fractional[CoefficientT]
  given Fractional[CoefficientT] = coefficientIsFractional
  def apply[A](body : FractionalComputable[A]): A =
    body(using this)
}

Produce a Constructor

This one is designed to be used to prefix-tag executable blocks, see below for an example.

object FrComp {
  inline def apply[A](inline body : CoefficientIsFractional ?=> A): FractionalComputable[A] = body
}

Library or User Code

Produce a few Effect Handlers

object DoubleIsFractional extends CoefficientIsFractional {
  override type CoefficientT = Double
  override val coefficientIsFractional: Fractional[Double] = math.Numeric.DoubleIsFractional
}

object Fp3IsFractional extends CoefficientIsFractional {
  val fp3 = FiniteField(3)
  override type CoefficientT = fp3.Fp
  override val coefficientIsFractional: Fractional[CoefficientT] =
    fp3.FpIsFractional
}

User Code

Produce a (lazy) computable value

I had to make this one extra lazy to get the Ammonite console to play nicely. Either val or lazy val would make the console try to print it out, which made it try to evaluate it, which is not possible because there's no available CoefficientIsFractional object around. Ideally, these should be possible to state as val instead when we need them, just need to figure out how to make that workable.

import scala.math.Fractional.Implicits.infixFractionalOps
def frcomp : FractionalComputable[Boolean] =
  FrComp {
    val cfr : CoefficientIsFractional = summon[CoefficientIsFractional]
    import cfr.{*,given}
    val fr : Fractional[CoefficientT] = summon[Fractional[CoefficientT]]
    val x : CoefficientT = fr.one + fr.one + fr.one
    x == fr.zero
  }

So, here, frcomp will compute 1+1+1 in whatever field we decide to work in, and output a boolean dependent on whether or not the result is zero in that field. Ideally I would like to find a way to reduce the imports and summon boiler plate at the beginning, but it is not yet clear to me how.

Once we have our computation specified, we can run the actual computation by feeding in an object carrying the field definitions:

@ DoubleIsFractional(frcomp) 
res10: Boolean = false

@ Fp3IsFractional(frcomp) 
res11: Boolean = true