Skip to content

Latest commit

 

History

History
683 lines (473 loc) · 19 KB

functions.livemd

File metadata and controls

683 lines (473 loc) · 19 KB

Functions

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"},
  {:smart_animation, github: "brooklinjazz/smart_animation"}
])

Navigation

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • How do we define a function?
  • How do we call a function?
  • How do we use the pipe operator |> to pipe function calls into each other?

Functions

Elixir is a functional programming language. So you can imagine that functions must be important. But what is a function?

Input And Output (IO)

A function is a set of repeatable instructions. A function accepts some input, and returns some output.

  flowchart LR
    Input --> Output
Loading

Black Box

How the function converts some input to some output is often referred to as a black box. It's a black box because you don't need to know (or can't know) the details of how it works.

  flowchart LR
    Input --> B[Black Box] --> Output
Loading

Creating A Function

Let's create a function called double which will take in a number and double its value.

flowchart LR
  A[2] --> B[double] --> C[4]
Loading

Now, let's create our first function. At first, it's going to do nothing. A function must have an output. We can return nil for now.

double = fn -> nil end

You may see some weird-looking output like #Function<45.65746770/0 in :erl_eval.expr/5>. Don't worry too much about that. It's how Elixir represents a function internally.

Parts Of A Function

Let's break down what we did above.

  1. double is a variable name. Often you'll refer to it as the function name. It can be any valid variable name.

  2. We bind double to an anonymous function. The anonymous function is everything from the fn to the end.

    flowchart LR
     A[function name] --> B[=]
     B --> C[anonymous function]
    
    Loading
  3. Elixir uses the fn keyword to define a function.

  4. The next value -> separates the function head and the function body.

  5. The function head describes the input of the function. In this example, it's empty.

  6. The function body contains the function's implementation or black box . In this example, it returns nil.

  7. Elixir uses the end keyword to stop creating a function.

flowchart LR
   direction LR
   a[function name] --> B
   b[function head] --> A
   b[function head] --> B
   c[function body] --> C
   subgraph a[Breaking Down A Function]
      direction LR
      A[fn] ---- B
      B[->] --- C
      C[nil] --- D
      D[end]
   end
Loading

Calling A Function

Our double function doesn't do much at this point, but let's see the output that it returns.

We use the .() syntax in Elixir to get the function's output. We often say we are executing or calling a function.

double.()

double should return nil because that's all we've told it to do so far. However, we want it to multiply a number by 2.

To do that, we need to make the function accept some input. To do this, we define a parameter in the function like so.

double = fn parameter -> nil end

You'll notice a warning above. That's because Elixir is smart and lets us know that we've created a parameter, but we're not using it.

In Elixir, you can ignore this warning for unused variables by starting them with an underscore _

double = fn _parameter -> nil end

No more warning 😀 But we actually want to use that parameter, so let's modify the function to return the parameter instead.

double = fn parameter -> parameter end

The parameter is named parameter here for the sake of example. But it works a lot like a variable, and it can be named any valid variable name.

Let's rename it to number to clarify that we expect the input to be a number.

double = fn number -> number end

Now the function head takes in a value. We have to pass it an argument when we call it. The argument will be bound to the parameter when the function executes. We'll give it the integer 2.

double.(2)

Notice that if you try to call the function without an argument, it fails because it expects an argument. Not all languages do that, but Elixir is pretty smart 😎

double.()

Great, now all that's left is to multiply the parameter by 2. You should be familiar with this from the previous sections.

double = fn number -> number * 2 end

And you can use it to double any number.

double.(10)
double.(11)
double.(10 ** 2 - 1)

Under the hood, when the function runs, the parameter is bound to the argument's value.

Let's break down how a function executes step by step in the following slideshow.

frames = [
  "
  First, we define the `double` function and call it.
  ```elixir
  double = fn number -> number * 2 end
  double.(3)
  ```
  ",
  "
  The `double` function executes in place of the `double.(call)` with `number` bound to `3`.
  ```elixir
  double = fn number -> number * 2 end
  fn 3 -> 3 * 2 end
  ```
  ",
  "
  The function evaluates the function body between the `->` and the `end`
  ```elixir
  double = fn number -> number * 2 end
  3 * 2
  ```
  ",
  "
  `3` * `2` is `6`, so the function call returns `6`.
  ```elixir
  double = fn number -> number * 2 end
  6
  ```
  "
]

SmartAnimation.new(0..(Enum.count(frames) - 1), fn i ->
  Enum.at(frames, i) |> Kino.Markdown.new()
end)

As expected, double.(3) returns 6.

double.(3)

Implied Return Values

Some languages require explicit return values.

However, in Elixir the output of a function is always the last line.

For example, notice that the return value below is first + second, which equals 3.

multiline_function = fn ->
  first = 1
  second = 2
  first + second
end

multiline_function.()

Your Turn

Create a function is_even?/1 that accepts an integer, and returns true if the integer is even, or false if the integer is odd.

is_even?.(2) # true
is_even?.(1) # false
Example solution
is_even? = fn int -> rem(int, 2) == 0 end 

Enter your solution below.

Multi-Parameter Functions

Functions can accept multiple inputs. Separate parameters with commas , to create a multi-parameter function.

sum3 = fn param1, param2, param3 -> param1 + param2 + param3 end

sum3.(2, 3, 4)

Keep in mind that the first argument will be the value of the first parameter, and the second argument will be the value of the second parameter. You can repeat this with as many parameters as you want!

to_list = fn a, b, c, d, e -> [a, b, c, d, e] end

to_list.(1, 2, 3, 4, 5)

But usually, you want to avoid having too many parameters because it makes your function hard to understand.

A parameter can be bound to any valid data type, so you could instead use an associative data structure like a map or keyword list.

Function Arity

The number of parameters your function accepts is called the arity of the function.

A function with no parameters has an arity of zero. A function with one parameter has an arity of one, and so on.

You refer to the function as function_name/arity thus a function named add_two with two parameters is called add_two/2.

Your Turn

Create a function calculate_force/2 that accepts a mass and acceleration parameters. The calculate_force/2 function should return mass * acceleration.

calculate_force.(10, 10) # 100
calculate_force.(2, 4) # 8
Example solution
calculate_force = fn mass, acceleration -> mass * acceleration end

Enter your solution below.

Shorthand Syntax

Anonymous functions can be defined using a shorthand syntax. It is only an alternative and shorter version to define a function. You will sometimes see shorthand syntax, so it's helpful to understand it. However, it should not be over-used. Otherwise, your program may be less clear.

You can still bind the anonymous function to a variable with the shorthand syntax. However, you define the function with &() and put the function body between the brackets.

Here's the same double function using short-hand syntax.

double = &(&1 * 2)
double.(5)

&1 means the first parameter. If the function had more parameters, you could access them with &2, &3, and so on.

add_two = &(&1 + &2)
add_two.(2, 3)

Your Turn

Using shorthand syntax, create a subtract/2 function which subtracts two numbers.

subtract.(1, 1) # 0
subtract.(20, 25) # -5
Example solution
subtract = &(&1 - &2)

Enter your solution below.

Your Turn

Using shorthand syntax, create a multiply_three/3 function which multiplies three numbers together.

multiply_three.(2, 2, 2) # 8
multiply_three.(2, 5, 3) # 30
Example solution
multiply_three = &(&1 * &2 * &3)

Enter your solution below.

First-class Functions

Functions in Elixir are first-class citizens.

For our purposes, this means we can bind functions to variables, store them in other data types, pass them as arguments to other functions.

We'll see something like #Function<42.3316493/1 in :erl_eval.expr/6> if we try to view a function as data. This is the internal representation of the function in Elixir. We can only view a function this way, we cannot use this syntax in our code.

[fn int -> int * 2 end, 2, 3, 4]

Functions are just data as far as our programs are concerned. We can work with them like we would work with any data structure.

[my_function | _tail] = [fn int -> int * 2 end, 2, 3, 4]

my_function.(10)

If a function takes in another function as a parameter, it's called a higher-order function. The function passed in is called a callback function.

callback_function = fn -> end

higher_order_function.(callback_function)

For example, we can create a call_with_2/1 higher order function, that accepts a callback function, and calls it with the integer 2.

call_with_2 = fn callback -> callback.(2) end

We can then use the call_with_2/1 higher order function with an add_three/1 callback function. This calls add_three/1 with 2 to return 5.

add_three = fn int -> int + 3 end

call_with_2.(add_three)

Instead of binding the callback function to a variable, we can pass in the anonymous function directly.

call_with_2.(fn int -> int + 3 end)

Callback functions are useful for creating re-usable behavior with slight alterations. For example, in Elixir we have a built-in function Enum.map/2 which we will cover in later lessons.

Enum is a module. We'll cover modules in the next Modules reading material.

Enum.map/2 can accept a list as the first argument, and a callback function as the second argument. The callback function is applied to every element in the list to create a new list.

Enum.map([1, 2, 3], fn element -> element * 2 end)

Your Turn

Create a call_with_10_and_20/1 function. The call_with_10_and_20/1 function should accept a call back function, and call the function with 10 as the first argument, and 20 as the second argument.

add = fn int1, int2 -> end

call_with_10_and_20.(add) # 30
Example solution
call_with_10_and_20 = fn callback -> callback.(10, 20) end

Enter your solution below.

call_with_10_and_20 = nil

Pipe Operator

To create more complex behavior, you'll often compose smaller functions together. Composing functions together reflects nature of problem-solving where we take large problems and break them down into smaller ones.

To help compose functions together, Elixir provides the pipe |> operator. That's the | symbol likely above your enter key, and the greater than > symbol side by side to make |>.

The pipe operator allows you to take the output of one function and pass it in as an argument for the input of another function.

flowchart LR
  A[Input] --> B[Function1]
  B --> C[Pipe]
  C --> D[Function2]
  D --> E[Output]
Loading

Why is this useful? Without the pipe operator you can wind up writing deeply nested function calls.

four.(three.(two.(one.())))

Or rebinding values between function calls.

a = one.()
b = two.(a)
c = three.(b)
d = four.(c)

But with the pipe operator, you can chain functions together.

one.() |> two.() |> three.() |> four.()

If a function is called with multiple arguments, the function piped in will be the first argument.

two(1, 2) # how to call two/2 by itself.

# How To Use The Pipe Operator
# To Call The Two/2 With One/1 As The First Argument.
one.() |> two.(2)

You can also pass in a value to a pipe. It's generally non-idiomatic (not following the standards of the community) to use the pipe operator for a single value and function.

# Non-idiomatic
1 |> two.()

# Idiomatic
two.(1)

# Idiomatic
1 |> two.() |> three()

The pipe operator doesn't change the behavior of a program. Instead, the pipe operator exists as syntax sugar to improve the clarity of your code.

Your Turn

Create three functions add/2, subtract/2, and multiply/2 which add, subtract, or multiply two numbers.

Use these functions with the pipe operator |> to:

  1. start with 10.
  2. add 2.
  3. multiply by 5.
  4. subtract by 4.
Example solution
add = fn int1, int2 -> int1 + int2 end
subtract = fn int1, int2 -> int1 - int2 end
multiply = fn int1, int2 -> int1 * int2 end

10
|> add.(2)
|> multiply.(2)
|> subtract.(4)

Enter your solution below.

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Functions reading"
$ git push

We're proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation