Exploring Functions, Data Structures, and Strings in Elixir
Elixir uses square brackets for lists. The head of a list is the first element, and the tail contains the rest of the elements.
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)
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
{:ok, 1, "a"}
# pattern match
{:ok, result} = {:ok, "good"}
# this raises a match error
{:ok, result} = {:ok, "good", "one more"}
# empty tuple
{}
The anonymous function `add` is stored in a variable.
Arguments appear to the left of `->`, and the code appears to the right, enclosed by `fn` and `end`. A dot (`.`) between the variable and parentheses invokes the function.
Single and double quotes are used for characters and strings, respectively, and can be accessed via `hd/1` and `tl/1`. Tuples use curly brackets and can contain any value.
As immutable structures, tuples return a new tuple for every operation. The original tuple remains unchanged, as demonstrated with `put_elem/3`.
All functions in the Kernel module are imported into the namespace. Elixir has boolean values `true` and `false`, and provides predicate functions for type checking. Atoms are constants whose values are their names, such as `:ok` and `:error`.
Difference Between Lists and Tuples
Lists are stored in memory as linked lists, where each element points to the next until the end is reached. This means accessing the length of a list is a linear operation because the entire list must be traversed to determine its size.
Tuples, yet, are stored in memory differently. Retrieving the size or accessing an element by index is fast. But, updating or adding elements to a tuple is costly because it involves creating a new tuple in memory.
When updating a tuple, all entries are shared between the old and new tuples except for the updated entry. This means tuples and lists in Elixir can share their contents, reducing memory allocation needs. These performance characteristics influence how these data structures are used.
Elixir allows you to omit the leading `:` for the atoms `false`, `true`, and `nil.` Strings in Elixir are delimited by double quotes and encoded in UTF-8.
Elixir supports string interpolation, and strings can include line breaks, which were introduced using escape sequences. To print a string, use the `IO.puts/1` function from the `IO` module. Note that `IO.puts/1` returns the atom `:ok` after printing.
Strings in Elixir are represented as contiguous sequences of bytes known as binaries. For example, a string with the grapheme "ö" takes 2 bytes in UTF-8, so a string with 5 graphemes may have a byte count of 6. To get the actual length of a string based on graphemes, use the `String.length/1` function.
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.