Thursday, September 29, 2016

Elixir Destructuring

Break it all apart

Welcome to the fourth installment of our work with Strings. As usual here's a reminder of the original problem we were trying to solve:

take_prefix.("Mr. John", "Mr. ")
# returns 'John'

We want to be able to chop off some prefix and return the suffix of a string.

So far we've come up with three implementations, each one improving upon each other. Our initial implementation:

# slow bc of multiple String.lengths
take_prefix = fn full, prefix ->
  base = String.length(prefix)
  String.slice(full, base, String.length(full) - base)

IO.puts take_prefix.("Mr. John", "Mr. ")

The second implementation used ranges:

# replace one of our slow length call with a range
take_prefix = fn full, prefix ->
  base = String.length(prefix)
  String.slice(full, base..-1)

IO.puts take_prefix.("Mr. John", "Mr. ")

The third implementation used binary functions due to their constant speed regardless of the size of the given string.

take_prefix = fn full, prefix ->
  base = byte_size(prefix)
  binary_part(full, base, byte_size(full) - base)

IO.puts take_prefix.("Mr. John", "Mr. ")

The final improvement is really more aesthetics. We can make this solution a bit more functional and feel more Elixir-y by using a concept known as destructuring.

Destructuring is a way of taking a complicated data structure and breaking it apart into simplier components. Quick example:

[a, b, c] = [1, 2, 3]
# a now has the value 1
# b now has the value 2
# c now has the value 3

In this example, we took the array holding three numbers and broke it apart into it's separate elements. This is destructuring in a nutshell. Taking something and breaking it apart.

Here's another example using tuples (a data structure holding elements that are contiguous in memory).

{status, status_message} = {:ok, "Success"}

Sometimes our complex object that we want to break apart has information we don't care about. You might think you could do something like this:

# this will not work!
{status, status_message} = {:ok, "Success", "Junk"}
#** (MatchError) no match of right hand side value: {:ok, "Success", "Junk"}

Elixir thinks you messed up, so you have to be very explicit in telling it that you don't care about the last match. You do this by using an underscore.

{status, status_message, _} = {:ok, "Success", "Junk"}

Now this will work. Alright, I think we have all the tools we need to understand the final solution.

take_prefix = fn full, prefix ->
  base = byte_size(prefix)
  # this is the destructuring
  <<_::binary-size(base), rest::binary>> = full

IO.puts take_prefix.("Mr. John", "Mr. ")

Here we are destructuring the full string into two binary components. The << and >> signify that this is a binary data structure, in this case with two elements. The first element will contain a binary string that is the binary size of the prefix and the second element is the rest of the string. Let's get into a bit more detail.

Remember what we saw last week, a String is just a binary string in disguise. So what we are doing here is figuring out the number of bytes used in the prefix (so we know how much to chop off). Then we destructure the full string into two binary parts. The first binary part is the number of bytes that we calculated before (but now represented as the size in binary due to us wanting to destructure this into a binary data structure). And the second part is the actual string that we want to return, represented as a binary data structure. But as we saw last week, a String is just a UTF-8 encoded binary so returning this is just fine.

Destructuring is a powerful technique and is very useful in producing short and concise code. Elixir is not the only language that has this feature. ECMAScript 2016 has it as well.

With that, this concludes our multi-post demonstration on a simple problem I found from the Elixir docs, and how they were able to iterate through a few solutions until the settled on their favorite implementation. I felt like their explanation didn't go into enough detail, which is why we've been looking at each a bit more closely these past few weeks. Hopefully you learned something and had fun while doing it.

Next week, we'll discover and discuss a new topic that I haven't yet decided on yet :)

No comments: