SBTB 2015: Jean Rémi Desjardins, Automatic Concurrency through Computation Expressions
Recording: SBTB 2015: Jean Rémi Desjardins, Automatic Concurrency through Computation Expressions
question and the is yours thank you okay everyone um so I think it goes without saying that you guys can totally feel free to interrupt me this is a very very intimate setting um so I'm going to talk to you about uh expressions with there's a library that I worked on u a while ago and try and kind of explain why I worked on it so can someone tell me what's wrong with this code or what someone might consider to be wrong with this code sure sure uh so you're saying a false data dependency between what you can't perform the get address operation until you receive the result and get phone so you're the code is claiming that there's a dependency there when there's no actual dependency okay um right so actually what I was kind of okay so um Travis said that there's a dependency because get address um it's as if it depended on get phone but it doesn't really um and I so I guess Travis kind of jumped ahead and um sorry sorry no no no worry so the so what I was kind of looking for here is actually the fact that um these Futures are not going to execute in parallel um because when you call the function and then you get a future um but but I guess that's kind of the saying the same thing actually um and then what about now we now the Futures are executing um in let me just in parallel can you hear me correctly if I okay I just it's annoying to kind of put my mouth in it constantly um so yeah now they're executing parallel but we still have a problem and the problem is still emanates from the fact that there's a uh kind of a false dependency here and the problem is that if the first um future completes in one second let's let's say and then the second one completes in 2 seconds and the third one actually fails after after 10 milliseconds then the following code is going to fail after 2 seconds and in my experience most of the time in U enterprise software and just software in general that's uh not what you want you could actually make an argument that you actually want your code to be perfectly deterministic and you want to fail with you want to look at all the things because you want to always fail with the same thing but I think in practice most of the time if if one of the things is going to fail most people want to know immediately and and don't really care care about the failure being deterministic so that's kind of the problem and a solution would be the following code so we have our three Futures this time now we've kind of introduced Scala Z we're using applicative to apply the three we're combining them um actually that's sorry let's say we're rendering same same idea and now it's going to fail after 10 milliseconds um because we're using applicative and applicative is capable of well applicative basically doesn't have the dependency right so you're you're doing things in parallel so that's the solution um but then here's another problem where here we actually have a dependency so in order to get the address we need the phone um and to get the Spam score we also need the phone and so in this case uh we want to go back to our for comprehension um model where we have a dependency um but then there isn't a dependency between get address and get spam score so uh we're still failing after 2 seconds because get address takes two seconds get spam score fails after 10 milliseconds but we're not able to kind of address this um so this is another solution where we address this um and here we're kind of falling back to not using a for comprehension notation we map over the phone then we use our applicative 2 in order to uh combine address and spam spam score together um but this code is kind of I mean it's pretty straightforward but it's kind of sarting starting to be a little bit boilerplate and one needs to kind of kind of think oh how am I going to combine these things together to make sure it behaves in the way I want it to behave so a better solution I think is through Expressions the library that I wrote um and within an expression block you can extract things um that are in an abstraction or kind of like a a monad um and so so here we're we're extracting phone we put that in p and now we render P we render extract of get address P extract of get spam score p and now this fails after 10 milliseconds um so it's pretty concise it does what you want um so yeah so this is kind of my motivation example um because I was trying to write this kind of code that fail fast and I ended up using scaled uh when I was solving it at my company but I was kind of super happy I wanted something that you could use um like a nice concise kind of for comprehension notation but for comprehension doesn't do this and if you prefer something that looks a little bit more like for comprehension here you open expression block file P extract address spam score kind of it looks like a for comprehension render and this behaves the same way except that it fails it fails fast which is kind of what I was going for so why exactly does this not fail fast I will not go into the details of why that is it has to do with with monads and the way they're they're built um you can watch my talk at Pacific Northwest Scala 2014 in order to see that and well if we have times and you guys if you guys have questions at the end we can I have a few slides on it but I'm going to skip it for now so Expressions expressions are a notation for kind of all the things um so what are the features it uses the least powerful interface and that's so it uses applicative when it can instead of using monad all the time and that means that Futures can fail fast it also means that you can use uh validation inside of Expressions it plays well with if and match statements so this is kind of one of the reasons that uh I think the Scala async await project was created is because four comprehensions don't really match well with if and match statements when you kind of mix them together you end up with this blob of code that's not always super readable um so so it plays well with if and match statements um it unlike Scala Ayn A8 which is specialized for futures it's a United a unified notation that works for all abstractions so it'll work for Io for future for task um for validation and anything that has an applicative oron ad defined for it and finally it's customized will if you don't want your futures to fail fast you can provide an instance of applicative that does not do fail fast um if that's what you prefer so Taps Taps into type classes to do to be highly customizable so here are a few examples um this is the the failing fast kind of example that I used for motivating so the yeah the idea is that you want to fail fast um I'll skip that because we already went through it interacting with if so here we have three Futures the first one Waits a second second one Waits 5 seconds and the the other one Waits 2 seconds and if we do a for comprehension here um and then after that within the yield we do if a is equal to something then polish b or else polish C if a is equal to something then there's no need to wait on c um unfortunately if we use it in the for comprehension this whole thing is going to take 5 seconds because we need to wait on ABC before applying our operation um and we could fix this with the second version um where we have nested four comprehensions and now it only takes two seconds but now we have nested four comprehensions it's kind of hard to read and so with Expressions you have something that's relatively close to the four comprehension not notation above um but it it only takes uh two seconds because it's through the magic of of applicative and type classes it kind of figures out that uh it doesn't need to wait on C in order to polish B because a was equal to something thing do you guys have any questions I've been kind of plowing through no okay uh and so one of the nice things obviously like for comprehensions this supports kind of all any notation so option either writer list IO task um it's fully gener generalized notation and also it supports validation which is kind of interesting um and I wrote in theory because I I I was working on this um few minutes ago and I didn't quite get it to work but uh in theory a really interesting thing is uh so validation doesn't work in for comprehensions because there's no monad defined for it and the reason there's no modad is because the idea of validation is to accumulate errors and the way to do that is using applicative and that all makes sense um and obviously you can use validation with expression but one could argue that it's not super useful to just use that because um if you were to just use applicative with it the notation is actually pretty decent but you could Define a monad for validation that um uh defines app as not um kind of stopping and so accumulating errors and defines bind or flat map as kind of stopping and so that way you can have this Big Blob of code and if you have dependencies then obviously if a fails and you need a in order to produce another validation then you won't get any errors from that validation because that validation was never able to be created because you didn't have the a but if you have a bunch of things that are sort of independent then you can accumulate um errors so in this case here if we look at the content of expression sorry that's a little bit small I'm realizing now um so the content of expression here we're extracting the first validation and with that we are getting the third one but the second and second one and the fourth one are independent um and if you were to try and do this with a for comprehension it wouldn't work because there's a dependency between the data and so I mean yeah just validation doesn't work in a for comprehension but but here you can do it and you'll still get the two uh message errors because a was a success we computed C from a or three anyway there's four of them a BC D um so you get C from a um and then that one fails and then D which was totally independent failed and so you end up with failure uh of both of them so I find this is kind of neat because you can work with validation and um the notation will just kind of do the right thing and find as many errors as possible depending on your dependencies between data excuse yes I've never seen four equal signs is that what I see on that last oh yeah so that's just because in oh yeah so the question is uh what are the the four equal signs and the four equal signs are come from specs two so when I was writing the presentation um I just want to make sure I'm not putting any code examples that that don't actually work so so that's just like copy pasting the code from uh from my specs 2 in the in the library and four equals is is just asserting that the two are equal and if they're not equal it'll print out saying you know this and this is is not equal you said it would be equal and if it's equal the test passes you could write should be it's kind of it's a short hand for should be really so yeah supports validation um so how does it work it uses Scala Ed um even though it doesn't technically need to it could use the same mechanism as for comprehensions which is to just expect filter and map and well in this case zip because zip would be the equivalent of applicative um well apply um so it could it could use in it could work in the the same way as uh as for comprehensions in order to not have a dependency on Scala Zed but right now it kind of it kind of depends on Scala Zed because it has this nice definition of all these abstractions so scaled defines funter and apply and applicative and and monad uh and basically what we're doing is we desugar the code within the macro um to use apply to map Point bind all these functions and we're very careful to use the the weakest uh method that we actually need um because for instance in in the case of validation you can Define well you could either go the root of saying well there there is no monad for validation in which case the syntax still works or you can say here's a monad but we're not going to just let um the applicative of of the monad for validation be derived from the monad we're actually going to make sure that apply actually is implemented in a way that um that that supports the the behavior that we want um so so yeah that's what that looks like and yeah so the macro is a fairly simple code transformation um so if we have ABC and then we have a a definition Fu that works on string and some of these are futures of string instead of being strings so we just put this expression Fu extract a extract B and really what it desar to is a call to applicative do apply to um as we see below um and so in this case one could argue that you might be it might be easier to just use the applicative notation like just use the the the raw the raw um applicative uh version is actually shorter than the version with expression but eventually you kind of start to see the savings um this slide is just to um talk about the importance of using the least powerful interface like I kind of alluded to before um so you the the more powerful an abstraction is uh the the less flexibility ility you have when you're implementing instances of of that abstraction so Monet is very powerful you can express kind of any any computation with monad but it it it limits the um it limits the the flexibility when you're implementing the the instance of the type class for for monad for instance so it's it's and you know you guys can ask me questions or watch my other talk um but basically it's impossible to define a monad for uh for future that uh that fails fast um well actually impossible you can Define like like I I said before you can define a um an instance of monad and make sure to override the the uh the definition of apply which is normally derived from the implementation of bind um and in which case you can define a monad that that fails fast but traditionally it's considered that you kind of can't Define am Mona that that fails fast um so that's that so here's an example where Monet is required so uh we have a function Fu which takes strings and a function bar which takes the string and the function bar it produces a string and so the uh our code with an expression is calling we're extracting a which is a future calling bar and then we get another future which we need to extract again and then we pass that into Fu and and so basically the idea is here you there's no you need to use monad you you cannot get away with applicative so um Expressions is going to to convert the above code into the code that we see below which is using uh monad and in this case once again not a huge difference yes what's the return type of yeah I mean it would be uh in this case it would be string yeah yeah it's too bad that it kind of got cut off but yeah so fu is is like a normal function and is kind of like one what might one might say like a monatic function it returns or or anyway it returns a future so like it's kind of putting it back in the context of a future and you need to like extract it again but that's yeah that's obviously a good question um so yeah and and then in the case of if statements you want to use monad um and the reason why you want to use monad is because if you don't use monad you can't do that uh you can't um avoid the branches that you you want to avoid um so in this case you know if a turns out to be true we we want to kind of ignore C we don't care if C takes you know hours and hours and hours or fails it doesn't matter because we don't need it at this point and and you need monad in order to to do that to support that um so this is an example of where Monet is more powerful so we need to use it but the trade-off is that the person implementing bind is less flexible than than apply so we won't be able to fail fast on it but that's that's fine that's what we want um then we have our match statement so a match statement is Rel uh match statements are actually kind of the the trickiest part of of implementing this macro this is a fairly straightforward uh match statement which kind of looks almost like an if and so the transformation is the same but um Scala has this uh concept of a stable identifier in a match statement so here Fu Y is uh it's not it's not saying oh okay in because normally if there was no back ticks on the fu y it would be saying match on anything and bind the value of of extract bar to to fui but in this case because of the back tis what we're saying is we're saying in the case where extract bar is equal to Fu Y which is a previously defined value then do this um and I don't think this is a feature of pattern matching that's used all that much in Scala but if if this uh macro is going to support all the scal scale language it kind of needs to support this kind of thing and the as you can see below like kind of the desugared version of that is uh is a little bit more involved you definitely don't want to like write that on your own and then if you have two of them it just gets even worse so there are there are some similar projects out there and so one of them is effectful um so effectful the stated goal is to be kind of like asyn a weight but generalized to any monad um so it's very similar to this one uh the the disadvantage is it doesn't do the it doesn't choose monad or or applicative it always uses monad so it it doesn't support the idea of failing fast it doesn't support validation in a nice way uh and that was kind of the reason why I've I I worked on Expressions then there's another project really interesting project called Scala workflow which actually is extremely featureful it supports nested abstractions it uh supports the use of the weakest um abstraction but unfortunately uses untyped macros which have kind of been abandoned um in Scala 21 and so the project is kind of just like being left there right now and and to be fair also because it was using unti macros um the macro is really complicated and it reused it was kind of Reinventing the wheel a lot and uh it kind of reimplemented scoping instead of letting scalla C take care of that and because of that it had very limited support for um for like the entirety of the language so um so I think it was in some ways kind of was a dead end but anyway we'll see if if untyped macros become a thing again and if it proves possible to I'm not sure about that approach so I after seeing it I I still thought it was worth kind of putting energy into my own project and then finally asyn um which has the advantage of being I guess a sip and like the most popular most well-known one but I kind of think that it would be unfortunate if asyo weight kind of well anyway I I I don't really know but it it strikes me that asyo weight is specialized for future when you can make something that um it could support you know task and and all these abstractions so and plus I think a wait I'm not sure it's like built on on sound principles um cu the like the implementation seems like this this big black box I'm not sure and and and it can actually support other Futures but you need to implement this huge interface um which kind of strikes me as odd when we have this nice um kind of category Theory defining applicative and monad with very minimalistic interfaces that allow you to do whatever you want um and is already fairly well known so it strikes me as a lot to have this weird kind of like huge definition of what a future is and you need to kind of like jam your thing into into this um and plus I did tests and it doesn't fail fast so so for for us at the time it was not an option um and I don't know probably it's possible to make it fail fast just just doesn't uh so limitations of the current project um it actually supports a bunch of stuff like well obviously you know just simple function application that's like the first thing IFL statements function curring Works string interpolation Works uh blocks of code with value definitions work um basic match statements work um so yeah the big problem is match statements um there are there's a lot of match statements that that match statements like get desugared already by the compiler into this like really complicated thing and it just makes it really hard as the implementor of a macro to kind of like deal with it um and then P pattern matching in a value definition actually desugar to a pattern match so so that's that's where it kind of starts to break down if you have complicated pattern matches and and so when would you want to use expressions in your own code uh so I believe that if you're writing kind of this big block of a synchronous code um like you know your typical like backend to a service where you're doing you're doing a lookup to get you know in this case um like a phone and address all that kind of stuff whatever um or you know tweets and stuff like that um and you just like you have all these asynchronous functions and you just want to like jam them together and render them whenever they're all done and if one of them fail like kind of like the most like straightforward case then I think it makes sense to use Expressions because it's just going to do like the right thing um and it's and it's like low like it raises kind of the level of abstraction where you don't need to think oh do I need to map or do I need to flat map or you know do I need to apply because I it's just just like just like compose them in the most like straightforward way and I'll do that for you um which is which is nice um and you know kind of the same thing if uh if you have these in this case it's option instead of future and the idea is you know we have a match statement we have if statement if we would try to do this with a four comprehens it would kind of start to break down because you know four comprehens expect this kind of linear thing um and here we have a bunch of options because um we're using a Json Library like the the Rapture Json library and when you do as string depending on your mode um it returns an option because maybe that you know maybe first fruit maybe fruits on the Json was not a thing um so all these are options and we just deal with them as if they weren't options and the whole thing is going to return option and you kind of don't need to think about it too much but it's not like a fullblown replacement for for for comprehensions um so this is an example taken from remotely which is an open source RPC that I worked a little bit on and here we have an example of a four comprehension where we call task. delay on system. current milliseconds so this is a function time which takes a task and produces a new task which is going to produce the same result and a duration so how much time it took to produce that task um and we we implement it with a for comprehension we do current mes then we execute the task and then we check the current meles again we substract it to pretty straightforward um and in this case as Travis pointed at the beginning there's kind of a dependency in the order so for comprehensions you know they're very good at sequencing things exactly um in practice we use them for a lot of cases where we don't and that's that's kind of where where I think Expressions comes in but in this case we actually really want to make sure that we we check the time we execute the task and then we check the time again uh were we to try and do this with Expressions it would kind of magically but it's not magic it's a very principle based on you know but um it would say hey there's no dependency between task and T1 so let's do them in parallel um and so possibly you'd end up with the task saying that it took zero seconds even though it might have taken five um so so definitely if you if you need precise sequencing of stuff um then you want to use four comprehensions but in all the other cases where you just want to jam things together um I think you'd be better off using expressions in terms of future work uh it would be nice to uh work with scal meta um because right now macros are kind of a pain um it would be really ni oh so this actually improv Scala check function generation I didn't uh update the uh the slides but um I have a pretty proud of my test suit I probably have like a humongous amount of well a lot of tests um that basically say um and uses skele check to say you know given like these three uh options of of something and you know given arbitrary functions on them Expressions should always give the same thing as you know what you'd expect it to be desugared to um and that's kind of nice it gives me a a decent amount of of confidence into yes could you just take a minute talk about yeah sure so Scala is kind of the um it's a project to kind of give facelift macros in in future in a future version of Scala um it wants to kind of Define its own version of the As and one that's uh I mean obviously you know Eugene would be better if i' had explain it but I'll trying um it kind of wants to Define its own as which is like nice to work with um because the of the compiler which is kind of the def facto as right now it's kind of it's optimized for performance and um so sometimes it's a little bit less it's it's not safe by construction so like you can constructs that are going to be like rejected by the compiler because they don't make sense but you can construct them so it's not type safe in that way and also um the the the the type checking kind of mutates the the trees so it's not perfectly immutable um and So scal Meta wants to define a kind of this like thirdparty um as which is safe by construction and immutable and then all the tools like macros or and Ides and you know any other tool that needs to manipulate an EST can use this kind of safe a and then scal and then the the the the Scala compiler trees can kind of implement their own version kind of like yeah anyway like plug in that and say so yeah it's basically yeah I don't know if uh that helps okay kind of fumbling here um anyway so Scala Che has has if you say you know given a function in the current version it generates you a constant function it's not able to generate a um an actual kind of arbitrary function um which which kind of reduces the amount of confidence I have in in the test because you know there's a lot of arbitrary functions doing things that might expose bugs in my thing where constant functions would appear to work perfectly um but luckily um there's been a there's been kind of a a renewal in work for scalet to support generation of arbitrary functions which is really nice um another thing I'd like to do is Implement context manipulation that's something from Scala workflow where sometimes inside of an expression it would be nice to kind of just add something to the monad depending what it is so if you're doing logging say at this point you want to add logging um I think I have an I might have a quick example of that on the next slide um supported nested abstractions that would be really nice um potentially that could get rid of the whole um uh Monet Transformers pattern um because if if Expressions had good support for nested um monads then then one could you know get get rid of all the boiler plate that kind of tries to address that problem but I don't know if that's possible we'll see um obviously fix the know limitations and generalize the tests because right now I would like to say for any abstraction that has a monad and any function and any initial value this should hold true but right now I kind of have to hardcode it to option because scallet check doesn't doesn't work at the type level if you want if you will like it won't Generate random types um to uh to like test your your function on this is just what I meant by context manipulation um so here we're using the writer abstraction um so you know eight we're we're saying this is a magic value um and then random I'm a random value and then within our expression when we're manipulating this stuff we we want to say that if if B is equal Z then we want to just like log the fact that we avoided a division by zero yay um else do a divid by B and so because the point of Expressions is to kind of unwrap us from the from whatever abstraction in this case writer and we can deal with values as if they were actual like kind of primitive values but within that context it can be hard to kind of get back to the context of writer um you need to kind of do two for coms or two expressions um if if within an expression you have this kind of magic context thing that allows you to kind of at that point just stick something into the context whatever that context is it would be nice because then you have the just one one like expression and you can arbitrarily kind of go in and out of the uh of the context So yeah thank you do you guys have any yeah sure U does that work with the reader monad uh it should I don't know if I have explicit tests with reader but reader I mean it's it's a monad so it should work yeah oh so the question was does it read does it work with the reader monad and um the answer is it in principle it should work with with any Monet it's a generalized notation yes Tris do you see so what would the advantage be between this approach and the macro that takes a for comprehension identifies the dependencies and rewrites them using an applic operation so it's similar except yeah that would allow so the question is what's the difference between this approach and an approach where the for comprehension is within the macro and um and then the the the definition macro kind of rewrites the the for comprehension um so I think there's like two so the first element is unfortunately um as a Implement you don't get the for comprehension you would get the flat map and the map but you might there might be like tricks to try and figure out that it is a for comprehension or or may maybe that's fine maybe the fact that you receive a flat map and map you can kind of still rewrite at that point I'd have to think about it um but it's definitely an interesting remark because um I actually think that this this macro does kind of two things and and they don't need to be coupled the first thing is it does the same thing as a syn A8 which is to say sometimes four comprehensions break down with if and match um so it does that and then the second thing is to kind of use applicative when it when it can um and I I don't think those things necessarily need to be coupled they're just coupled at this point for Simplicity because a def macro was kind of the simplest thing to do um but but definitely yeah that that would be an idea because you know conceivably one could want to mix kind of sequencing precisely things with with not sequencing precisely and I I thought about that like is there a way that that it could be kind of more unified so yeah definitely good question rough proof of concept and I haven't pushed it through to something that I would be comfortable using and so I was wondering would it make sense for me to use something like this or to continue the process of making Mye comprehension writer work um yeah it's an interesting I mean maybe we should have a look at it together and yeah um actually there's another project that I didn't mention here because I didn't have time to update my slides that came out from some people in Japan they write it they they wrote a post on scalaz mailing list um I forget the name of their project but yeah everyone seems to be writing kind of macro dsls to uh to kind of like make it simpler to work with monad it's interesting yeah at first maybe I would have kind of focused on another project had I known that everyone wanted to but it's kind of an UNS solved problem so so yeah we should like have a look at it any other questions I have one other question sure so a lot of people do object to having a a monad who that where the the app the apply inconsistent with the applicative I I so we actually do this in production in some cases and I've never actually seen real problems with it so I was wondering you obviously have cases where you are using a Monet for validation that is inconsistent with validation applicative operations have you seen actual problems with this in practic so the question is some people object to um an inconsistent a monad where the the the the um implementation of apply is not derive from bind so thusly probably inconsistent with with bind um and but it is possible to Define this in practice to to Define to kind of override the version of of apply to not be dependent on bind which allows you more flexibility um and so are you know are there gotes to that why what's the reason for people kind of objecting to that um I'm not sure for validation I just I just like did that right now I didn't really get it to work I think it'll work but um I I have put uh this with uh with future and production and I haven't ran into any issues I I don't see any fundamental reason why it's not possible to to do both so so it would be a good question to kind of ask the you know these people why why this is an objection cool thank you