A question on style here…
I have an object algebra type that has various operations on it. These allow an algebra instance to apply operations through a result, like this (the type signatures are simplified)…
trait Algebra[Result]:
def empty: Result
def addSomething(partialResult: Result, newData: String): Result
def forgetSomething(partialResult: Result, obsoleteKey: Int): Result
def joinSomethingsTogether(partialResult: Result, thingOne: Int, thingTwo: Int): Result
end Algebra
Algebras can be supplied for production and for testing. It’s also possible to delegate from one algebra to another, performing some pre / post-processing in the outer algebra. Cool.
There is a consumer of algebras that takes an algebra and pumps out some result by applying operations (again, simplified):
def weeklyGrind[Result](algebra: Algebra[Result]): Result =
val monday = algebra.empty
val tuesday = algebra.addSomething(monday, "A hard working day.")
val wednesday = algebra.joinSomethingsTogether(tuesday, 3, 4)
val thursday = algebra.forgetSomething(wednesday, -16)
// Long weekend follows...
thursday
end weeklyGrind
You might look at that result threading and wonder, but hold that thought…
So far, so good - the result threading isn’t too prolix in practice, as it happens.
However, the latest wheeze is to allow one algebra to be supplied to the weeklyGrind
, and then to play back the same operations to another algebra with a completely different implementation. This is because weeklyGrind
can be a very expensive computation in the real world case.
So let’s write an algebra to do that:
case class Recording(playback: [Result] => Algebra[Result] => Result)
// ^^^
// Polymorphic because we don't know what kind of result
// the downstream algebra will work with.
class RecordingAlgebra extends Algebra[Recording]:
def empty: Recording =
Recording(
[Result] =>
((downstreamAlgebra: Algebra[Result]) => downstreamAlgebra.empty)
)
def addSomething(partialResult: Recording, newData: String): Recording =
Recording([Result] => { (downstreamAlgebra: Algebra[Result]) =>
val theStorySoFar = partialResult.playback(downstreamAlgebra)
downstreamAlgebra.addSomething(theStorySoFar, newData)
})
def forgetSomething(partialResult: Recording, obsoleteKey: Int): Recording =
???
def joinSomethingsTogether(
partialResult: Recording,
thingOne: Int,
thingTwo: Int
): Recording = ???
end RecordingAlgebra
Now one can playback the working week ad-nauseum:
val onTheRecord = weeklyGrind(new RecordingAlgebra)
val _ = onTheRecord.playback(new DoSomeActualWorkAlgebra)
val _ = onTheRecord.playback(new BookInMindlessTaskCodesAlgebra)
val _ = onTheRecord.playback(new MoanEndlesslyAboutFutilityOfWorkAlgebra)
val _ = onTheRecord.playback(new BragHyperbolicallyBeforeBonusEvaluationAlgebra)
Excellent. The thing is, the way Recording.playback
is defined leads to a deepening stack as the partial results are computed in their original order. This can be a problem in practice.
So I could refactor to use:
- explicit continuation-passing written in longhand to chain the merge algebra calls using tail recursion,
- or
Eval
to get a trampolined alternative to continuation passing, - or
State[Unit]
to thread the partial results through the merge algebra calls (and keep the trampolining), - or
StateT[Reader, Unit]
to thread the partial results and supply the merge algebra … - … or something completely different.
I’m open to making Algebra
into a typeclass too, but only if it significantly simplifies things.
What would you folks do?