Teoría

  • Pipe Operator
  • Strings, Charlist y Binarios
  • Alias, Import, Use

Pipeline Operator

Bash example

ls | grep "Do"

Elixir without pipeline

string            = "hello, world!"
words             = String.split(string, " ")
capitalized_words = Enum.map(words, &String.capitalize/1)
Enum.join(capitalized_words, " ")

Elixir with pipeline

"hello, world!"
  |> String.split(" ")
  |> Enum.map(&String.capitalize/1)
  |> Enum.join

String

Strings are a sequence of bytes

iex> string = <<104,101,108,108,111>>
"hello"
iex> "hello" <> <<0>>
<<104, 101, 108, 108, 111, 0>>

Charlist

iex> 'hełło'
[104, 101, 322, 322, 111]
iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>

322 is the Unicode codepoint for ł but it is encoded in UTF-8 as the two bytes 197, 130.

Code point

You can get a character’s code point by using ?

iex> ?Z
90

Back to strings

iex> string = "\u0061\u0301"
"á"
iex> String.codepoints string
["a", "́"]
iex> String.graphemes string
["á"]

Basic functions

iex> String.length "Hello"
5
iex> String.replace("Hello", "e", "a")
"Hallo"
iex> String.duplicate("Oh my ", 3)
"Oh my Oh my Oh my "
iex> String.split("Hello World", " ")
["Hello", "World"]

Alias / require / import / use

Note: The special forms alias, use, import, and require are ways of accessing functions or macros outside of the current module. The forms alias and import are used to be able to refer to functions without having to use their fully qualified names. The form use is used to add functionality to the current module by running a macro from another module. When macros are used from an external module, require is needed to make the macros available to the compiler at compile time.

Alias

Used to shorten the references to a specific module.

# Fully qualified User struct
%Application.User{}

# alias is used to shorten the fully qualified name
alias Application.User, as: User

# After aliasing
%User{}

# alias without `:as` option will automatically use the last
# part of the module name after the last period
alias Application.User
# is the same as
alias Application.User, as: User

Import

Imports specific functions into the current module so they can be called without using their module name.

# Without import functions need to be called
# by their full name including the module
"ABC" = String.upcase("abc")

# Import a single function with the form
# import Module, only: [function_name: arity]
import String, only: [upcase: 1]

# upcase can now be used without the module name
"ABC" = upcase("abc")

# Imports all functions in the String module.
# It is recommend to use only option above to
# only import the functions you need.
import String

Require

Makes a macro from an external module available to the compiler at compile time.

defmodule Hello do
  # Example macro to add say_hello function to the module
  defmacro hello_macro do
    quote do
      def say_hello do
        IO.puts "Hello"
      end
    end
  end
end

defmodule MyModule do
  # Without require here results in the following error:
  # (CompileError) iex:37: you must require Hello before invoking the macro Hello.hello_macro/0
  require Hello
  Hello.hello_macro
end
# Prints Hello
MyModule.say_hello

Use

Adds functionality to the current module by calling another module’s __using__ macro.

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def say_hello do
        IO.puts "Hello"
      end
    end
  end
end

defmodule MyModule do
  use Hello
end
# prints "Hello"
MyModule.say_hello

Multi alias/import/require/use

alias MyApp.{Foo, Bar, Baz}

Elixir Flashcards

  • Kernel II
  • Enum I
  • Enum II

Ecto

Creamos migración para Comentarios

Vemos tareas disponibles de Ecto:

mix ecto

Vemos documentación de tarea de generación de migración:

mix help ecto.gen.migration

Creamos nueva migración

mix ecto.gen.migration add_comments

Entramos al archivo nuevo y agregamos body y post_id (Correjido):

create table(:comments) do
  add :body, :string
  add :post_id, references("posts", on_delete: :delete_all)

  timestamps()
end

Corremos migración:

mix ecto.migrate

Corremos queries en iex

import Ecto.Query

query = from p in "posts", select: p.id
Repo.all(query)

query = from p in "posts", select: %{id: p.id, title: p.title, body: p.body}
Repo.all(query)

query = from p in "posts", join: c in "comments", on: c.post_id == p.id, select: [p.id, c.id]
Repo.all(query)

Ejemplo con Pipe: (los parentesis son necesarios)

(
"posts"
|> order_by([:title])
|> select([:title, :body])
|> Repo.all()
)

Ecto.Schema

defmodule Yo.Blog.Post do
  use Ecto.Schema

  schema "posts" do
    field :body, :string
    field :title, :string

    timestamps()
  end
end

Corremos queries usando el schema Post:

import Ecto.Query
alias Yo.Blog.Post

Repo.all(Post)

query = from p in Post, where: p.id < 5
Repo.all(query)

Creamos Schema de Comentarios

defmodule Yo.Blog.Comment do
  use Ecto.Schema
  alias Yo.Blog.Post

  schema "comments" do
    field :body, :string
    belongs_to :post, Post

    timestamps()
  end
end

Y en post

has_many :comments, Yo.Blog.Comment

La macro schema nos define un struct:

%Post{}

Corremos queries en iex

import Ecto.Query
alias Yo.Repo
alias Yo.Blog.Post
alias Yo.Blog.Comment

Repo.all(Post)

Repo.all(Comment)

query = from p in Post, join: c in Comment, on: c.post_id == p.id
Repo.all(query)

query = from [p, c] in query, select: {p.title, c.body}
Repo.all(query)

Creamos archivo .iex.exs:

import Ecto.Query
alias Yo.Repo
alias Yo.Blog.Post
alias Yo.Blog.Comment

Insertamos comentario sin post

Repo.insert(%Comment{body: "Probando"})
# Ups! Necesitamos validaciones!

Ecto.Changeset

Vamos a correr uno por vez:

import Ecto.Changeset

post = %Post{title: "titulo"}

# Sin nada
cast(post, %{}, [:title, :body])

# Con titulo
cast(post, %{title: "nuevo titulo"}, [:title, :body])

# Con titulo y body
changeset = cast(post, %{title: "nuevo titulo", body: "nuevo body"}, [:title, :body])

changeset.valid?

changeset.errors


# Con validación
(
  changeset = cast(post, %{}, [:title, :body])
  |> validate_required([:title, :body])
)

changeset.valid?

changeset.errors

IO.puts(inspect(cast(post, %{}, [:title, :body]), structs: false, pretty: true))

En comment.ex agregamos:

def changeset(comment, attrs) do
  comment
  |> cast(attrs, [:post_id, :body])
  |> validate_required([:post_id, :body])
end

Probamos insertar otro comentario sin post usando el changeset

changeset = Comment.changeset(%Comment{}, %{body: "Otro comentario"})

changeset.valid?

changeset.errors

Repo.insert(changeset)

# Ahora no me lo permite :)

En post.ex agregamos:

def changeset(post, attrs) do
  post
  |> cast(attrs, [:title, :body])
  |> validate_required([:title, :body])
end

Queremos que los posts tengan títulos únicos.

Creamos una nueva migración:

mix ecto.gen.migration add_unique_index_to_post_title

Agregamos index único al titulo de Posts:

defmodule Yo.Repo.Migrations.AddUniqueIndexToPostTitle do
  use Ecto.Migration

  def change do
    create unique_index("posts", [:title])
  end
end

Vamos a iex e intentamos insertar el Post 2 veces

changeset = Post.changeset(%Post{},%{title: "a", body: "asa"})
Repo.insert(changeset)
Repo.insert(changeset)

Ahora agregamos una restricción:

|> unique_constraint(:title)

Contexto

Modificamos get_post! para que nos traiga los comentarios.

  def get_post!(id) do
    Repo.get!(Post, id) |> Repo.preload([:comments])
  end

Si quiero que venga en una sola query tengo que ser explícito con los joins.

def get_post!(id) do
  Repo.one!(
    from p in Post,
      where: p.id == ^id,
      left_join: c in assoc(p, :comments),
      preload: [comments: c]
  )
end

Corremos tests

mix test

Modificamos test:

test "get_post!/1 returns the post with given id" do
  post = post_fixture()
  loaded_post = Blog.get_post!(post.id)

  assert post.title == loaded_post.title
  assert post.body == loaded_post.body
  assert loaded_post.body == []
end