FPOO Chapter 1 in Elixir

[boilerplate bypath=”fp-oo”]

Exercise 3: add-squares

First up, we have add-squares . Let’s write a test…

test "1.18-3: add-squares" do
  assert(add_squares([1, 2, 5]) == 30)
end

My Elixir version takes a list rather than a variable number of arguments, because Erlang doesn’t do the varargs thing.

As for implementation…

def add_squares([n|ns]), do: n*n + add_squares(ns)
def add_squares([]), do: 0

Elixir is all about recursion and pattern matching.

(Aside: I understand why it’s there, but that darn comma after the method and before the do: when writing one-liners gets me every freakin’ time.)

Hmmm… so far FPOO has avoided introducing defun, instead defining functions by creating them anonymously and then assigning them to a name. I’m suddenly curious how easy this is in Elixir.

add_squares = fn
                [n|ns] -> n*n + add_squares(ns)
                []     -> 0
              end

I realized halfway through writing this that it will lead to a compile error:

(CompileError) lib/fp_oo_elx/exercises.ex:17: function add_squares/1 un
defined

I remember now. There’s no way (that I’m aware of) to refer to an anonymous function within itself, so we can’t do recursive anonymous functions in Elixir.

(Aside: why is there no do after fn???)

OK, I guess I’ll stick to named functions. I can always take anonymous-style references to named functions with the capture (&) operator, so hopefully this won’t get in the way.

Exercise 4: Bizarro-factorial

“Implement a bizarre version of factorial that uses neither iteration of recursion”. Specifically, the instructions say to use range and apply.

test "1.18-4: bizarro-factorial" do
  assert(bizarro_factorial(5) == 120)
end
def bizarro_factorial(n) do
  (1..n) |> Enum.reduce(&(&1 * &2))
end

This is probably a gratuitous use of the pipeline operator (|>), but I don’t care!

Note that FPOO specifies that I use only range and apply. The range operator (..) is the Elixir equivalent of range. On the other hand, apply doesn’t really translate well. Sure, Elixir has it. But Erlang (and thus Elixir) doesn’t have the concept of functions that take arbitrary numbers of arguments, the way * in lisp can yield the product of an arbitrary number of numbers:

(* 1 2 3 4 5)

Instead, Kernel.* is a strictly binary operator. So I have to cheat and use Enum.reduce, which is a recursive function under the covers.

Exercise 5: Various sequence functions

I’m going to quickly run through these just so I know what the Elixir equivalents are. I’ll use the Stream versions when they exist, since the Clojure versions demonstrated in the book all operate on potentially lazy sequences.

test "1.18-5: sequence functions" do
  # take
  assert(Enum.take([1,2,3], 2) == [1,2])
  # distinct
  assert(Enum.uniq([1,2,1,3,2]) == [1,2,3])
  # concat
  assert(Stream.concat([[1,2], [3,4]]) |> Enum.take(4) == [1,2,3,4])
  # repeat
  xs = Stream.repeatedly(fn -> "x" end)
  assert(xs |> Enum.take(3) == ["x", "x", "x"])

  # interleave
  # there appears to be no interleave. There's Enum.zip, which only
  # zips two collections, and isn't lazy(?).

  # drop
  assert((1..4) |> Enum.drop(2) == [3,4])

  # drop-last
  assert((1..4) |> Enum.slice(0..-2) == [1,2,3])

  # flatten
  assert(List.flatten([[1,2], [3,4]]) == [1,2,3,4])

  # partition
  assert((1..10) |> Enum.partition(&Integer.even?(&1)) == {[2,4,6,8,10], [1,3,5,7,9]})

  # every?
  assert([2,4,6] |> Enum.all?(&Integer.even?(&1)) == true)
  assert([1,4,6] |> Enum.all?(&Integer.even?(&1)) == false)

  # remove
  assert((1..10) |> Stream.reject(&Integer.even?/1) |> Enum.take(5) == [1,3,5,7,9])
end

These translations were delightfully easy to do; in almost every case, the Elixir version of the Clojure function either had a) the same name; or b) the name of the equivalent operation in Ruby (e.g. remove becomes reject).

One thing that has stood out as I’ve worked through these is that the Stream module is a lot more limited than the Enum module. And from my brief experimentation, Enum functions are not lazy. So, for instance, this expression will never return, even though we are only trying to take the first three unique items from the stream:

[1,2,3,4,5] |> Stream.cycle |> Enum.uniq |> Stream.take(3)

Evidently Enum.uniq tries to convert the stream into a fixed collection, rather than returning a filtered stream in this case.

Since FPOO is already talking about laziness a lot (and all sequences seem to be treated as lazy and potentially infinite in Clojure), this may become a problem for later examples. More broadly, this doesn’t bode well for writing truly generic functions that can process any kind of collection, including streams, in Elixir. However, Elixir is still very young, and I suspect that the Stream library will grow with time.

Exercise 6: prefix-of?

test "1.18-6: prefix-of?" do
  assert(prefix_of?([1,2], [1,2,3,4]) == true)
  assert(prefix_of?([2,3], [1,2,3,4]) == false)
end
def prefix_of?(candidate, sequence) do
  import Enum
  size   = count(candidate)
  subseq = sequence |> take(size)
  candidate == subseq
end

Exercise 7: tails

test "1.18-7: tails" do
  assert([1,2,3,4] |> tails == [[1,2,3,4], [2,3,4], [3,4], [4], []])
end
def tails([_|xs] = sequence), do: [sequence|tails(xs)]
def tails([]), do: [[]]

Marick says “my solution is very much in the functional style”, and then goes on to offer some hints having to do with using Clojure’s range and map. I’m going to go out on a limb and say that the solution I came up with first is even more in the functional style, since it relies entirely on destructuring and recursion and doesn’t require any library calls at all.

Just for fun, here’s a more direct translation of Marick’s solution:

def tails2(seq) do
  import Enum
  0..count(seq) |> map(&drop(seq, &1))
end

His solution is actually a bit more involved than this, because it involves mapping over both the range 0..count(seq) and a repeated list of the sequence itself. I’m guessing this is because he hasn’t yet explicitly introduced lambdas apart from the top-level function definitions.

(def tails
  (fn [seq]
    (map drop
         (range (inc (count seq)))
         (repeat (inc (count seq)) seq))))

If nothing else, this demonstrates that Clojure’s map can map over multiple sequences in parallel, which is kinda cool.

5 comments

Leave a Reply

Your email address will not be published. Required fields are marked *