Perl Weekly Challenge: Now with Elixir!

One of the things I want to challenge myself to do is learn some more useful things, and one of the languages they’re using at work is Elixir. It’s a functional language, not a procedural language like Perl, so this is not only learning a new language but it’s learning a new way to think about code.

Since I can’t really learn by just reading about a language or watching a bunch of excellent videos one of my coworkers produced, I decided that I needed to start doing the Perl Weekly Challenge tasks in Elixir. Today, I’m tackling PWC 267 Task 1.

One of the first things that wound up biting me was trying to do something like this:

if product == 0 do
  sign = 0
else
  sign = product / abs(product)
done
sign

But what I was getting were warnings:

    warning: variable "sign" is unused (if the variable is not meant to be used, prefix it with an underscore)
    │
  7 │       sign = product / abs(product)
    │       ~
    │
    └─ elixir/ch-1.exs:7:7: PWC.productSign/1

    warning: variable "sign" is unused (if the variable is not meant to be used, prefix it with an underscore)
    │
  5 │       sign = 0
    │       ~
    │
    └─ elixir/ch-1.exs:5:7: PWC.productSign/1

    error: undefined variable "sign"
    │
  9 │     sign
    │     ^^^^
    │
    └─ elixir/ch-1.exs:9:5: PWC.productSign/1

This confused me for a while, but then I found out what was happening:

This is also a good opportunity to talk about variable scoping in Elixir. If any variable is declared or changed inside ifcase, and similar constructs, the declaration and change will only be visible inside the construct.

Elixir: case, cond, and if

I finally wound up with the following implementation for the task:

# You are given an array of @ints.

# Write a script to find the sign of product of all integers in
# the given array. The sign is 1 if the product is positive,
# -1 if the product is negative and 0 if product is zero.

# Example 1
# Input: @ints = (-1, -2, -3, -4, 3, 2, 1)
# Output: 1
#
# The product -1 x -2 x -3 x -4 x 3 x 2 x 1 => 144 > 0

# Example 2
# Input: @ints = (1, 2, 0, -2, -1)
# Output: 0
#
# The product 1 x 2 x 0 x -2 x -1 => 0

# Example 3
# Input: @ints = (-1, -1, 1, -1, 2)
# Output: -1
#
# The product -1 x -1 x 1 x -1 x 2 => -2 < 0

defmodule PWC do

  def productSign(ints) do
    product = Enum.reduce(ints, &( &1 * &2 ))
    sign = if product == 0, do: 0,
                          else: div(product, abs(product))
    explain = "The product "
            <> Enum.join(ints, " × ")
            <> " => "
            <> to_string(product)
    explain = cond do
                sign > 0 -> explain <> " > 0"
                sign < 0 -> explain <> " < 0"
                true     -> explain
              end
    {sign, explain}
  end

  def solution(ints) do
    IO.puts("Input: @ints = (" <> Enum.join(ints, ", ") <> ")")
    {sign, explain} = PWC.productSign(ints)
    IO.puts("Output: " <> to_string(sign) )
    IO.puts("\n" <> explain)
  end
end

IO.puts("Example 1:")
PWC.solution([-1, -2, -3, -4, 3, 2, 1])

IO.puts("")

IO.puts("Example 2:")
PWC.solution([1, 2, 0, -2, -1])

IO.puts("")

IO.puts("Example 3:")
PWC.solution([-1, -1, 1, -1, 2])

$ elixir elixir/ch-1.exs
Example 1:
Input: @ints = (-1, -2, -3, -4, 3, 2, 1)
Output: 1

The product -1 × -2 × -3 × -4 × 3 × 2 × 1 => 144 > 0

Example 2:
Input: @ints = (1, 2, 0, -2, -1)
Output: 0

The product 1 × 2 × 0 × -2 × -1 => 0

Example 3:
Input: @ints = (-1, -1, 1, -1, 2)
Output: -1

The product -1 × -1 × 1 × -1 × 2 => -2 < 0

This still feels a lot like the solutions I put together in Raku, Perl, and Python, though. 🤔


Today I showed my coworker Doug this post, and he had some suggestions: he said he was a fan of “little guard functions”:

  defp sign(n) when n > 0, do: " > 0"
  defp sign(n) when n < 0, do: " < 0"
  defp sign(_), do: ""

This defined different functions that depend on the values of the parameters, which is a pattern that I’m seeing a lot in examples, but hadn’t thought to use. And though he liked my use of Enum.reduce, he pointed me at Enum.product, which already did what I needed Enum.reduce to do. He also modeled the proper way to do string interpolation:

  def productSign(ints) do
    product = Enum.product(ints)
    sign =
      case product do
        0 -> 0
        _ -> div(product, abs(product))
      end
      explain =
        "The product #{Enum.join(ints, " × ")} => #{to_string(product)}#{sign(sign)}"
    {sign, explain}
  end

So, with that feedback, I was able to refine my solution function that I use to call productSign and format the output:

defmodule PWC do

  defp sign(n) when n > 0, do: " > 0"
  defp sign(n) when n < 0, do: " < 0"
  defp sign(_), do: ""

  def productSign(ints) do
    product = Enum.product(ints)
    sign =
      case product do
        0 -> 0
        _ -> div(product, abs(product))
      end
      explain =
        "The product #{Enum.join(ints, " × ")} => #{to_string(product)}#{sign(sign)}"
    {sign, explain}
  end

  def solution(ints) do
    IO.puts("Input: @ints = (#{Enum.join(ints, ", ")})")
    {sign, explain} = PWC.productSign(ints)
    IO.puts("Output: #{to_string(sign)}\n#{explain}")
  end
end

That feels a bit more functional and less like my earlier procedural solutions in Raku and Perl. Tonight, I’ll tackle this week’s Task 2 and see how functional I can get it right out of the gate.


The code can be found on GitHub at https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-267/packy-anderson/elixir