Last active
February 19, 2020 14:53
-
-
Save nathanjohnson320/976c77ec409e120006f1ae430275052e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Elixir in 5 minutes | |
# To run this script, do elixir processes.exs | |
# Comments are made with the pound sign | |
# Strings are standard double quotes "" | |
# To write to the CLI you can use IO.puts or IO.write | |
IO.puts "Hello, world" | |
# Get input with IO.gets | |
input = IO.gets "IO.gets will take CLI input? (y/n)" | |
# Interpolate strings by using #{} inside double quotes. You can interpolate functions as well | |
IO.puts "I said: #{input}" | |
# Double quoted strings are also binaries. A binary is literally a sequence of bytes | |
IO.puts "Strings are binaries? #{is_binary(input)}" | |
# This will print hello with unicode characters since it translates codepoints to strings, elixir has excellent unicode support | |
IO.puts <<104, 101, 197, 130, 197, 130, 111, 0>> | |
# Pattern matching is the most productive part of the elixir language | |
# This is similar to destructuring in javascript but a lot more powerful | |
# time for the basic data types and pattern matching | |
# Elixir has numbers | |
IO.inspect 2 | |
# And floats | |
IO.inspect 2.24123123123 | |
# And booleans | |
IO.inspect true != false | |
IO.inspect not true == false # not == ! | |
IO.inspect true and false != true # and == && | |
IO.inspect true or false == true # or == || | |
# And we already know it has strings | |
IO.inspect "This is a string" | |
# It also has character lists, this data type is primarily used for working with erlang modules | |
IO.inspect 'hełło' == [104, 101, 322, 322, 111] | |
# There are string constants called atoms, be careful when using them as they are not garbage collected | |
IO.inspect ":ok is an atom |> #{is_atom(:ok)}" | |
# Tuples are fixed data normally used for configurations | |
{ 1, 2 } = { 1, 2 } | |
my_tuple = { 1, 2 } | |
# Atoms in tuples are most commonly used to match the returned value of function | |
# Introducing the case statement which pattern matches multiple lines and returns on the first match | |
# and the |> (pipe) operator which sends the contents of the first variable | |
# as the first arg of the next function | |
case File.read("./swag") do | |
{:ok, contents} -> contents | |
{:error, code} -> "BAD #{code}" | |
end | |
|> IO.inspect | |
# You can assign variables to their corresponding match on the right hand side in tuples | |
{ 1, x } = my_tuple | |
IO.inspect x | |
# You can also get an element from an existing tuple, 2 should equal 2 | |
IO.inspect elem(my_tuple, 1) == x | |
# This works for maps which are simple key value stores | |
# To access values in a map of this form use standard JS syntax my_map["my name"] | |
my_map = %{ "my name" => "is not rick", "swag" => "team", "not" => true} | |
IO.inspect my_map["my name"] | |
# You can match part of a map | |
%{ "my name" => name } = my_map | |
IO.puts "My name #{name}" | |
# Maps can also use atoms as keys but the whole map must use the same syntax, it can't have atom and | |
# string keys all at once. If you use an atom key then you can also access maps as my_map.swag | |
# It is also worth mentioning that since elixir is an immutable language this my_map | |
# and the one above are two different variables | |
my_map = %{ my_name: "is not rick", swag: "team", not: true} | |
IO.inspect my_map.swag | |
# Update maps has a special syntax | |
my_map = %{ my_map | my_name: "is Nathan", not: false } | |
IO.inspect my_map.not | |
# Elixir also has linked lists which we saw above with character lists | |
my_list = [100, 200, 300, 400] | |
# It also has the cons operator for destructuring lists as head and tail | |
[head | tail] = my_list | |
IO.puts "Head is 1, #{head}, Tail is [2,3,4]: #{tail}" | |
# you can match specific indices of a list as well | |
[ one, two, three | four ] = my_list | |
IO.inspect one | |
IO.inspect two | |
IO.inspect three | |
IO.inspect four # This will be a list | |
# Lists with atom indexes are called "keyword lists", underneath this is a list of two item tuples [{:beginning, true}, {:second, "second"}] | |
my_keywords = [ beginning: true, second: "second"] | |
IO.inspect my_keywords[:beginning] | |
# You can also leverage anonymous functions, note the "." before the arguments to easily identify that it is anonymous | |
my_func = fn(arg1) -> IO.puts arg1 end | |
my_func.("Hello, world!") | |
# Pattern matching works inside anonymous function args as well | |
my_func = fn({:ok, arg1}) -> IO.puts arg1 end | |
my_func.({:ok, "Hello, world of patterns!"}) | |
# Now we know all the basics and can talk about processes which is the magic part of elixir/erlang | |
# Processes are lightweight threads that can be spawned, assigned to variables, monitored, and send messages to eachother | |
# which allows you to build gigantic fault tolerant applications | |
# start by spawning a process, it will be given a unique process identifier that is returned when the process terminates | |
my_process = spawn fn -> IO.puts "I'm a process" end | |
IO.inspect my_process | |
# Pause for one hundred milliseconds, note that you can directly work with erlang modules by accessing them as ":module"."function" | |
:timer.sleep 100 | |
# After the process executes it dies and is garbage collected | |
IO.inspect Process.alive?(my_process) | |
# Messages can talk to each other with send and receive | |
process_1 = spawn fn -> | |
receive do | |
{:ok, msg} -> IO.inspect msg | |
{:error} -> IO.inspect "ERROR" | |
end | |
end | |
process_2 = spawn fn -> | |
send(process_1, {:ok, "HERES A MESSAGE FOR YA"}) | |
end | |
# Wait a little bit for the messages to send | |
:timer.sleep 100 | |
# Both processes are done | |
IO.inspect Process.alive?(process_1) | |
IO.inspect Process.alive?(process_2) | |
# You can get the current process' PID by calling self() | |
IO.inspect self() | |
# Now for modules, modules contain a set of functions as well as macros for abstracting/attaching functionality | |
# to other modules. You can have modules inside of modules too. | |
defmodule OuterModule do | |
defmodule MyMod do | |
# Structs are maps with default values and are declared under module namespaces | |
defstruct make: "Nissan", model: "Sentra", year: 2014 | |
# the @doc macro allows you to place markdown in your code. Using mix docs (ex_doc module) allows | |
# you to generate html pages with these macros. These can only be put inside modules though | |
@doc ~S""" | |
# Here is a header | |
Here is a command | |
`cd home` | |
## Examples | |
You can even put iex commands in the docs as examples and these will be run as unit tests | |
iex> MyMod.print("CREATE shopping\r\n") | |
nil | |
""" | |
def print(args) do | |
IO.puts args | |
end | |
end | |
# Return a MyMod struct with the year overridden | |
def get_mod(year) do | |
%MyMod{year: year} | |
end | |
end | |
# You can invoke module functions as follows | |
OuterModule.MyMod.print("HELLO") | |
# Normally you can access structs as %<ModuleName>{} but since we're in a script | |
# we have to wrap the create of the struct in a function | |
IO.inspect OuterModule.get_mod(2016) | |
# Now that we understand the basics of modules I'll mention a few of the most | |
# useful in Elixir's stdlib | |
# Enum, (ee noom) you will use this module most frequently. It iterates over | |
# data types that have implemented the Enumerable protocol (a protocol is basically an interface) | |
# We'll also introduce range which generate every value from the first to the second | |
enumerable_list = 1..10 | |
Enum.each(enumerable_list, fn(item) -> IO.puts "- #{item} - " end) | |
Enum.map(enumerable_list, fn(item) -> item * 2 end) |> IO.inspect | |
# Maps are enumerate in tuple format as {key, val} | |
enumerable_map = %{ a: 1, b: 2, c: 3, d: 4, e: 5 } | |
Enum.reduce(enumerable_map, 0, fn({_, val}, acc) -> acc + val end) |> IO.inspect | |
# Streams are similar to Enum but they are evaluated lazily, meaning only when called | |
# this way you can iterate over theoretically infinite datasets without running out | |
# of memory. To grab values from a stream you can use Enum.take, you can also leverage | |
# a good number of Enumerable functions (like reduce) | |
# Here's fibonacci as a stream, taking the first 50, and printing them out | |
Stream.unfold({1, 1}, fn {a, b} -> {a, {b, a + b}} end) |> Enum.take(50) |> IO.inspect | |
# Stream and Enum are both really useful for data iteration/aggregation. | |
# Elixir provides some other useful control flow/data aggregation constructs outside the stdlibs | |
# since these types of tasks are commonplace | |
# the first of these is "for" also called a "comprehension" | |
# for takes in a series of generator functions which are variables that you can use in reduction | |
# it also allows for "filter" statements to widdle down that list | |
for a <- 1..10, # Generate list of 10 items | |
b <- 1..10, # Generate list of 10 items | |
rem(a, 2) == 0, # Filter non-odd numbers from a | |
rem(b, 2) == 1, # Filter non-event numbers from b | |
do: {a, b} # Take a tuple of a, b elements | |
|> IO.inspect | |
# for is used for list generation and matching but there are a lot of times where | |
# we don't want to generate anything but rather handle a series of pattern matches for | |
# control flow. The mechanism for doing so is "for"s cousin "with". "with" helps you write | |
# code to handle the happy path | |
less_than_ten = fn(arg) -> | |
# Cond runs through a series of conditions and returns the first that evaulates to true | |
cond do | |
arg < 10 -> {:ok, arg} | |
arg >= 10 -> {:error, arg} | |
end | |
end | |
less_than_five = fn(arg) -> | |
cond do | |
arg < 5 -> {:ok, arg} | |
arg >= 5 -> {:error, arg} | |
end | |
end | |
is_four = fn(arg) -> | |
cond do | |
arg == 4 -> {:ok, arg} | |
arg != 4 -> {:error, arg} | |
end | |
end | |
# Instead of nesting a series of case statements we can match only | |
# the patterns that we want and if they don't match exactly then it'll | |
# fall into the else block. | |
four = with {:ok, arg} <- less_than_ten.(4), | |
{:ok, arg} <- less_than_five.(arg), | |
{:ok, arg} <- is_four.(arg) do arg | |
else | |
_ -> {:error, four} | |
end | |
IO.inspect four | |
not_four = with {:ok, arg} <- less_than_ten.(6), | |
{:ok, arg} <- less_than_five.(arg), | |
{:ok, arg} <- is_four.(arg) do arg | |
else | |
{:error, val} -> {:error, val} | |
_ -> {:error, nil} | |
end | |
IO.inspect not_four | |
# Here are a few honorable mention modules that you'll probably use | |
# Map | |
# Task | |
# OptionParser | |
# Regex | |
# Code <-- Be careful with this | |
# Macro <-- Be careful with this | |
# Mix | |
# Now that we know the language basics it's time to cover elixir's CLI tool Mix | |
# With any of these you can use the "help" command to print out useful info about | |
# whichever mix task you're trying. For example `mix help new` | |
# create a new app | |
# mix new <app> | |
# create new supervised app | |
# mix new <app> --sup | |
# run tests | |
# mix test | |
# Dependencies are accessible via git or in the standard repo hex.pm | |
# to add a dependency follow the instructions in the module's readme | |
# and place it under the dependencies function inside mix.exs | |
# Get dependencies | |
# mix deps.get | |
# Compile dependencies | |
# mix deps.compile | |
# Run a mix project with iex | |
# iex -S mix | |
# Run a mix app or script | |
# mix run | |
# Run a mix app or script without halting | |
# mix run --no-halt | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment