In my last email, I mentioned how I hated having to nest lambdas for asynchronous callbacks in my network code:
obj1.connect("param", ()=>obj2.connect("asdf", ()=>obj1.join(obj2, ()=>println("done for now")
)))was godawful ugly. So, I proposed using for comprehension to clean things up a bit, which turned out to be very similar to how Akka's futures work (though not quite the same)for(res < - obj1.connect("param");
res <- obj2.conncet("asdf");res <- obj1.join(obj2))println("done for now")definitely looks better, but I don't always return something, and I can't put arbitrary code in.
So, my latest solution I whipped up rather quickly may mitigate some of my issues, but it also might be ugly. It's certainly not too novel, borrowing a bit from akka and F#'s async workflows...The idea is to instead use this syntax:
obj1.connect("param") ~>
obj2.connect("asdf") ~>obj1.join(obj2) ~>println("done for now") run()The final 'run' syntax is a bit weird, had some ideas on how to clean that up, but I was curious to get thoughts on the general approach...--Vincent
for(res < - obj1.connect("param");
res <- obj2.conncet("asdf");res <- obj1.join(obj2))println("done for now")definitely looks better, but I don't always return something, and I can't put arbitrary code in.Doesn't seem that bad to me (considering that your API heavily side-effects), even parallelizing the connects:obj1.connect("param") zip obj2.connect("asdf") flatMap { _ => obj1 join obj2 } foreach { _ => println("done for now" }
What type does connect return? I reckon you could tidy it further.
For your thought:
case class Param[F[_], X](k: String => F[X])
* has map if F has map
* has flatMap if F has flatMap
On Wed, May 30, 2012 at 6:57 PM, Vincent Marquez <vincent...@gmail.com> wrote:
Thanks for taking the time to reply Josh.
On Wed, May 30, 2012 at 3:22 PM, Josh Suereth <joshua....@gmail.com> wrote:
Funny enough, I'm giving a talk on this *exact* subject to the Pittsburgh techfest. Not to spoil things until after the talk, I believe you want "Monadic workflows". Key to this is the ability to lift behavior into the computational context (Future).
Cool. When is this talk taking place. I'll keep an eye out and hopefully it's recorded or you can put slides up.June 9th. Not sure on recordings. I plan to give it more than once :)
You are side-effecting *way* more than necessary here. If connect returns a connected object, then you'd have:
If you're talking about my Future having side effects, I could make it immutable and the API should stay the same.If you mean my obj.connect() method must have some implicit side effects, then I fear we might get into a bit of a philosophical debate here. :-) I don't mind it having side effects, the actual code I'd be using this for uses typed actors for any objects with internal state, so I should have thread safety from that. Tthe connect method would return a future that then sends an actor message to obj)I got ya. You just want to asynchronously sequence a set of messages to a bunch of actors? I think my early example (with for { _ <- ...}) shows that...
ALSO, if you abstract out the Future, you can wire in the execution context with implicits. That is, you can make a "SingleThreaded" context where everything executes immediately for testing.
Great idea. Thanks.
Some of us call this kind of behavior "monadic workflows", and they're pretty powerful, but very abstract. I think we should probably stick with common conventions in scala (i.e. for-expressions) rather than custom methods and DSLs.
Given that I don't have to return a new connected obj (so for comprehension can be a bit cumbersome) do you still think an API to reduce the "{ }" for maps is a bad idea?It's now a matter of expectations and conventions in your company. I think we (the scala community) are starting to use for comprehensions both for searching collections *and* for monadic workflows. If you use a for-comprehension, you're using a more commonly understood scala convention. Haskell has its do-notation, we use for-comprehensions. If you create a new DSL for the same kind of thing, just know that you're setting up a new way to do the same thing that you'll have to teach everyone in your company.Personally, I stick to for-comprehensions because you have to learn those quickly to get into scala.
I agree the syntax is nasty - I would love to see an equivalent of F#'s workflow syntax. F# provides lots of different operators that are bound to keywords inside the workflow, the one that performs a "bind" and throws away the result is called "do!" (by analogy with "bind" which is written as "let!" - a sort of amplified "let"). Using F#'s syntax you would write your program as:async {do! obj1.connect("param")do! obj2.connect("asdf")do! obj1 join obj2do! Future(println("Done for now")}which is pretty nice.
val behavior : Future[Unit] =for {_ <- obj1.connect("param")_ <- obj2.connect("asdf")_ <- obj1 join obj2_ <- Future(println("Done for now")} yield ()seems to be the most idiomatic Scala way of what I'm trying to do (ignoring the fact that I need my Future to do something a wee bit different), but I do hate that I'd have to explicitly wrap non-Future returning calls in a future.
But it seems like we could leverage those abstractions better with additional language support. I agree that putting as much into libraries is ideal. However, macros open up a world of possible nicer syntax for this kind of development. Nicer library-based syntax with good conveniences.
Some additions to for comprehensions can also help IMHO. I agree that a library/user will know best. But some things are better with language support, like first-class functions.
Possibly once we have type-level macros everything is easier?
Perhaps we agree on things we'd like to see? I'm mostly unfamiliar with F# workflows but I like the term for the general pattern.
Also comprehensive comprehensions seem nice.
This is obviously a "nothing" program, but of course, that's not what
you are doing, because you are side-effecting. In other words, you want
to run in some kind of "first-class" environment for "side-effecting."
Scalaz has this if you are interested and I hear regular reports of its
reinventing and it is called IO.
If you take this to its obvious conclusion, sequence is not the only combinatory.
I’ve been playing with something here https://github.com/razie/gremlins
Some syntax samples include using + or --> but also seq/par and even CSP style syntax.
par {
seq {
inc
log($0)
}
seq {
inc
log($0)
}
}"""
Or CSP
v(c) (c ? P | c ! Q)
all this is obviously based on a rather heavy DSL library which uses all kinds of conversions to wrap things and bind them.
Cheers,
RAzie