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 position where 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"