🚀 Elixir Introduction

🎯 Complete Definition

Elixir is a dynamic, functional language designed for building scalable and maintainable applications. It runs on the Erlang VM (BEAM), known for low‑latency, distributed, and fault‑tolerant systems. Created by José Valim in 2012, Elixir combines Ruby‑like syntax with Erlang’s industrial‑grade capabilities. It embraces immutability, pure functions, and the actor model for concurrency.

🔬 Core Characteristics

  • Functional: Immutable data, first‑class functions, no implicit `this` or `self`.
  • Concurrency (Actor model): Lightweight processes (not OS threads) communicate via message passing.
  • Fault‑tolerant: “Let it crash” philosophy with supervisors and OTP.
  • Metaprogramming: Hygienic macros (compile‑time AST transformation).
  • Polyglot: Seamless interoperability with Erlang libraries.
  • Tooling: Built‑in build tool (Mix), package manager (Hex), and interactive shell (IEx).
  • Real‑time: Used for low‑latency web apps (Phoenix framework) and embedded systems.

📊 Industry Usage

Elixir powers high‑traffic websites (Pinterest, Discord, Bleacher Report), messaging systems, IoT platforms, and financial services. The BEAM VM can handle millions of concurrent connections with minimal resource usage. Companies like WhatsApp (Erlang) and Discord (Elixir) rely on its robustness.

IO.puts("💧 Elixir 1.17 - CodeOrbitPro Pro Track") # Interactive Elixir (IEx) would show: # iex> "Hello" <> " " <> "world" # "Hello world" # Simple functional style list = [1, 2, 3] double = Enum.map(list, fn x -> x * 2 end) IO.inspect(double) # [2, 4, 6]

📊 Basic Types

🎯 Complete Definition

Elixir types are all immutable. The basic categories include integers, floats, booleans, atoms, strings, lists, tuples, and binaries. Types are inferred dynamically but with strong typing (no implicit coercion). The is_* guard functions allow runtime checks.

🏷️ Type categories

  • Integer: Arbitrary precision (big integers).
  • Float: 64‑bit double precision; 1.0.
  • Boolean: true and false (actually atoms).
  • Atom: Constant whose value is its name (:ok, :error).
  • String: UTF‑8 binary, written "hello".
  • List: Linked list, e.g. [1,2,3].
  • Tuple: Fixed‑size contiguous memory, e.g. {:ok, value}.
  • Binary: Sequence of bytes, <<1,2,3>>.

🔧 Special types

Ranges (1..10), Keyword lists ([{:name, "val"}] or [name: "val"]), Maps (%{key: value}), and Functions (fn x -> x end).

# integers, floats a = 42 b = 3.14 # atoms status = :success # booleans (atoms :true / :false) is_elixir_cool = true # strings name = "Elixir" # lists list = [1, 2, 3] # tuples tuple = {:ok, "result"} # maps map = %{language: "Elixir", year: 2012} IO.inspect({a, b, status, is_elixir_cool, name, list, tuple, map})

🔢 Operators

🎯 Complete Definition

Operators in Elixir are either built‑in (like arithmetic) or defined as macros (like |>). Because everything is immutable, operators never mutate; they return new values. Important operators include the pipe |>, concatenation <> for strings/binaries, and the match operator = (which performs pattern matching, not assignment).

📋 Operator Categories

  • Arithmetic: +, -, *, / (returns float), div/2, rem/2.
  • Comparison: ==, !=, === (strict), !==, <, >, <=, >=.
  • Logical: and, or, not (strict, require boolean). &&, ||, ! (allow any type).
  • Binary: &&& (AND), ||| (OR), ^^^ (XOR), <<<, >>>.
  • Pipe: |> passes left result as first argument to right function.
  • String: <> concatenation.
  • In: in checks membership (1 in [1,2,3]).
# arithmetic IO.puts(10 + 5) # 15 IO.puts(10 / 3) # 3.3333333333333335 # strict vs loose equality IO.puts(1 == 1.0) # true IO.puts(1 === 1.0) # false # logical operators true and false # false nil || 42 # 42 # pipe operator list = [1, 2, 3, 4] result = list |> Enum.filter(fn x -> rem(x,2)==0 end) |> Enum.sum() IO.puts(result) # 6 # concatenation "Hello" <> " " <> "Elixir" # "Hello Elixir"

🔀 Pattern Matching

🎯 Complete Definition

Pattern matching is the cornerstone of Elixir. The = operator is the match operator, which attempts to match the right‑hand side against the left‑hand pattern. If successful, variables on the left are bound. Matching can destructure tuples, lists, maps, and structs. It is used extensively in function clauses, case, and with.

🔍 Matching contexts

  • Variable binding: x = 1 binds x to 1.
  • Tuple destructuring: {:ok, data} = {:ok, 42} binds data.
  • List matching: [head | tail] = [1,2,3] gives head=1, tail=[2,3].
  • Map matching: %{key: value} = %{key: 10} binds value=10.
  • Pin operator ^: Use existing variable's value in pattern.
# basic match x = 5 {x, y} = {10, 20} IO.puts(x) # 10 (rebound) IO.puts(y) # 20 # pin operator z = 3 ^z = 3 # matches, but doesn't rebind # ^z = 5 # would raise MatchError # list pattern [a, b, c] = [1, 2, 3] # head/tail [first | rest] = [1, 2, 3, 4] IO.puts(first) # 1 IO.inspect(rest) # [2,3,4] # map pattern %{lang: lang} = %{lang: "Elixir", rank: 1} IO.puts(lang) # Elixir

🔄 Control Flow

🎯 Complete Definition

Control flow in Elixir relies on pattern matching and recursion rather than traditional loops. Constructs include case, cond, if/unless, and with (for chaining happy paths). All branches must return compatible types; there is no early return except inside functions.

🏗️ Control Structures

  • case – pattern match against multiple clauses.
  • cond – series of conditions (like else‑if).
  • if / unless – also available as inline if condition, do: ....
  • with – combines pattern‑matching steps; fails fast if any mismatch.
# case case File.read("unknown.txt") do {:ok, content} -> IO.puts(content) {:error, reason} -> IO.puts("Error: #{reason}") end # cond score = 85 grade = cond do score >= 90 -> "A" score >= 80 -> "B" true -> "C" # always matches end IO.puts(grade) # if/unless age = 20 if age >= 18 do IO.puts("Adult") else IO.puts("Minor") end # with with {:ok, data} = File.read("config.json"), {:ok, parsed} = JSON.decode(data) do parsed else error -> {:error, error} end

⚙️ Functions

🎯 Complete Definition

Functions are first‑class citizens. Named functions are defined inside modules with def / defp (private). Anonymous functions use fn ... end. Elixir supports guard clauses, default arguments, and multiple clauses via pattern matching. Functions are identified by name and arity (count/1).

🏗️ Function Features

  • Named functions: def add(a, b), do: a + b
  • Multiple clauses: def fact(0), do: 1; def fact(n), do: n * fact(n-1)
  • Guard clauses: def max(a, b) when a >= b, do: a
  • Default values: def greet(name \\ "World")
  • Anonymous: sum = fn (x,y) -> x + y end
  • Capture: &(&1 + &2) shorthand.
defmodule Math do def square(x), do: x * x def fact(0), do: 1 def fact(n) when n > 0, do: n * fact(n-1) def divide(_, 0), do: {:error, "div by zero"} def divide(a, b), do: {:ok, a / b} end IO.puts(Math.square(5)) # 25 IO.puts(Math.fact(5)) # 120 IO.inspect(Math.divide(10, 0)) # {:error, "div by zero"} # anonymous function add = fn a, b -> a + b end IO.puts(add.(3,4)) # 7 # capture double = &(&1 * 2) IO.puts(double.(5)) # 10

📋 Collections

🎯 Complete Definition

Collections include lists (linked), tuples (ordered, fixed‑size), maps (key‑value), and keyword lists (proplists). All are immutable. Operations return new collections. Lists are efficient for prepend/head‑tail recursion; tuples for small fixed data; maps for associative arrays.

🏗️ Collection Details

  • List: [1,2,3] – head/tail access, concatenation ++, difference --.
  • Tuple: {:ok, value} – indexed access via elem(t, index), put_elem.
  • Map: %{a: 1, b: 2} or %{"key" => value} – fast key lookup, update with %{map | key: new}.
  • Keyword list: [name: "Elixir"] – list of tuples, allows duplicate keys, often used for options.
# lists list = [1, 2, 3] new_list = [0 | list] # [0,1,2,3] [head | tail] = list # head=1, tail=[2,3] # tuples point = {10, 20, 30} IO.puts(elem(point, 1)) # 20 # maps map = %{name: "Elixir", year: 2012} IO.puts(map.name) # Elixir updated = %{map | year: 2024} IO.inspect(updated) # keyword list opts = [if_exists: :update, timeout: 5000] IO.inspect(opts[:timeout]) # 5000

🔑 Enum & Stream

🎯 Complete Definition

Enum module provides eager, polymorphic functions for working with collections (anything enumerable). Stream provides lazy, composable enumerables that avoid intermediate lists and can work with infinite collections. Both follow the functional pipeline style.

🔥 Common functions

  • map/2, filter/2, reduce/3, each/2
  • sort/1, uniq/1, join/2
  • any?, all?, member?
  • chunk_every/2, zip/2
  • Stream functions: Stream.map/2, Stream.filter/2, Stream.cycle/1
list = [1, 2, 3, 4, 5] # Enum eager squares = Enum.map(list, fn x -> x * x end) evens = Enum.filter(list, &(rem(&1, 2) == 0)) sum = Enum.reduce(list, 0, &+/2) IO.inspect(squares) # [1,4,9,16,25] IO.inspect(evens) # [2,4] IO.puts(sum) # 15 # Stream lazy (computation only when consumed) stream = list |> Stream.map(&(&1 * &1)) |> Stream.filter(&(&1 > 10)) result = Enum.to_list(stream) IO.inspect(result) # [16,25] # infinite stream naturals = Stream.iterate(1, &(&1+1)) first_ten = naturals |> Enum.take(10) IO.inspect(first_ten) # [1,2,3,4,5,6,7,8,9,10]

📝 Strings & Binaries

🎯 Complete Definition

Strings are UTF‑8 encoded binaries (binary type). They are immutable and support interpolation "#{var}". Character lists (single‑quoted) exist for Erlang compatibility but are rarely used. Binaries (<< … >>) represent raw bytes. The String module provides Unicode‑aware functions.

🔧 Key operations

  • length/1 (byte count) vs String.length/1 (graphemes).
  • split/2, join/2, trim/1
  • upcase/1, downcase/1, capitalize/1
  • contains?, starts_with?
  • to_integer/1, to_atom/1
# string interpolation name = "Elixir" IO.puts("Hello, #{name}!") # concatenation "foo" <> "bar" # "foobar" # heredoc doc = """ multi line string """ # String module text = " Elixir is awesome " IO.puts(String.trim(text)) IO.puts(String.upcase(text)) IO.puts(String.length(text)) # 22 (including spaces) IO.puts(byte_size(text)) # 22 (same because ASCII) # binaries bin = <<1, 2, 3>> IO.inspect(bin) # <<1,2,3>> # pattern match binary <> = <<1,2,3>> IO.puts(a) # 1

📦 Modules & Structs

🎯 Complete Definition

Modules are namespaces for functions, macros, and structs. They can be nested, and attributes (@name) store module‑level constants. Structs are extensions of maps with a fixed set of keys, defined inside modules via defstruct. They provide compile‑time checks and default values.

🏗️ Module Features

  • defmodule defines a module.
  • @moduledoc documentation.
  • defstruct defines a struct.
  • @type and @spec for typespecs.
  • use / import / alias / require for compilation directives.
defmodule Person do defstruct name: "", age: 0, country: "Unknown" @doc "greets the person" def greet(%Person{name: name}) do "Hello, #{name}!" end end # creating a struct john = %Person{name: "John", age: 30} IO.puts(Person.greet(john)) # updating struct older_john = %{john | age: 31} IO.inspect(older_john) # nested module alias alias Person, as: P bob = %P{name: "Bob"} IO.puts(bob.country) # "Unknown"

🛡️ Protocols

🎯 Complete Definition

Protocols are a way to achieve polymorphism in Elixir: a protocol defines a set of functions that can be implemented for different data types. They are similar to interfaces in OOP but are open (can be implemented for any type, even built‑ins). Common built‑in protocols: Enumerable, Inspect, String.Chars.

🔌 Defining & using

defprotocol declares the functions; defimpl provides implementations for specific types. Protocols can also be derived automatically for structs.

defprotocol Greeter do def greet(entity) end defimpl Greeter, for: Person do def greet(%Person{name: name}), do: "Hello, #{name}!" end defimpl Greeter, for: Atom do def greet(atom), do: "Hello, #{atom}!" end IO.puts(Greeter.greet(%Person{name: "Alice"})) # Hello, Alice! IO.puts(Greeter.greet(:world)) # Hello, world! # Using built-in protocol Inspect IO.inspect([1,2,3], label: "list") # uses Inspect protocol

⚠️ Exceptions

🎯 Complete Definition

Exceptions in Elixir are raised with raise/1 and rescued with try/rescue. Elixir follows Erlang’s “let it crash” philosophy; exceptions are often used for programmer errors, while expected failures are handled via tuples ({:ok, _} or {:error, _}). The try block can also have catch and after.

🛠️ Exception constructs

  • raise "oops" – raises RuntimeError.
  • raise ArgumentError, message: "bad arg"
  • try do ... rescue e in RuntimeError -> ... end
  • after – always runs.
  • throw / catch – non‑local return (less common).
# defensive with {:ok, _} / {:error, _} pattern result = try do # might raise File.read!("nonexistent") rescue e in File.Error -> {:error, e.reason} end IO.inspect(result) # {:error, :enoent} # using raise / rescue try do raise "failure" rescue e in RuntimeError -> IO.puts("Caught: #{e.message}") after IO.puts("cleanup") end # custom exception defmodule MyError do defexception message: "default error" end raise MyError, message: "custom"

⚡ Concurrency (Actor)

🎯 Complete Definition

Concurrency in Elixir uses the actor model: lightweight processes (not OS threads) that isolate state and communicate via asynchronous messages. Processes are spawned with spawn, and messages are sent with send and received with receive. Thousands of processes can run simultaneously. The BEAM VM preemptively schedules them.

🏗️ Process primitives

  • spawn(fn -> ... end) – creates a new process, returns PID.
  • send(pid, message) – sends async message.
  • receive do ... end – receives a message (blocks).
  • Process.register(pid, name) – alias by atom.
  • self() – current process PID.
parent = self() # spawn child child = spawn(fn -> receive do {:hello, msg} -> send(parent, {:ok, "child received: #{msg}"}) end end) send(child, {:hello, "world"}) receive do {:ok, response} -> IO.puts(response) after 1000 -> IO.puts("timeout") end # register Process.register(child, :worker) send(:worker, {:hello, "via register"}) # State in loop (server) defmodule Counter do def loop(count) do receive do {:increment, caller} -> send(caller, {:ok, count + 1}) loop(count + 1) end end end pid = spawn(fn -> Counter.loop(0) end) send(pid, {:increment, self()}) receive do {:ok, new} -> IO.puts(new) end # 1

🏗️ OTP & Supervisors

🎯 Complete Definition

OTP (Open Telecom Platform) is a set of libraries and design principles for building robust, fault‑tolerant applications. Key behaviours: GenServer (generic server), Supervisor (process monitoring), Application (component packaging). Supervisors define restart strategies (one‑for‑one, rest‑for‑one) to keep systems alive.

🏭 Core OTP behaviours

  • use GenServer – implements server with call/cast.
  • use Supervisor – supervises child processes.
  • Application – start/stop callbacks.
  • Task – asynchronous units of work.
  • Agent – simple state containers.
defmodule Stack do use GenServer # Client API def start_link(initial) do GenServer.start_link(__MODULE__, initial, name: __MODULE__) end def push(value) do GenServer.cast(__MODULE__, {:push, value}) end def pop do GenServer.call(__MODULE__, :pop) end # Server callbacks def init(stack), do: {:ok, stack} def handle_call(:pop, _from, [head | tail]), do: {:reply, head, tail} def handle_cast({:push, value}, state), do: {:noreply, [value | state]} end # start the server {:ok, _pid} = Stack.start_link([]) Stack.push(42) Stack.push(100) IO.puts(Stack.pop()) # 100 # Supervisor example (conceptual) children = [ {Stack, []} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts)