004: Exploring Functions, Data Structures, and Strings in Elixir
The anonymous function add is stored in a variable. Arguments appear on the left of -> and code on the right, delimited by fn and end. A dot (.) between the variable and parentheses invokes the function. Anonymous functions are categorized by argument number and access the scope of their definition. Elixir uses square brackets for lists, whose head is the first element and the tail is the rest. Single and double quotes indicate characters and strings accessed via hd/1 and tl/1. Tuples use curly brackets, also holding any value. As immutable structures, every operation returns a new tuple. The original is unchanged, as seen with put_elem/3.
square = fn(x) -> x * x end | |
# calling an anonymous function uses a period | |
# before the parentheses | |
4 = square.(2) | |
# pattern matching the arguments can be used | |
first = fn([head|rest]) -> head end | |
1 = first.([1, 2]) | |
# anonymous functions are commonly used as arguments | |
# to other functions | |
[1, 4, 9] = Enum.map([1, 2, 3], square) | |
[3, 4, 5] = Enum.map([1, 2, 3], fn(x) -> x + 2 end) |
Elixir functions are identified by their name and arity, representing the number of arguments they take. For instance, trunc/1 denotes a function named trunk that takes one argument. At the same time, trunc/2 refers to a different function with the same name but two arguments.
defmodule Examples do | |
# basic defintion | |
def do_stuff( params ) do | |
"result" | |
end | |
#shorthand syntax | |
def shorthand(), do: "result" | |
# defp is for private functions | |
defp private_method, do: "private" | |
# params can pattern match | |
def match_this(%{:key => value}), do: value | |
# the first matching function is called (order matters) | |
def test(test), do: "matches any param" | |
def test([]), do: "never matched" | |
end |
The Elixir shell includes the h function for accessing documentation. You can also use h with the module+function syntax to look up anything, including operators.
All functions in the Kernel module are imported into the namespace. Elixir has true and false boolean values, providing predicate functions for type checking. Atoms are constants whose values are their names, used to state the operation's state with values like :ok and :error.
{:ok, 1, "a"} | |
# pattern match | |
{:ok, result} = {:ok, "good"} | |
# this raises a match error | |
{:ok, result} = {:ok, "good", "one more"} | |
# empty tuple | |
{} |
What is the difference between lists and tuples? Lists are stored in memory as linked lists, meaning that each element holds its value and points to the following element until the end of the list is reached. This means accessing the length of a list is a linear operation: we need to traverse the whole list to figure out its size. Tuples, but are stored in memory. This means getting the tuple size or accessing an element by an index is fast. Yet, updating or adding elements to tuples is expensive because it requires creating a new tuple in memory. For instance, when you update a tuple, all entries are shared between the old and the new tuple, except for the access that has been replaced. In other words, tuples and lists in Elixir can share their contents. This reduces the amount of memory allocation the language needs to perform. Those performance characteristics dictate the usage of those data structures.
Elixir allows you to skip the leading `:` for the atoms `false`, `true`, and `nil.` Double quotes delimit strings in Elixir, and they are encoded in UTF-8. Elixir also supports string interpolation. Strings can have line breaks in them. You can introduce them using escape sequences. You can print a String using the `IO.puts/1` function from the `IO` module. Notice that the `IO.puts/1` function returns the atom `:ok` after printing. Strings in Elixir are represented internally by contiguous sequences of bytes known as binaries. Notice that the number of bytes in that String is 6, even though it has 5 graphemes. That's because the grapheme "ö" takes 2 bytes to be represented in UTF-8. We can get the actual length of the String, based on the number of graphemes, by using the `String.length/1` function.
Elixir also provides anonymous functions. Anonymous functions allow us to store and pass executable code around like an integer or a string.
Code
Log Level
Source: Log Level in Elixir on Exercism
You are running a system comprising a few applications producing many logs. You want to write a small program to aggregate those logs and label them according to their severity level. All applications in your system use the same log codes, but some legacy applications don't support all the codes.
defmodule LogLevel do | |
def to_label(level, legacy?) do | |
cond do | |
level == 0 and !legacy? -> :trace | |
level == 1 -> :debug | |
level == 2 -> :info | |
level == 3 -> :warning | |
level == 4 -> :error | |
level == 5 and !legacy? -> :fatal | |
true -> :unknown | |
end | |
end | |
def alert_recipient(level, legacy?) do | |
cond do | |
to_label(level, legacy?) in [:error, :fatal] -> :ops | |
to_label(level, legacy?) in [:unknown] and legacy? -> :dev1 | |
to_label(level, legacy?) in [:unknown] -> :dev2 | |
true -> false | |
end | |
end | |
end |
The code defines a " LogLevel " module in the Elixir programming language. The module contains two functions, "to_label" and "alert_recipient".
The "to_label" function takes two arguments, "level" and "legacy?". The function uses a "cond" statement, a form of conditional expression in Elixir, to map the input "level" to a specific label. The mapping is as follows:
If "level" is 0 and "legacy?" is not true, the label returned is ":trace".
If "level" is 1, the label returned is ":debug".
If "level" is 2, the label returned is ":info".
If "level" is 3, the label returned is ":warning".
If "level" is 4, the label returned is ":error".
If "level" is 5 and "legacy?" is not true, the label returned is ":fatal".
If none of the above conditions are met, the label returned is ":unknown".
The "alert_recipient" function takes two arguments, "level" and "legacy?" and uses a "cond" statement to determine who should be notified based on the label returned by the "to_label" function. The mapping is as follows:
If the label returned by "to_label" is ":error" or ":fatal", the recipient returned is ":ops".
If the label returned by "to_label" is ":unknown" and "legacy?" is true, the recipient returned is ":dev1".
If the label returned by "to_label" is ":unknown" and "legacy?" is not true, the recipient returned is ":dev2".
If none of the above conditions are met, the recipient returned is "false".
Darts
Source: Darts in Elixir on Exercism
defmodule Darts do | |
@type position :: {number, number} | |
@doc """ | |
Calculate the score of a single dart hitting a target | |
""" | |
@spec score(position) :: integer | |
def score({x, y}) do | |
cond do | |
x * x + y * y > 100 -> 0 | |
x * x + y * y > 25 -> 1 | |
x * x + y * y > 1 -> 5 | |
true -> 10 | |
end | |
end | |
end |
The darts module has code for scoring a dart throw. The function "score/1" takes a position argument and returns an integer, representing the dart's score. The position is a two-element tuple of numbers defined by the @type annotation. The @doc attribute provides documentation for the function, while the @spec annotation specifies the argument and return types.
Using pattern matching, the score/1 function extracts the x and y coordinates from the position tuple. It then calculates the score using the cond control structure, which evaluates each condition and returns the corresponding score when the first condition is true.
The first condition checks whether the dart is outside the dartboard, assigning a score of 0. The second and third conditions check whether the dart is in the outer or middle circle, with scores of 1 and 5, respectively. The final condition assigns a score of 10 to the innermost circle.
Resources
The `with` statement in Elixir is a valuable tool for addressing the problem of handling errors in functions chained together with the pipe operator. `With` allows for more flexibility and control in handling errors and better readability, as it allows for matching intermediary results. Furthermore, variables on the left side must use `{:ok, X}` format to indicate success. Otherwise, errors will be treated as matches.
Elixir as a (non-)Functional Programming Language | by Developing | Medium
Ruby and Elixir can implement a singleton class but have different internal workings. The author of the text argues that learning Elixir to learn functional programming is misguided, as Elixir lacks many of the protections and benefits of a more traditional available programming language. Instead, the author suggests that Elixir is better suited to handle concurrency and asynchrony.