Recursion in Elixir
Intro
I recently wrote an article on Recursion in Ruby, and this is meant to be its Elixir counterpart. It will provide a way to compare solving the same problems in both languages and a chance to talk about some of their differences.
Heads & Tails
What would normally be an array
in other languages is a list
in Elixir. A list is actually represented as a linked list. A linked list is a pretty simple concept... you have the head
of the list, which is your actual data: A number, a string, etc... and you have the tail
, which points to the rest of your list.
list = [1,2,3,4,5]
Looking at this list, the head is 1
, and the tail is [2,3,4,5]
. This is an important concept in languages which use recursion a lot, because you'll want to process the head of the list, and then re-run the same function on the tail of it.
Pattern matching
Pattern matching is another key aspect of Elixir (and functional programming languages in general). It allows you to assign variables based on patterns.
list = [1,2,3,4,5][head | tail] = list
This as you might guess takes the head of the list and puts it into the head variable, and the tail of the list into the tail variable.
Your method arguments can also have patterns like this in them:
def myfunc([head | tail]) do# somethingendmyfunc([1,2,3,4,5])
Again it goes a step further in Elixir where you can have multiple definitions of the same method, and it will choose the appropriate one to call based on which pattern matches correctly.
def myfunc([head | tail]) do# we have head and tailenddef myfunc([]) do# we have empty listendmyfunc([1,2,3,4,5])myfunc([])
Ending recursion
In Ruby I had to have an if
statement to help determine when to stop recursion... something to check if the array was empty or some other clause to break the recursion.
In Elixir you can accomplish this by providing different method definitions... one definition will stop your recursion (an empty list []), while another will perform the work and continue the recursion.
If you were summing the values in a list, you could end the recursion by returning 0 when there is an empty array.
def myfunc([]) do0end
Tail call optimization
Tail call optimization is a technique that allows the compiler to call a function without using any additional stack space. This is achieved by making the function end (or return) with a call to another function... in this case itself because we are focusing on recursion.
A method that is not tail call optimized:
def sum_no_tail([]) do0enddef sum_no_tail([head | tail]) dohead + sum_no_tail(tail)endsum_no_tail([1,2,3,4,5]) # 15
Now the same thing written in a tail optimized form:
def sum(list, acc \\ 0)def sum([], acc) doaccenddef sum([head | tail], acc) dosum(tail, acc + head)endsum([1,2,3,4,5]) # 15
Setting default values
In Elixir you can set default argument values by declaring a method without a body, utilizing the format arg \\ value
to denote what the default value is.
def sum(list, acc \\ 0)
Examples
Factorials
Factorials in math are written like 6!
and are the result of 6 * 5 * 4 * 3 * 2 * 1
.
defmodule MyRecur dodef fact(num, acc \\ 1)def fact(0, acc) doaccenddef fact(num, acc) dofact(num - 1, acc * num)endendMyRecur.fact(6) # 720
Fibonacci
I heard a funny definition of Fibonacci that goes like this: "Fibonacci - A problem used to teach recursion in computer science." The first 10 Fibonacci numbers go like this: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 - The next number is the sum of the previous 2 numbers. While not the most efficient Fibonacci solution, it'll do the trick when the goal is to demonstrate recursion!
defmodule MyRecur dodef fib(n) when is_integer(n) and n > 1 domap(Enum.to_list(1..n), &fibn(&1))enddefp fibn(n, current \\ 0, next \\ 1)defp fibn(0, current, _next), do: currentdefp fibn(n, current, next), do: fibn(n - 1, next, current + next)endMyRecur.fib(10) # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Summing List
defmodule MyRecur dodef sum(list, acc \\ 0)def sum([], acc) doaccenddef sum([head | tail], acc) dosum(tail, acc + head)endendMyRecur.sum([1,2,3,4,5]) # 15
Mapping List
defmodule MyRecur dodef map(list, f, acc \\ [])def map([head | tail], f, acc) domap(tail, f, acc ++ [f.(head)])enddef map([], _f, acc) doaccendendMyRecur.map([1,2,3,4,5], fn (elem) -> elem * elem end)# [1, 4, 9, 16, 25]
Reducing List
defmodule MyRecur dodef reduce([head | tail], acc, f) doreduce(tail, f.(head, acc), f)enddef reduce([], acc, _f) doaccendend# JoinMyRecur.reduce(["A", "AB", "ABC"], "", fn(elem, "") -> elem(elem, acc) -> "#{acc} #{elem}"end)# "A AB ABC"# SumIO.inspect MyRecur.reduce([1,2,3,4,5], 0, fn (acc, elem) -> acc + elem end)# 15# Longest StringMyRecur.reduce(["A", "AB", "ABC"], "",fn (acc, elem) ->if String.length(acc) > String.length(elem) doaccelseelemendend)# "ABC"# CountMyRecur.reduce([1,2,3,4,5], 0, fn (_elem, acc) -> acc + 1 end)# 5# MapMyRecur.reduce([1,2,3,4,5], [], fn (elem, acc) -> acc ++ [elem * elem] end)# [1, 4, 9, 16, 25]