Recursion Issue

Hi,

I am attempting to solve a problem in Exercism “Protein Translation” and implemented the following code. I want the split(tail, final_list) to recursively add the 3 char to final_list. What am I doing wrong and how can I improve the code.

defmodule PT do

  def rna(string) do
    split(string, [])
  end

  def split(string, final_list) when string == "", do: final_list

  def split(string, final_list) do
    splitting = String.split_at(string, 3)
    [head | tail] = Tuple.to_list(splitting)

    final_list ++ [head]
    split(tail, final_list)
  end
end

i am getting this error:

iex(21)> PT.rna("Owaissdui")
** (FunctionClauseError) no function clause matching in String.Unicode.next_grapheme_size/1    
    
    The following arguments were given to String.Unicode.next_grapheme_size/1:
    
        # 1
        ["issdui"]
    
    Attempted function clauses (showing 10 out of 19263):
    
        def next_grapheme_size(<<13::integer(), 10::integer(), rest::binary()>>)
        def next_grapheme_size(<<"\r"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"\n"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇰"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇱"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇲"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇳"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇴"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇵"::binary(), rest::binary()>>)
        def next_grapheme_size(<<"󠇶"::binary(), rest::binary()>>)
        ...
        (19253 clauses not shown)

Thanks

One issue that jumps out right away is that you are not binding the expression:

final_list ++ [head]

to any variable (in order to capture the new value).

3 Likes

You might simplify with

def split("", final_list), do: final_list

We live in an immutable world :slight_smile:

final_list is not changing…

2 Likes

You are getting the error because tail is a list, but you are passing it to split/2 which expects the first argument to be a string. split/2 takes tail and tries to call String.split/2, which then throws an error because it expects its first argument to be a string, not a list.

If you simply want to split a string into lists of three characters, I can think of ways that are simpler, but using this approach I would suggest:

  def split("", final_list), do: Enum.reverse(final_list)

  def split(string, final_list) do
    {first_three_characters, rest_of_word} = String.split_at(string, 3)
    split(rest_of_word, [first_three_characters | final_list])
  end
4 Likes

You are right, but here am just appending the head to final list right or am doing something wrong ?

Wont it just return the value as we that in elixir the last expression of a function gets returned automatically. I might be wrong, can you kindly advice ?

you are absolutely right and it’s such a dumb mistake to make. A very solid way of using pattern matching. One question, why you have used a tuple and why not a list

    {first_three_characters, rest_of_word} = String.split_at(string, 3)

Is it because we have fixed arguments or is there any other reason as well?

Also, in this case

[first_three_characters | final_list]

it’s concatenating a string with a list, how it actually works?

String.split/2 returns a tuple, so only a tuple will match on the result returned by that function.

I am prepending the string to an accumulated list of strings. This is a common practice as prepending to a list is more efficient than appending to one. That is also why Enum.reverse/1 is called at the end, because prepending ends up generating a list of items in reverse that needs to be reversed again to preserve the original order.

2 Likes

Thank you for such a detailed response.

I think reading this section in the docs on the ‘left hand copy’ of the ++ operator could clarify why prepending is faster, for future reference.

1 Like

In this case, the last expression in your function is

split(tail, final_list)

which calls split/2 using the same value for final_list that was passed to the function originally. You have already discussed that tail is causing a type error in this case, but regardless, the result of your list concatenation operation is simply thrown away.

1 Like