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.

Reply via email to