[Proposal] Inspection using a function

130 views
Skip to first unread message

Jonathan Moraes

unread,
Dec 28, 2020, 2:49:43 PM12/28/20
to elixir-lang-core
Hello alchemists,

I have a simple yet powerful proposal for an inspection function that can apply a desired function in a value and return the value itself.

For example, if today I want to inspect the keys of a given map inside a chain of pipes, I need to do the following:

...
|> map
|> inspect_keys()
|> ...

defp inspect_keys(map) do
  IO.inspect(Map.keys(map))
  map
end

Another example: if I want to count an Enumerable inside a function:

defp my_function(param) do
  list = build_list(param)
  IO.inspect(Enum.count(list))
  list
end

With a function that can inspect using a function AND return the value itself, both examples can be simplified:

...
|> map
|> IO.inspect_with(&Map.keys/1)
|> ...

defp my_function(param) do
  param
  |> build_list()
  |> IO.inspect_with(&Enum.count/1)
end

Since the second parameter from IO.inspect/2 is a keyword list of options, @v0idpwn (from Elixir Brasil Telegram group) suggested a new option called :apply. For example:

IO.inspect([1, 2, 3], apply: &Enum.count/1)

Felipe Stival

unread,
Dec 28, 2020, 3:01:21 PM12/28/20
to elixir-l...@googlegroups.com
I'm 100% in for this as a `:with` or `:apply` option for IO.inspect/2. I believe adding it as a new function may add unnecessary cognitive overhead. 

--
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/b0a390d1-9530-492c-b1fb-8ead8c9b938an%40googlegroups.com.

Zach Daniel

unread,
Dec 28, 2020, 3:07:06 PM12/28/20
to elixir-l...@googlegroups.com
Yeah, this is a great idea. I would love this :) I also agree that it would better to do it with an option though.

José Valim

unread,
Dec 28, 2020, 4:04:56 PM12/28/20
to elixir-l...@googlegroups.com
This has been discussed a couple times. I would prefer to introduce a general approach to this, such that:

    |> tap(&IO.inspect(Map.keys(&1))

But we always got stuck on what to call said function. 

Zach Daniel

unread,
Dec 28, 2020, 6:10:53 PM12/28/20
to elixir-l...@googlegroups.com
I can see how it might be hard to agree on a name. What if we just went with your gut then and called it `tap/1`? At some point you've just got to roll with something :D


Jonathan Moraes

unread,
Dec 28, 2020, 6:30:48 PM12/28/20
to elixir-lang-core
I agree with Zachary.

This "naming dilemma" reminds me of the usage of `each`, such as in `Enum.each/2`, to denote a mapping without a result.

It is not obvious at first (when you are learning Elixir) to understand what `each` and `map` do differently without checking the docs, examples, or using the functions.

The most important aspect of the name for this `tap` function is only to not conflict with other naming conventions or terminologies.

Maybe with `tap/1` we can start a naming convention for functions that achieves "result-immutability" (transitional functions / transparent functions) 

Even so, if `tap/1` is not a good candidate (which I think it is!), we can simply opt to be as verbose as possible while we try to achieve a better name. such as `apply_and_continue/1`, `exec_and_continue/1`, or `do_and_continue/1`.

Kevin Johnson

unread,
Dec 28, 2020, 9:55:47 PM12/28/20
to elixir-l...@googlegroups.com
 I would prefer to introduce a general approach to this, such that:
How general do you want it to be? 
Is this to cater solely for conveniently inlining side-effects; e.g. write to log, network, console or are there other use-cases that you envision?
Do you envision inlining multiple side-effects:
`|> tap(&Map.keys/1, [&IO.inspect/1, &KafkaEx.produce("foo", 0,  &Poison.encode!(&1)), ...])` to facilitate some fan-out strategy with perhaps some desired options?

Zach Daniel

unread,
Dec 28, 2020, 10:20:06 PM12/28/20
to elixir-l...@googlegroups.com
That takes it a bit too far for my taste. That part can easily be done by passing an anonymous function and calling a series of functions.

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

José Valim

unread,
Dec 29, 2020, 4:47:17 AM12/29/20
to elixir-l...@googlegroups.com
I propose we simply add two functions to Kernel: tap and then.

1. tap receives an anonymous function, invokes it, and returns the argument. It can be found in Ruby and .NET Rx.

2. then receives an anonymous function, invokes it, and returns the result of the anonymous function. It will be how we can pipe to anonymous functions in Elixir. It is named andThen in Scala and known as then in many promise libraries across ecosystems.

I think this can improve the piping experience considerably while keeping things functional.

w...@resilia.nl

unread,
Dec 29, 2020, 6:31:55 AM12/29/20
to elixir-lang-core
1. tap:
I think adding tap will be very useful. I often am doing something like

```
ast = quote do ... end
IO.puts(Macro.to_string(ast)
ast
```
when debugging macros.
Being able to change this to
```
quote do ... end
|> tap(&IO.puts(Macro.to_string(&1))
```
will be very welcome. :-)

2. then:
What José is referring to (when talking about `then` in relation to other languages where it is also used for e.g. promises) is the monadic 'bind' operation. You might also know it as `>>=` as well as `andThen`:
- `>>=` is the (non-descriptive) symbolic name that is used in Haskell, PureScript, and F# as well as many papers.
- `bind` is the name given to above operation. It is also a frequently-used name whenever an operator is not used. In fact, `bind` is used in the Elixir ecosystem right now. One example that comes to mind is `StreamData`. There are probably others.
- `andThen` sees usage in Scala, Elm and as already mentioned by José in many other contexts whether they deal with only promises, only parsers, only nondeterminism, etc... or monads in general.

Even if it is more verbose, I think the name `andThen` is more descriptive than plain `then`. Therefore I prefer `andThen`.

But rather I'd not add it at all:
This proposed function will only be specialized to the "identity monad".
The bind operation is the place where the unwrapping of the monadic value ought to happen. The identity monad is the one case where there is nothing to unwrap.
`then/2` as described is a function that does nothing over using the function directly, except for "circumventing" the parsing precedence issue of `&` vs `|>` (if you need a refresher, find prior discussion about allowing anonymous functions and captures in pipes here).
`then/2` only exists for improved syntax, not for improved semantics.
The fact that we are specializing it for this single syntactic purpose makes me consider that maybe we'd be better off choosing a different name that does not have this pre-existing meaning attached.

Even if you're unfamiliar with monads or algebraic datatypes in general, you'll be able to understand the problem of restricting a general operation to one specific case.
It's a bit like saying "Let's add a `Kernel.sum/1` that sums (only) lists of integers." It 'works' but what about lists of floats? sets of integers? lists of decimals? etc.
There is a lot of missed potential.
There is a high possibility that a decision like this cannot be extended or altered later on in a backwards-compatible way.
There is a high likelihood of people trying to use it in contexts where it cannot be used and being confused by it or introducing bugs.


So I'd seriously consider using a different naming scheme for `then`.
I'd prefer a simpler name with less of a pre-existing meaning.
Possibly just `fun/2`.


Happy holidays! :-)

~Marten/Qqwy

José Valim

unread,
Dec 29, 2020, 7:31:08 AM12/29/20
to elixir-l...@googlegroups.com
Hi Marten,

Thanks for the feedback!

The only reason I picked "then" is exactly because, if we ever introduce something akin to monads, I wouldn't pick "then" because I don't think it is clear enough on its monadic value. Also note that:

1. "andThen" in Scala is function composition and not bind. bind is flatMap (which I personally prefer)

2. "andThen" in Elm is indeed bind but it is not polymorphic, so you have Task.then, Maybe.then, etc. Therefore, even if we decide to go with "then" in the future, there is no naming conflict, unless we choose a polymorphic monad implementation. In fact, Kernel.then could then be used as an introduction to other monadic modules.

So overall I think we are clear. This will be a bad choice only if all of the below are true:

1. We introduce monads
2. We pick "then" as the name for bind
3. Monads are polymorphic so we are accessing them via an imported "then" instead of qualified per module

Happy holidays!

Louis Pilfold

unread,
Dec 29, 2020, 8:26:46 AM12/29/20
to elixir-lang-core
Hello!

As a data point JavaScript, TypeScript, Rust, Gleam, and Rescript/ReasonML/Bucklescript all use then or and_then as the name for that monadic function in their standard libraries.

It seems to be a fairly common name these days so I'd probably favour using a different one to avoid any confusion.

Cheers,
Louis


José Valim

unread,
Dec 29, 2020, 8:43:09 AM12/29/20
to elixir-l...@googlegroups.com
Thanks Louis!

Quick question: in Gleam, do you have polymorphic dispatch? Or do you have to do Result.then, Option.then, etc? If that's the case, then I think we could argue Kernel.then for identity monad and people can provide their own variants, as in Gleam, if they want to go down this route.

I think then would only be a problem if we want to introduce a polymorphic monadic then in the future in Kernel.

Louis Pilfold

unread,
Dec 29, 2020, 9:02:48 AM12/29/20
to elixir-lang-core
Heya

No polymorphic dispatch, so `result.then` as you've said. I think that is also true of all the languages I've listed there, JavaScript's duck typing aside.

Cheers,
Louis


Paul Clegg

unread,
Dec 29, 2020, 1:11:55 PM12/29/20
to elixir-l...@googlegroups.com
On Tue, Dec 29, 2020 at 1:47 AM José Valim <jose....@dashbit.co> wrote:
I propose we simply add two functions to Kernel: tap and then.

1. tap receives an anonymous function, invokes it, and returns the argument. It can be found in Ruby and .NET Rx.

2. then receives an anonymous function, invokes it, and returns the result of the anonymous function. It will be how we can pipe to anonymous functions in Elixir. It is named andThen in Scala and known as then in many promise libraries across ecosystems.

I'm all for "tap"; I've been writing this function myself for a while now, as easy as it is...  Not as sure about "then", though, I've been able to get this to work without much problem, ala;

iex(30)> foo = &(IO.puts(&1))
&IO.puts/1
iex(31)> "biff" |> (foo).()
biff
:ok
iex(32)> foo = &(&1 <> &2)
#Function<43.97283095/2 in :erl_eval.expr/5>
iex(33)> "biff" |> (foo).("bar")
"biffbar"

The parens + .() combination seem to work just fine without any additional work.  It's just the tap that gets messy if you want to do it "inline" often:

iex(34)> "biff" |> (fn x -> IO.puts("foo"); x; end).()
foo
"biff"

...so for that, I'd love 'tap()' so I don't forget to return the original x.  :D

...Paul






Austin Ziegler

unread,
Dec 29, 2020, 3:23:17 PM12/29/20
to elixir-l...@googlegroups.com
Couldn’t this be a special case of `Kernel.apply/2`?

```elixir
@spec apply(fun | any, list(any) | fun) :: any
def apply(arg, fun) when is_function(fun, 1)) do
  fun.(arg)
end

def apply(fun, args) do
  :erlang.apply(fun, args)
end
```

This would read really well (IMO):

```elixir
[1, 2, 3, 5, 7]
|> apply(&Enum.map(&1, fn x -> x * 2 end))
```

If it’s preferred not to make a special form of apply/2, then perhaps `then_apply/2`?

-a

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


--

Louis Pilfold

unread,
Dec 29, 2020, 3:53:54 PM12/29/20
to elixir-lang-core
Heya

"apply" is the name I first thought of here. In fact I've tried to use apply like this before!

Cheers,
Louis

Zach Daniel

unread,
Dec 29, 2020, 3:59:37 PM12/29/20
to elixir-l...@googlegroups.com
That variant doesn’t really work because what if you want to pass a function as the first argument to what you’re calling? I don’t think muddying the behavior of “apply” is worth the extra confusion. I think either `tap/2` or a `with` Inspect option is the way. People will ultimately need to look this function up anyway, I don’t think the merit of the name is “would I guess this name based on what I want”, because most things on programming fail that litmus test 😂. We just need a memorable/reasonable name.

Austin Ziegler

unread,
Dec 29, 2020, 10:24:00 PM12/29/20
to elixir-l...@googlegroups.com
Using an alternate form of `apply/2` would work, because, while there’s no guard from the Elixir side, the second parameter of `apply/2` must be an array.

```
iex(1)> apply(fn q -> q end, fn -> nil end)
** (ArgumentError) argument error
    :erlang.length(#Function<21.126501267/0 in :erl_eval.expr/5>)
iex(1)> apply(fn q -> q end, [fn -> nil end])
#Function<21.126501267/0 in :erl_eval.expr/5>
iex(2)> apply(& &1, 3)
** (ArgumentError) argument error
    :erlang.length(3)
iex(2)> apply(& &1, [3])
3
iex(3)> apply(& &1, [])
** (BadArityError) #Function<7.126501267/1 in :erl_eval.expr/5> with arity 1 called with no arguments
```

-a

Zach Daniel

unread,
Dec 29, 2020, 11:25:57 PM12/29/20
to elixir-l...@googlegroups.com
Ah, you’re right. Apologies. I think I’d still prefer not to overload it, but you are correct that it wouldn’t conflict.

Reply all
Reply to author
Forward
0 new messages