Introducing IEx and Elixir
We'll start off by talking a little bit about the Elixir programming language that Phoenix sits atop. The most important thing to note, especially if you're coming from another programming language such as Ruby, Go, PHP, JavaScript, or Java, is that there is one significant difference between Elixir and the rest: Elixir is a functional language. While this doesn't change everything dramatically, it does introduce a few gotchas that we will need to get out of the way.
Note
If you're already comfortable with Elixir and IEx, feel free to skip this section, as this is intended to be a quick introduction or refresher to the Elixir language and the IEx debugging tool.
Before we dive too far into those gotchas, let's first get acquainted with a tool that is going to make our development process in Elixir much simpler: IEx.
What is IEx?
The good news: Elixir includes a tool called a Read-Evaluate-Print-Loop (REPL), which is essentially an Elixir shell. You can think of this as being similar to an irb in Ruby or the node shell in Node.js. It is a very detailed and helpful tool that we'll be referring to quite a bit in our journey of building this full web app! To open REPL up (assuming you have Elixir installed), run the following command:
$> iex
You should see something similar to the following snippet appear on your screen:
$> iex Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace] Interactive Elixir (1.5.0) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
Now we can start messing around with some sample Elixir code! Let's start off with a couple of base scenarios.
Variables in Elixir
Like most other programming languages, Elixir has a concept of variables. Variables are, simply put, just a place to store values or data. Variables in Elixir do not require any sort of type definition or declaration that they're variables, as you might see in languages such as JavaScript. We can declare a new variable as simply as follows:
iex(1)> greeting = "Hello There" "Hello There"
Note
In IEx, the output of any statement entered is always the next line in the shell.
Variables point to memory locations, so when you use them in your code, the values stored in those memory locations are automatically pulled out for you, as shown in the following example:
iex(2)> greeting "Hello There"
While variables can be reassigned in Elixir, the actual values are immutable! This brings us to our first gotcha in Elixir: immutability.
Immutability in Elixir
The first thing to note is Elixir's concept of immutability. This means that any variables used will retain their value whenever they're passed along. This is a big difference from languages such as Ruby, where the state of any data you pass on is no longer guaranteed to retain its original value. Objects are a good example of this in Ruby.
To demonstrate this concept a little bit better (don't worry about the syntax yet; we'll get to that!), let's take a look at a code snippet in Javascript, as follows:
function upcase_name(p) { p.name = p.name.toUpperCase(); } var person = { name: "Brandon" } // Name here is still 'Brandon' person // Output is { name: "Brandon" } upcase_name(person) person // Oh no! Output is now { name: "BRANDON" }
Oh no! We've just introduced a scenario where calling a function is destructive whether we've intended it to be or not! Let's look at a similar block of code in Elixir in the following example:
upcase = fn p -> Map.put(p, :name, String.upcase(p.name)) end person = %{ name: "Brandon" } # Name here is still 'Brandon' upcase.(person) person # Output for name is still 'Brandon'!
This is likely our intended behavior, and it prevents people from doing wacky things while programming, such as writing functions that end in? (typically used to denote a function that answers a question about the arguments in a Boolean). (Sadly, I've seen this more times than I'd care to admit).
Of course, none of these examples are without their own respective gotchas (and honestly, which programming language nowadays doesn't have its fair share?). For example, the following snippet is considered completely legitimate code in Elixir:
person = %{ name: "Brandon" } person = %{ name: "Richey" }
But, wait! I hear you cry, I thought Elixir's variables are immutable!Well, the answer to that is that they are immutable! Or rather, the memory locations that they point to are; therefore, every time we create a variable we're essentially creating a pointer to a location in memory. In our case, it stores our previous map into some memory location (we'll call it 0 x 01). When we reassign a person, however, we store the value of %{ name: "Richey" }
into a different memory location (we'll call it 0 x 02). This is what is being passed along to our functions when we pass in the person
variable; here, we're basically telling Elixir, Hey, use the value stored in memory location 0 x 01, which is what the variable person is currently pointing at. This means that we don't have to worry about the value in 0 x 01 being changed as long as it is still in use.
Understanding the different types in Elixir
Elixir has a number of different built-in types that we'll be working within the course of building our applications. They are the following:
- Strings (sometimes referred to as binaries)
- Integers
- Floats
- Lists
- Maps
- Keyword lists
- Tuples
- Modules
- Functions (functions themselves break into two separate types: module functions and anonymous functions)
Let's take a look at the various representations of these types and use some of the tools in IEx to understand more about what we're working with.
Getting more information with the i helper
IEx has a really handy helper function available to us: i
. This function comes in two separate arities, i/0
and i/1
(arities, in this case, being the number of arguments that we have to supply to each version of the function). i/0
displays information about the last value output in IEx, whereas i/1
displays information about a particular value or variable. Let's take a look at a few examples:
iex(3)> x = 5 5 iex(4)> i Term 5 Data type Integer Reference modules Integer Implemented protocols IEx.Info, Inspect, List.Chars, String.Chars
Let's talk about what the preceding example is giving us. First, it tells us the Term
, which almost literally means the value itself. Next, we have the Data type
, which tells us what Elixir is classifying this particular value as (in the example, it's an integer). Next, it tells us the Reference modules
, which is a fancy way of telling us what modules we should use to be able to interact with this particular data type. Finally, it gives us a list of implemented protocols. These are additional modules that provide some sort of behavior that we expect that data type to adhere to if it implements a particular behavior. You can think of them as being similar in concept to interfaces in other programming languages.
Getting more information with the h helper
IEx also provides us with another incredibly helpful built-in function: h
. Similar to i
, h
has two arities available for us to use: h/0
, which displays the help page for IEx, and h/1
, which displays the help for a particular module. So, while the following example won't work:
iex(5)> h 5 Invalid arguments for h helper: 5
This following example will:
iex(6)> h Integer Integer Functions for working with integers.
Using i
and h
effectively will go a long way towards helping us understand more of Elixir as we go along!
Using IEx and helpers to understand types
Going back to our integer example, we saw that there were a few different protocols that integers implement that we can take advantage of, but they may not make immediate sense to us. Let's use String.Chars
as an example, where we'll call the h
helper on String.Chars
to learn more about the module from its documentation:
iex(7)> h String.Chars String.Chars The String.Chars protocol is responsible for converting a structure to a binary (only if applicable). The only function required to be implemented is to_string/1, which does the conversion. The to_string/1 function automatically imported by Kernel invokes this protocol. String interpolation also invokes to_string/1 in its arguments. For example, "foo#{bar}" is the same as "foo" <> to_string(bar).
Neat! It's like having a language guide built into our programming environment. It's difficult to stress just how useful this ends up being in practice, especially if you're like me and like doing programming where there's little access to the internet and you need to figure out how to use something like String.Chars
. Reading the previous example, we see one particular snippet:
The only function required to be implemented is to_string/1
, which does the conversion.
Let's dive into that further, again using h/1
, as shown in the following example:
iex(8)> h String.Chars.to_string/1 def to_string(term) Converts term to a string.
The preceding snippet tells us a huge amount about that particular module and function. Based on the description and provided function signature, we can infer that the Integer
module implements a String.Chars
protocol, which that means it needs to implement a to_string/1
function that matches the preceding function signature. Therefore, we can further infer that the way to convert an integer to a string is with the following method:
iex(9)> Integer.to_string(5) "5"
Et voila! We've followed the chain of using i/1
and h/1
to figure out exactly how to perform an operation on a particular data set, as well as what assumptions we can make about the protocols implemented for that particular data type, and so on. Given this particular revelation, let's start expanding on this a little bit more and take a look at some of the other data types that exist in Elixir and a sample of the other operations that we can perform on them:
iex(10)> i 1.0 Term 1.0 Data type Float Reference modules Float Implemented protocols IEx.Info, Inspect, List.Chars, String.Chars
So, if we wanted to represent something with decimal places, we'd use a float. Types in Elixir are inferred, so you don't have to explicitly specify the type for each variable. In addition, you can store any type in the same variable when you reassign it, so something like the following snippet is a perfectly valid operation (despite being very bad practice):
iex(11)> x = 5 5 iex(12)> x = "Hello" "Hello"
If you're coming from a language such as Ruby or JavaScript none of this will be very surprising, but if you're coming from a language that is strongly-typed, this might be a little more off-putting. There are stricter ways to enforce types using tools such as Dialyzer, but in my experience, I've found those to be used relatively rarely. Let's now try using the information helper on a string of data to see what information we get back from IEx. Take a look at the following example:
iex(13)> i "Hello There" Term "Hello There" Data type BitString Byte size 11 Description This is a string: a UTF-8 encoded binary. It's printed surrounded by "double quotes" because all UTF-8 encoded codepoints in it are printable. Raw representation <<72, 101, 108, 108, 111, 32, 84, 104, 101, 114, 101>> Reference modules String, :binary Implemented protocols IEx.Info, Collectable, Inspect, List.Chars, String.Chars
Here, we see that we have a standard string and that there are a lot of the same implemented protocols that we saw on floats and integers as well. We see the same few protocols (as well as a new one, Collectable
). Now, if we want to see the operations provided by one of the reference modules (string in our case), IEx provides another awesome way to get that information out. In our IEx console, we can simply type in String.
(notice the period!) and then hit Tab on our keyboard, for example:
iex(14)> String. Break Casing Chars Normalizer Tokenizer Unicode at/2 capitalize/1 chunk/2 codepoints/1 contains?/2 downcase/1 duplicate/2 ends_with?/2 equivalent?/2 first/1 graphemes/1 jaro_distance/2 last/1 length/1 match?/2 myers_difference/2 next_codepoint/1 next_grapheme/1 next_grapheme_size/1 normalize/2 pad_leading/2 pad_leading/3 pad_trailing/2 pad_trailing/3
printable?/1 printable?/2 replace/3 replace/4 replace_leading/3 replace_prefix/3 replace_suffix/3 replace_trailing/3 reverse/1 slice/2 slice/3 split/1 split/2 split/3 split_at/2 splitter/2 splitter/3 starts_with?/2 to_atom/1 to_charlist/1 to_existing_atom/1 to_float/1 to_integer/1 to_integer/2 trim/1 trim/2 trim_leading/1 trim_leading/2 trim_trailing/1 trim_trailing/2 upcase/1 valid?/1
Let's use h/1
again to get a little more information about a particular string and the operations we can perform on it, as shown in the following snippet:
iex(15)> h String.replace/4 def replace(subject, pattern, replacement, options \\ []) Returns a new string created by replacing occurrences of pattern in subject with replacement. The pattern may be a string or a regular expression. By default it replaces all occurrences but this behaviour can be controlled through the :global option; see the "Options" section below. ## Options • :global - (boolean) if true, all occurrences of pattern are replaced with replacement, otherwise only the first occurrence is replaced. Defaults to true • :insert_replaced - (integer or list of integers) specifies the positionwhere to insert the replaced part inside the replacement. If any position given in the :insert_replaced option is larger than the replacement string, or is negative, an ArgumentError is raised. # ...
That's a lot of information, yes, but it's also all incredibly useful. Let's start with a very simple operation and replace the e
in our greeting variable to an x
. We see that the signature for String.replace/4
is replace(subject, pattern, replacement, options \\ [])
. Given that, let's quickly create a greeting variable and change every e to an x:
iex(1)> greeting = "Hello" iex(2)> String.replace(greeting, "e" ,"x", global: true) "Hxllo"
Your objects have no power here
Another thing that might be a little more difficult to contend with, especially if you're coming from a language where object-oriented programming is treated as a first-class citizen, is that objects are not a thing you can use in Elixir to organize your code or group data and functionality together. Again, let's use JavaScript as an example, as follows:
var greeting = "Hello There" greeting.toUpperCase() # Convert a string to upper case by calling a function on the "greeting" string object # End result: "HELLO THERE"
Here, you'll instead want to get used to the concept of using modules and functions to accomplish the same things. You can think of modules as a collection of related functions that operate on particular sets or types of data, or are otherwise grouped together by some sort of common element. So, the preceding block of code would instead be written as the following in Elixir:
greeting = "Hello There" String.upcase(greeting) # Results in the same "HELLO THERE" message as the Javascript example
This is going to be a very common pattern that you should familiarize yourself, as you will frequently see function called as [Module Name].[Function Name]([Arguments])
. Try to shy away from calling functions on objects and data and instead get used to calling functions with the module name.
Note
You're not required to do this; there are actually ways to shorten this even further through the use of the alias and import statements!