New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Seqexec execution with a Zipper #85
Seqexec execution with a Zipper #85
Conversation
These 2 functions help with initialization and finalization of a Seqexec queue. Now `next` is only used when the execution is ongoing.
def actions: List[Action] = { | ||
|
||
// not available in scalaz? | ||
def lefts[L, R](xs: List[L \/ R]): List[L] = for { -\/(r) <- xs } yield r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftMap
perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about the Haskell lefts
. leftMap
maps over every left
but I just want to collect every left in a list of \/
. I'm pretty sure there'll be something in scalaz to do that but can't find how it's named.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, you can do collect
on the list. something like
xs.collect {
case -\/(e) => e
}
or perhaps
xs.collect {
case e @ -\/(_) => e
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.collect
is probably the most efficient solution but you could also say .separate._1
. Scalaz would take a PR if you wanted to add this to MonadPlus
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I try to avoid collect
if there is another alternative with total functions but this looks like an ideal case.
I didn't know about .separate._1
. I'll also look into the laws for MonadPlus
in Scalaz. In Haskell there is no consensus about which laws should a MonadPlus
obey and I'm curious to see which ones Scalaz sticks to.
/** | ||
* Transform an *unconsed* pending `Execution` into `Current` in addition to | ||
* returning the unwrapped `Execution`. | ||
*/ | ||
// TODO: Use same structure for `Current` and `Queue.Execution3`? | ||
private def currentify(exe3: Queue.Execution3[Action]): (Execution[Action], Current) = { | ||
private def currentify(exe3: Queue.Execution3[Action]): (Current) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by cons
and uncons
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest I didn't really know what cons
stands for until I looked it up but it's often used when adding an element to the front of something that looks like a single linked list. I guess the term is more widespread in Haskell/Lisp than anywhere else.
I could rename it if there is another term for that in Scala. How is ::
pronounced in Scala?
In any case these functions should be internal to just that module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right ::
is called cons
in a list. I just wondered what was the use here. I haven't seen uncons
though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why's the return type in parentheses?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me how the opposite of cons
should be named, I've seen it as uncons
and snoc
. Monocle uses snoc
so if we end up using Monocle, snoc
would be a better name.
I forgot to remove the parentheses, good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
snoc
typically is right-append. I think uncons
is more typical.
Apparently SEQNG-4 is no longer an issue after fixing SEQNG-1 and SEQNG-3. This test only prints the output to console, it doesn't test the execution is correct on its automatically.
prime
and cleanup
#resolve
After writing more tests I realized the completed |
case -\/(-\/((actions, seqid, stepid))) => (actions, Current(vec(actions), Some((seqid, stepid).left))) | ||
case -\/(\/-((actions, stepid))) => (actions, Current(vec(actions), Some(stepid.right))) | ||
case \/-(actions) => (actions, Current(vec(actions), None)) | ||
case -\/(-\/((actions, seqid, stepid))) => Current(vec(actions), Some((seqid, stepid).left)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is a bit strange to see two \/
nested. what do they represent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each sum type of the \/
represents if either a Sequence
or a Step
or a Execution
in the current Step
was completed. It's explained in the documentation for that function but I should put a small comment for each case pattern to make the code easier to follow.
\/
with more than 2 types was strange for me too because in Haskell you can't do that. One of the nice features of \/
is that it supports sum types of arbitrary number of elements, however in practice it turns out it's not so great because as you can see, pattern matching is not so straightforward.
The solution for this is to create a custom sum data type. This will be necessary, in any case, when adding more complex Steps so I'm writing this down as an improvement to do later.
In any case this weird sum types are internal to this object, they are not meant to be used anywhere else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In think it's fine this way, unless you think a custom type will add value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shapeless provides an open coproduct type that lets you express A \/ B \/ C
as A :+: B :+: C :+: CNil
, and then you can fold by applying a polytypic function. It's much nicer in general but probably not introducing for isolated cases like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way \/
handles more than 2 types with those constructors is a bit hacky. For now it's fine, but I'll keep and eye on Shapeless if I ever need more robust sum types.
@@ -187,7 +201,8 @@ object Queue { | |||
mseq match { | |||
// No more Steps in current Sequence, remove Sequence. | |||
// TODO: listTailPLens? | |||
case None => ((exe, seq0.id, stepid).left.left, Queue(q.sequences.tailOption.getOrElse(List()))) | |||
case None => ((exe, seq0.id, stepid).left.left, | |||
Queue(q.sequences.tailOption.getOrElse(List()))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Often List()
is represented as Nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know. I think I have more instances of List()
spread over the code, so I'll change them all.
gets(_.current.actions) >>= ( | ||
actions => Nondeterminism[Task].gatherUnordered( | ||
actions.zipWithIndex.map(act(_)) | ||
).liftM[EngineStateT] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you remind me what liftM
does here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just for lifting a Task
into Engine
: Task[A] ~> Engine[A]
. Engine
is a monad transformer of just 1 layer, the StateT with the Seqexec State over Task
as the base monad.
What I got from the discussion we had a while back, with @swalker2m and @tpolecat too, is that 1 layer is manageable but more than 1 layer become too cumbersome in Scala. It seems that when the need for more transformer layers arise all the solutions lean towards the use of Free Monads for cleaner and more composable effects.
I wanted to experiment first with a simple toy example combining 2 effects using Free monads and coproducts before trying to do it in the Seqexec code, but so far I could get away with just 1 transformer layer without too much trouble.
However, I don't think the code will be able to scale in complexity properly with just 1 layer. For example, another layer I wish we had is a Reader monad (a.k.a Kleisli) with the scalaz-stream queue so that we don't have to be passing the queue explicitly as a parameter all over the code. Also, a Reader will be inevitable when/if we need to access a global configuration object from the engine.
So at some point all those liftM
should go away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, the config system uses Kliesli
for configuration. About the scalaz-stream queue
the current implementation has a single queue
in an object becoming global. Would that work instead of passing it as a param?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A global mutable variable (in order to create a queue
we would have to do IO at some point) brings me painful memories from my Python times :). In the concrete case of queue
as a parameter it looks like it would be OK to use an unsafe global: it's only created once and it's only read everywhere and not meant to be modified.
But since it seems will have to include the Kleisli
for configuration anyway, we could piggyback the queue
in that Kleisli
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually in ocs3 the queue is passed as a variable, but initialized on the object construction
So passing it on the Kliesli
sounds equivalent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, now I see what you mean, I didn't consider that pattern. I'm reading now about TaskRef
to understand how ExecutorImpl
works. I also realize that the DhsClient
will have to follow a similar pattern to the scalaz-stream
queue creation, so I think it's worth exploring the possibilities.
In the end, I think it'll be worth having something similar to an Environment
object embedded in a Kleisli
with everything that needs to be read-only throughout the Seqexec code. This object can be created only once, when initialized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few comments.
status: Status) | ||
status: Status) { | ||
|
||
def isEmpty: Boolean = this.pending.sequences.isEmpty && this.current.isEmpty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor stylistic thing; it's not idiomatic to use this.
for members unless there is a need to disambiguate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was reluctant to use this
but wanted to make isEmpty
a method of Queue
instead of a standalone function over Queue
because it seems more natural for a publicly exposed API.
Is there a more functional way to implement this kind of methods? The Syntax Ops packages seem a bit overkill but perhaps they are worth writing for these cases anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind my previous comment. I just realized the parameters of a case class are in scope in its case class block.
(exe3pending, qp) <- Queue.uncons(st.pending) | ||
(actions, c) = currentify(exe3pending) | ||
} yield (actions, QState(qp, c, qd, st.status)) | ||
def next(qs: QState): Option[QState] = cleanup(qs) >>= (prime(_)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to write this as cleanup(qs) >>= prime
.
t => { | ||
val (exe3, q) = t | ||
QState(q, currentify(exe3), qs.done, qs.status) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A function literal can be expressed in cases.
Queue.uncons(qs.pending).map {
case (exe3, q) => QState(q, currentify(exe3), qs.done, qs.status)
}
/** | ||
* Transform an *unconsed* pending `Execution` into `Current` in addition to | ||
* returning the unwrapped `Execution`. | ||
*/ | ||
// TODO: Use same structure for `Current` and `Queue.Execution3`? | ||
private def currentify(exe3: Queue.Execution3[Action]): (Execution[Action], Current) = { | ||
private def currentify(exe3: Queue.Execution3[Action]): (Current) = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
snoc
typically is right-append. I think uncons
is more typical.
case -\/(-\/((actions, seqid, stepid))) => (actions, Current(vec(actions), Some((seqid, stepid).left))) | ||
case -\/(\/-((actions, stepid))) => (actions, Current(vec(actions), Some(stepid.right))) | ||
case \/-(actions) => (actions, Current(vec(actions), None)) | ||
case -\/(-\/((actions, seqid, stepid))) => Current(vec(actions), Some((seqid, stepid).left)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shapeless provides an open coproduct type that lets you express A \/ B \/ C
as A :+: B :+: C :+: CNil
, and then you can fold by applying a polytypic function. It's much nicer in general but probably not introducing for isolated cases like this.
def actions: List[Action] = { | ||
|
||
// not available in scalaz? | ||
def lefts[L, R](xs: List[L \/ R]): List[L] = for { -\/(r) <- xs } yield r |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.collect
is probably the most efficient solution but you could also say .separate._1
. Scalaz would take a PR if you wanted to add this to MonadPlus
.
case Result.OK => q.enqueueOne(completed(i)) | ||
case Result.Error => q.enqueueOne(failed(i)) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer match
over destructuring assignment because you'll get exhaustiveness checking. Also in general use flatMap
unless >>=
is clearly better because it's more efficient.
... = t match {
case (action, i) =>
action.flatMap {
case ...
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I thought destructuring assignments were syntactic sugar for _ match {...
. Also, TIL flatMap
and >>=
are not the same! I thought they were aliases of each other. Good to know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, destructing assignments are not checked carefully so you can get a match error at runtime. >>=
is equationally identical to flatMap
but has higher overhead because there is an intervening implicit conversion. In practice it rarely matters but it's good to remember.
case Status.Running => ( | ||
gets(_.current.actions) >>= ( | ||
actions => Nondeterminism[Task].gatherUnordered( | ||
actions.zipWithIndex.map(act(_)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can probably say .map(act)
here.
I don't really understand the new code review feature, sorry. Some of my comments are out of context because I added them to running conversations. So maybe look at the code tab. |
Among them: - case deconstruction instead of assignment deconstruction. - `>>=` -> `flatMap`. - Remove `this`. - Use `collect` instead of list monad with for-comprehension. - More commenting. This takes care of comments for gemini-hlsw#85.
The completion of Executions is still broken but since I'm realizing it will take some time to fix it, will it be OK to merge this PR as it is? |
Among them: - case deconstruction instead of assignment deconstruction. - `>>=` -> `flatMap`. - Remove `this`. - Use `collect` instead of list monad with for-comprehension. - More commenting. This takes care of comments for gemini-hlsw#85.
Among them: - case deconstruction instead of assignment deconstruction. - `>>=` -> `flatMap`. - Remove `this`. - Use `collect` instead of list monad with for-comprehension. - More commenting. This takes care of comments for gemini-hlsw#85.
Among them: - case deconstruction instead of assignment deconstruction. - `>>=` -> `flatMap`. - Remove `this`. - Use `collect` instead of list monad with for-comprehension. - More commenting. This takes care of comments for gemini-hlsw#85.
Among them: - case deconstruction instead of assignment deconstruction. - `>>=` -> `flatMap`. - Remove `this`. - Use `collect` instead of list monad with for-comprehension. - More commenting. This takes care of comments for gemini-hlsw#85.
Switch to Wavelength
With the Zipper there is now a deeper data hierarchy with a top level Queue, _Sequence_s, _Step_s, _Execution_s and _Action_s. Previously there were only the equivalent of _Execution_s and _Action_s. Also with this structure it comes support for marking
Execution
s as completed instead of remove them.The executions for each test can be seen printed to the console. The tests themselves only check that the executions finish.
Don't bother too much reviewing this PR since the changes are quite messy. It can be considered as a starting point for the Zipper based execution.