Proposal: Kernel.then/3

74 views
Skip to first unread message

Damir

unread,
Oct 8, 2021, 6:59:54 AM10/8/21
to elixir-lang-core
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) )

Allen Madsen

unread,
Oct 8, 2021, 9:48:56 AM10/8/21
to elixir-l...@googlegroups.com
No opinion on whether this function exists or not. But, if it did, I think the second argument should be a function, so that conditionality can depend on a property of the first argument.

--
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.

Paul Clegg

unread,
Oct 8, 2021, 11:35:47 AM10/8/21
to elixir-l...@googlegroups.com
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



--

Damir

unread,
Oct 8, 2021, 2:02:01 PM10/8/21
to elixir-lang-core
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:

José Valim

unread,
Oct 8, 2021, 2:12:48 PM10/8/21
to elixir-l...@googlegroups.com
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.

Damir

unread,
Oct 9, 2021, 5:41:46 AM10/9/21
to elixir-lang-core
> 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.

I can see what you mean.

> It should also be trivial enough to add to your own apps if really necessary.

True, I'll start experimenting with this helper function and see how it works out for me.

Thanks all for the the discussion :)!

Op vrijdag 8 oktober 2021 om 20:12:48 UTC+2 schreef José Valim:

Piotr Szmielew

unread,
Oct 10, 2021, 12:41:06 PM10/10/21
to elixir-l...@googlegroups.com
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.

Reply all
Reply to author
Forward
0 new messages