Some thoughts after writing my first program in Elixir

Thanks for your input.

Yes, I think that makes sense as well. So then I can simplify the code some more.

It may be an idea to read up on sum types (aka union types) vs product types (which you’re likely already familiar with), example:

Sum Types Are Coming: What You Should Know

That should reinforce the idea that the various function clauses assemble to one single function operating on a sum type (rather than falling back on the idea of “overloaded” functions).

1 Like

I don’t want to be too nitpicky but check/1 here is really just one function with multiple clauses. It is the elixir syntax which makes it look like 2 functions.

4 Likes

You’re right—Thanks for pointing that out :slight_smile:

Thanks for the pointers! I’ll check out that blog post. I’m working my way through “Programming Elixir” now—I imagine it will probably come up there as well in some later chapter.

Thank you everyone for taking the time to give me feedback and ideas on how to improve this simple program. I have pushed some improvements to the git repo after your suggestions.

Thank you for the helpful pointers, I’m also working my way through “Programming Elixir”. I’ve modified the code slightly, as to make it more verbose:

    defmodule FizzBuzz do
      @type result :: integer | :fizz | :buzz | :fizz_buzz

      @spec check(integer) :: result
      def check(number) when is_integer(number) do
        case {rem(number, 3), rem(number, 5), number} do
          {0, 0, 0} ->
            0

          {0, 0, _} ->
            :fizz_buzz
            IO.puts("FizzBuzz: " <> to_string(number))

          {0, _, _} ->
            :fizz
            IO.puts("Fizz: " <> to_string(number))

          {_, 0, _} ->
            :buzz
            IO.puts("Buzz: " <> to_string(number))

          {_, _, _} ->
            :number
            IO.puts("Number: " <> to_string(number))
        end
      end

      # can actually be arbitrary nested
      @spec check(list | Range.t()) :: [result]
      def check(numbers) do
        IO.inspect(numbers)
        Enum.map(numbers, fn number -> check(number) end)
      end
    end

`FizzBuzz.check([1..15])`

Here’s the output:

[Running] elixir “/home/master/Documents/dev/spectre/elixir/scratch.exs”

[1
15]
1
15
Number: 1
Number: 2
Fizz: 3
Number: 4
Buzz: 5
Fizz: 6
Number: 7
Number: 8
Fizz: 9
Buzz: 10
Number: 11
Fizz: 12
Number: 13
Number: 14
FizzBuzz: 15
[Done] exited with code=0 in 0.198 seconds

However, what I don’t understand is this:

[1
15]
1
15

That seems like IO.inspect(numbers) is called twice, which means that the function “check(numbers)” is called twice, before the “check(number)” function is called through “Enum.map()”. What am I missing here?

I’m not sure (haven’t tested this hypothesis), but I think that might be happening because you’re passing a list of ranges to FizzBuzz.check(), instead of just a single range.

Try changing FizzBuzz.check([1..15]) to FizzBuzz.check(1..15)

Also, thanks for sharing your solution :slight_smile:

1 Like

Thank you! That’s exactly right, I did pass a list instead of a simple range:)

FizzBuzz.check(1..15):

    [Running] elixir "/home/master/Documents/dev/spectre/elixir/scratch.exs"

    1..15
    Number: 1
    Number: 2

OK, kewl - that makes sense. However:

FizzBuzz.check([[1..15]])

[Running] elixir "/home/master/Documents/dev/spectre/elixir/scratch.exs"
[[1..15]]
[1..15]
1..15
Number: 1

Why/how does it ‘automagically’ flatten the list until it gets a range?

1 Like

Recursion.

 def check(numbers) do
   IO.inspect(numbers)
   Enum.map(numbers, fn number -> check(number) end)
 end

The “other” check/1 clause only works with integers. This one is run again and again - which happens to work for single element lists. Again, you have two clauses for the same check/1 function.

The following typing would be more explicit:

 @type result :: list | integer | :fizz | :buzz | :fizz_buzz
 @spec check(list | Range.t() | integer) :: result
1 Like

Thank you, very much appreciated. “Enum.map()” behaves differently than I would have expected, but I got it now:

iex(9)> a = [[[[["foo","bar","baz"]]]]]
[[[[["foo", "bar", "baz"]]]]]
iex(10)> Enum.map(a, fn x -> IO.puts(x) end)
foobarbaz

https://hexdocs.pm/elixir/Enumerable.html

Internally, [ `Enum.map/2` ](https://hexdocs.pm/elixir/Enum.html#map/2) is implemented as follows:

```
def map(enumerable, fun) do
  reducer = fn x, acc -> {:cont, [fun.(x) | acc]} end
  Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
end
```

In dynamic languages types beyond the intrinsic types are rarely discussed as there isn’t any static type checking. In the case of Programming Elixir 1.6 type specifications and Dialyzer/Dialixir are only discussed in Appendix 2: Type Specifications and Type Checking. And even there it only talks about Combining types without exploring that the new type is a sum type (or union type).

Only statically typed functional languages like Haskell, Elm or ReasonML (variant types) tend to broach that subject. Even so, it is often helpful to be type-aware even with dynamically typed languages.

For some background on the limitations of Dialyzer’s Success Typing see: learn you some Erlang: Type Specifications and Erlang (Don’t Drink Too Much Kool-Aid).

3 Likes

Thanks for the heads-up and useful info.

These are awesome, I hate squishing. ^.^

But yes, learn Dialyzer, the Elixir wrapper library for it of Dialyxir makes it so simple to use. :slight_smile:

1 Like

Also make sure you’re using the latest RC version of Dialyxir, it prints out the errors in Elixir format (instead of Erlang format)

2 Likes