We're using similar pattern in our code base: defmodule App.MaybePipe do defmacro left <|> right do quote do with {:ok, val} <- unquote(left) do val |> unquote(right) end end end end
which basically encapsulates logic of railway oriented programming. If either call next item in the pipeline (with result as first argument) if previous returned {:ok, result} tuple. Otherwise it's just "go to the side track" and return immediate value. We're using it for example like this: args |> validate_data() <|> maybe_new() <|> Repo.insert() I feel that it's a simple macro and sufficient for that needs. On Fri, Oct 8, 2021 at 8:12 PM José Valim <jose.va...@dashbit.co> wrote: > I don't believe we should necessarily be leading people to encode more > logic into pipelines. For example, sometimes the value that is in the > conditional comes from the pipeline, other times it is simpler to pattern > match with "case" or "with", etc. Or just a plain old conditional. It > should also be trivial enough to add to your own apps if really necessary. > > On Fri, Oct 8, 2021 at 8:02 PM 'Damir' via elixir-lang-core < > elixir-lang-core@googlegroups.com> wrote: > >> Hi! >> >> Thanks for the elaborate response! >> >> > You have to write "write_y()" function anyway, so create a version that >> has a no-op version that simply returns the argument: >> >> Sometime write_y is already there and then you need to write a wrapper >> function. Also I really try to avoid putting a boolean inside every >> function or letting it check for nil input values, i try to make this >> decision explicit at the caller location. >> >> > You can write an anonymous function inline that contains your logic >> pretty easily, although I don't think this syntax is well-liked, it's at >> least clear. >> >> Yes, now that then/2 is here it's even easier, but I agree that still the >> readability is not ideal :) >> >> > What you don't say whether or not the function called in the "then" >> function actually returns the original piped value, or if it returns the >> result of the "write_y" function >> >> like then/2 it should return the result of the write_y function >> >> > The idea of "conditionally apply sales tax to a price calculation" >> comes to mind, but you would encapsulate the logic into the function >> anyway, because the actual sales tax rate is different per state. I'd >> expect that to be more like: >> >> > item >> > |> apply_quantity_discount(quantity) >> > |> apply_sales_tax(state). # apply_sales_tax would know whether or not >> to do anything based on the state >> >> This example actually makes sense because the decision whether or not to >> apply function 'apply_sales_tax' is solely based on the 'state' argument. >> But what about 'apply_quantity_discount' ? Maybe we should only apply that >> function in certain situations and should the amount be computed inside >> that function? Of course you can put all this logic inside >> *apply_quantity_discount* but depending on how complex the *decision* and >> the actual *transformation* is, you might want to split it up into two >> separate functions. And then you have to write a maybe_ variant or put a >> boolean check inside the transformation function. >> >> Overall I'm also not 100% convinced that this is needed, but somehow it >> still feels like that from a functional programming perspective the concern >> of "deciding" and actually "applying" a transformation should be separate >> from each other, especially when the "deciding" part is not based on the >> input arguments of the transformation. >> >> > I really can't think of too many, and wonder if maybe there's just a >> better way for you to think about the problem in the first place? >> >> I'll dig more into our projects and see if I can come up with a list of >> real-world examples. >> >> Anyways, there's plenty food for thought here, thanks :)! >> Op vrijdag 8 oktober 2021 om 17:35:47 UTC+2 schreef ...Paul: >> >>> I think this has been brought up a few times and not sure there's a >>> great need for it. It can't (shouldn't) happen _that_ many times, because >>> if you're not going to do something with the result of a function the >>> chances are you should be bothering to do_something() to the function >>> anyway. And even then there are a few other ways this can be done that are >>> arguably cleaner. >>> >>> 1. You have to write "write_y()" function anyway, so create a version >>> that has a no-op version that simply returns the argument: >>> >>> def write_y(y, arg0, arg1, skip \\ false) >>> def write_y(y, _, _, false), do: y >>> def write_y(y, arg0, arg1, _) do >>> .... >>> >>> 2. You can write an anonymous function inline that contains your logic >>> pretty easily, although I don't think this syntax is well-liked, it's at >>> least clear. >>> >>> some_value >>> |> do_something() >>> |> (fn y -> >>> if y_needed, do: write_y(y, arg0, arg1), else: y >>> end).() >>> >>> 3. What you don't say whether or not the function called in the "then" >>> function actually returns the original piped value, or if it returns the >>> result of the "write_y" function. There _has_ been discussion about a >>> "tap" function that would let you basically encapsulate the logic in the >>> previous inline function example but be _sure_ to always return the >>> original piped value (so you don't forget the "else: y" bit). I don't >>> remember if that was something that eventually was decided or not, but it's >>> a simple function to write yourself if you really want it: >>> >>> def tap(value, block) do >>> block.(value) >>> value >>> end >>> >>> some_value >>> |> do_something() >>> |> tap(fn y -> >>> if y_needed, do: write_y(y, arg0, arg1) >>> end) >>> >>> The difference between #2 and #3 is what is ultimately returned if >>> "y_needed". The "tap" function sees a lot more use cases, I think, because >>> "side effects" are a more frequent occurrence -- tossing something into a >>> job queue, writing off a log message that isn't just "IO.inspect", >>> triggering an asynchronous operation, etc. Things where you want to do >>> something with an intermediate value but don't want to change it in the >>> process. >>> >>> I'm hard pressed to come up with a really great example of where you may >>> want to conditionally do something to a value inline that isn't better >>> handled with functional logic. The idea of "conditionally apply sales tax >>> to a price calculation" comes to mind, but you would encapsulate the logic >>> into the function anyway, because the actual sales tax rate is different >>> per state. I'd expect that to be more like: >>> >>> item >>> |> apply_quantity_discount(quantity) >>> |> apply_sales_tax(state). # apply_sales_tax would know whether or not >>> to do anything based on the state >>> >>> You say you have to do this "often" -- can you give us some real-world >>> examples of your code logic that you feel necessitates this kind of thing? >>> I really can't think of too many, and wonder if maybe there's just a better >>> way for you to think about the problem in the first place? >>> >>> ...Paul >>> >>> >>> >>> On Fri, Oct 8, 2021 at 3:59 AM 'Damir' via elixir-lang-core < >>> elixir-l...@googlegroups.com> wrote: >>> >>>> Often I have a function that I want to apply to a pipeline >>>> conditionally. I'm forced not to either write a "maybe_do_x" function or >>>> use an inline check with then/2. >>>> >>>> Now that we have then/2, I'm thinking it might be nice to have then/3 >>>> so that we don't need "maybe_" functions anymore, improving composability. >>>> Example usage: >>>> >>>> y_needed = # true or false >>>> >>>> some_value >>>> |> do_something() >>>> |> then(y_needed, &write_y(&1, arg0, arg1) ) >>>> >>>> -- >>>> >>> You received this message because you are subscribed to the Google >>>> Groups "elixir-lang-core" group. >>>> To unsubscribe from this group and stop receiving emails from it, send >>>> an email to elixir-lang-co...@googlegroups.com. >>>> To view this discussion on the web visit >>>> https://groups.google.com/d/msgid/elixir-lang-core/9b4501b4-e114-4421-b61a-74240d160a15n%40googlegroups.com >>>> <https://groups.google.com/d/msgid/elixir-lang-core/9b4501b4-e114-4421-b61a-74240d160a15n%40googlegroups.com?utm_medium=email&utm_source=footer> >>>> . >>>> >>> -- >> You received this message because you are subscribed to the Google Groups >> "elixir-lang-core" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to elixir-lang-core+unsubscr...@googlegroups.com. >> To view this discussion on the web visit >> https://groups.google.com/d/msgid/elixir-lang-core/71ebb1d0-c403-4054-aa79-60dd8d6a3a96n%40googlegroups.com >> <https://groups.google.com/d/msgid/elixir-lang-core/71ebb1d0-c403-4054-aa79-60dd8d6a3a96n%40googlegroups.com?utm_medium=email&utm_source=footer> >> . >> > -- > You received this message because you are subscribed to the Google Groups > "elixir-lang-core" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to elixir-lang-core+unsubscr...@googlegroups.com. > To view this discussion on the web visit > https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2B3HFsTvN0nDRHN0XQax%2BOvKMxrg_jqr0BG3qRmOta5KQ%40mail.gmail.com > <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4%2B3HFsTvN0nDRHN0XQax%2BOvKMxrg_jqr0BG3qRmOta5KQ%40mail.gmail.com?utm_medium=email&utm_source=footer> > . > -- You received this message because you are subscribed to the Google Groups "elixir-lang-core" group. To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CANmTJ6vx-Q5PWCewwitNbbGpvh_1Lbcow7_AXU-aGuKnQvnQJA%40mail.gmail.com.