Feature Idea: Explicit `recurse` keyword

suggestion
#1

For example:

def sum(list, sum \\ 0)
def sum([], n), do: n
def sum([head | tail], n) do
  recurse(tail, head + n)
end

The two benefits I can think of are:

1.) Function renaming is a bit easier.
2.) It could get special syntax highlighting, like super, allowing for easy identification of nested recursion.

Often, in complex functions or a file with similarly named functions, identifying where (or if at all) a function is recursive can be difficult.

I don’t think there are any optimizations that having this would offer, but from a linguistics standpoint I think it would be very nice.

Also I’d like to implement it if others think it sounds useful.

#2

I don’t see the advantages being worth an extra language keyword. It also does not handle springboard type of recursion in which function_a calls function_b which calls function_a.

If you want to do this for your own education, I’m totally behind you. But I would not want to see it in the language.

If this is easier for you to read because you’ve spent time with clojure, then be aware that clojure needs it to unroll the recursion into a loop since the JVM has no tail call optimization.

5 Likes
#3

Instead of this you can write: Enum.sum(enumerable) or for more generic solution you could write: Enum.reduce(enumerable, 0, & &1 + &2), so if you want a short form then you can use one of those 2 which are already implemented.

Anyway your example could be written in simple macro, but I don’t believe core team members would like to approve it, because it’s not really needed in Elixir itself. Such simple macro could be written in library as well.

#4

Its not based on experience with clojure (although that is probably where I originally saw the pattern). I do agree that it really only partially solves the problem of easily following the execution path of recursive functions (my contrived example really doesn’t illustrate the main point of confusing/complicated recursive functions) because of the springboard type of recursion you mentioned.

I think from a developer ergonomics standpoint that I’d much rather see an explicit call to recursion, as opposed to just referring to “the current function” by name. Its for the same reason that most style guides recommend referring to the current module as %__MODULE__{}, because it avoids an entire class of errors/mistakes. Consider a file with many functions that share similar names, like the below example (obviously we would never write code like this):

def doa_thing([head | tail]) do
  if some_condition(head) do
    doa_thing(tail)
  else
    do_athing(head)
  end
end

def do_athing(a) do
  ...
end

These sorts of things make it very difficult to read some classes of code.

super is an example of a function that could similarly be manually delegated, or provided by a library but exists in the language itself for ergonomics.

#5

Yeah, the sum example was just chosen out of a hat because its a simple recursion demo. I agree that the real question is “does it provide enough value to be in core”. I think its easy to downplay the value of explicitly stated recursion when looking at trivial examples, but when you look at some things (often used in Ecto’s DSL code, or in DSLs in general), or in code operating over tree structures, the lack of it is pretty noticeable, especially if the names chosen for functions aren’t very friendly.

#6

I completely agree that heavily recursive code can be difficult to follow. However, I find I rarely write that in application code. The vast majority of the time I use abstractions provided by the Enum and Stream modules.

Kernel.defoverridable/1 does add in a super “keyword”. I might could be convinced to have super refer to the current function if it is not overriding something else. Still, I’m not sure I’d use it personally.

#7

I’m also thinking of a kind of interesting use case for it, which is that you could use it in macros to do interesting things with a simple syntax.

defmacro retry(result, args, n) do
  quote do
    case result do
      {:error, error} ->
        if SomeAgent.number_of_attempts() <= unquote(n) do
          recurse(unquote_splicing(args))
        else
          {:error, error}
        end
      {:ok, value} -> {:ok, value}
    end
  end
end

def my_api_call(foo, bar) do
  result = call_api(foo, bar)
  retry(result, [foo, bar], 10)
end
#8

I can definitely see lots of use cases that it doesn’t cover/account for, but to me something like that sounds better than picking apart CALLER and going that route.

#9

I suppose I sort of don’t agree with this. I think complex code can be hard to follow, and if complex code is also recursing a bunch, then that too can be hard to follow. In such cases though I don’t see how a recurse keyword is going to improve that situation much. In cases where the complexity can be properly extracted and the code made clear, I guess I don’t see recursion as itself being a source of confusion.

1 Like
#10

I think I agree that whatever complexity/difficulty arises from writing recursive code could be solved by just writing clearer recursive code. However, I think my corollary of using __MODULE__ as opposed to writing out the current module name is the best example of why I think it would be useful. What would you think about something like this instead?

def sum(list, sum \\ 0)
def sum([], n), do: n
def sum([head | tail], n) do
 __FUNCTION__.(tail, head + n)
end
#11

Maybe, and I think realistically that’d be the way you need to do it, since something like recurse would break any existing code with a pre-existing recurse function.

I think the reason I’d still generally prefer to use the function name itself is that if the function is itself named well, then the call site reads better. __FUNCTION__(tail, head + n) doesn’t really tell me what’s going on, but sum(tail, head + n) does.

3 Likes
#12

Hm…thats an interesting point. I think its more important to know that “this is recursing”. A vast majority of the time I know what function I’m reading/editing when I’m doing it, so if I see __FUNCTION__(tail, head + n) I don’t think I’d get confused. I do see your point though. Hmm…

So one of the things I’ve used recursion heavily for is building the inclusion feature for a JSON API. So you can include relationships via include=foo.bar,foo.baz,buz. All of those records need to be processed fairly significantly, authorized, and manipulated in many ways. In many cases when you recurse is affected by the context of the operation, or in some branches/function heads you might not recurse at all. Being able to easily spot the recursion can significantly help with the mental overhead of determining the happy path.

With all of that said, it does sound like a problem that is solvable by a library for some particularly gnarly recursion, or maybe even by syntax highlighters recognizing recursion? To me it just kind of mirrors the way I think about recursion. I always end up asking myself “where does it recurse” and “in what circumstances does it not continue”, that sort of thing.

#13

You can roll your own :

defmodule Recurse do
  defmacrop recurse(args) when is_list(args) do
    {name, arity} = __CALLER__.function

    if length(args) != arity do
      raise ArgumentError, "recursing over different arity" 
    end

    quote do
      unquote(name)(unquote_splicing(args))
    end
  end

  def sum(list, sum \\ 0)
  def sum([], n), do: n
  def sum([head | tail], n) do
    recurse([tail, head + n])
  end
end

IO.puts Recurse.sum([1, 2, 3])

I am using a list because we don’t have varargs but someone could also define multiple arities, you would only need from 0 up to 255 :smile:. And I am out before the varargs discussion starts. :stuck_out_tongue:

6 Likes
#14

Unless you meta-program your macros, of course:

defmodule Recurse do
  for arity <- 0..15 do
    outer_args = Macro.generate_arguments(arity, __MODULE__)

    defmacro recurse(unquote_splicing(outer_args)) do
      inner_args = unquote(outer_args)

      case __CALLER__.function do
        {name, unquote(arity)} ->
          quote do
            unquote(name)(unquote_splicing(inner_args))
          end

        {_name, _arity} ->
          raise ArgumentError, "recursing over different arity" 
      end
    end
  end
end

defmodule Foo do
  import Recurse

  def sum(list, sum \\ 0)
  def sum([], n), do: n
  def sum([head | tail], n) do
    recurse(tail, head + n)
  end
end

IO.puts Foo.sum([1, 2, 3])

Please don’t do this.

7 Likes
#15

Not to worry, I have no intention of doing anything like that :slight_smile: To me it’s one of those patterns that does more harm as a library than good. It only really adds value if it becomes the standard to use that keyword. For the same reasons that elm removed custom operators really. IMO, it would save a fair amount of cycles when reading recursive code and that has a lot of value. I definitely see arguments either way though, and don’t see any reason to push it :slight_smile:

1 Like
#16

Yo dawg…

4 Likes
#17

If something has value as a language construct it definitely has value as an in-house tool. If the value of this somehow is non-existent in the small I’m not sure it’d be valuable in the large. My personal thoughts are that it’s certainly not useless, but I’d likely never use it. I imagine it’d be different if I was personally more involved in / influenced by Clojure.

#18

I think for a large team/code base, deviating from standards and best practices, even in ways that that feel generally useful, have consequences in the long run. Every deviation increases the cost of turn over, and makes context switching between projects that much more difficult. It’s not that I don’t see value in it as a utility, just that I don’t want to eventually be running my own twisted flavor of elixir with unrecognizable design patterns, at least for the sake of the person who has to maintain my code.

1 Like
#19

Disclaimer: The following post is full of opinions.

Define large team and large code base. I think it’s pretty diffuse. I would assume a ton of Erlang/Elixir teams are bigger than they ought to be.

I think the standard/best practice is to write the simplest / most understandable code. Sometimes that requires a macro. Even with functions you’re effectively defining your own sub-language. I think the “shun macros at all costs” attitude is surprisingly widespread and fairly counter-productive. It’s the “abstinence” of programming. Most reasonable people realize that it’s not the macros/sex that’s the problem, but rather the irresponsible use of them.

Turn over is expensive. The solution isn’t to optimize for turn over, but rather have a code base that people don’t actually want to leave because it refuses to use abstraction.

Erlang/Elixir is to me, fundamentally, about building a system of some kind. That system will have assumptions, behaviors and a whole heap of other things that are effectively unique to the very system you’re building. Having syntax-level abstractions that help you express those assumptions and behaviors is something I personally view as a tool for writing the best version of those expressions.

Lowest common denominator code will eventually come back to bite you when people feel trapped and want to leave because they can’t use the tools at their disposal, or when things that could be clearly expressed with a small DSL are now suddenly 10x the amount of LoC.

I find it refreshing to see communities that don’t have a tendency to write for the lowest common denominator, because it means that people are forced to advance and not simply accept mantras like “never write macros”, etc…

1 Like
#20

What bothers me about this is that it feels to me like there’s an underlying belief that recursion needs to be “more explicit” because it is “unusual”. In fact, it is not at all unusual, and thinking of it as something mysterious and advanced is a mindset inculcated by learning programming from languages with weak abstraction capabilities (e.g. any variant of BASIC).

2 Likes