small medium large xlarge

Dave_gnome_head_isolated_pragsmall
15 Jul 2013, 03:42
Dave Thomas (380 posts)
  • (Hard). Try adding a method definition with a guard clause to the Test module. You’ll find that the tracing now longer works.

    • Find out why
    • See if you can fix it

    (You may need to explore Kernel.def/4)

Ernie2_pragsmall
01 May 2014, 23:28
Ernie Miller (4 posts)

I struggled with this one for a half an hour or so. So:

If you need further help, here’s a function with a guard:

def add_list_with_guard(list)
when length(list) > 3 do
  Enum.reduce(list, 0, &(&1 + &2))
end

…and here is its representation before unquoted:

{:when, [line: 37],
 [{:add_list_with_guard, [line: 36], [{:list, [line: 36], nil}]},
  {:>, [line: 37], [{:length, [line: 37], [{:list, [line: 37], nil}]}, 3]}]}

Consider what the pattern matching in your macro is then considering its definition “name” and “args”. Then, consider whether or not you might be able to define a clause for the function that takes this into account.

Generic-user-small
25 Aug 2015, 16:50
Jim Kane (6 posts)

Based on Ernie’s helpful hints, I got partway there (the lack of other posts tells me I may not be the only one who did so).

Based on the dump of the function, a guard clause looks like the last element of its tuple is an array of more functions. We can assume that the head is the outer, and the tail is the list of guard functions. So, the function def ought to look something like this:

defmacro def(definition={:when, _, [outer_function | guard_functions]}, do: content) do
  quote do
    Kernel.def(unquote(outer_function)) do
    if Enum.all?(guards) do
      result = unquote(content)
      IO.puts "<== result: #{result}"
      result
    else
      IO.puts "<== guard clause failed, no result"
      nil
    end
  end
end

Before you rush to type this example in,… it does not work. Using defmacro requires you to mentally track whether you are writing a function or writing a function to write the function, and I am still working out which line I failed on (my guess is on the Enum.all/1 call). “Ceci n’est pas une pipe.”

Hopefully some other reader will find this sketch useful on top of Ernie’s initial hints. I find this part of Elixir fascinating!

Generic-user-small
02 Dec 2015, 16:35
Aaron Kuehler (2 posts)

Thanks Ernie and Jim for getting me started on this one. I added a bit to mimic the behavior when the guard clause fails.

tracer_6.ex

defmodule Tracer do
  defmacro def({ :when, _, [ function = { name, _, arguments } | guard_functions ] }, do: content) do
    [ caller | _ ] = __CALLER__.context_modules
    quote do
      Kernel.def(unquote(function)) do
        all_guards_passed = Enum.all?(unquote(guard_functions), &( &1 == true ))
        if(all_guards_passed) do
          unquote(content)
        else
          raise FunctionClauseError,
            module: unquote(caller),
            function: unquote(name),
            arity: unquote(length(arguments))
        end
      end
    end
  end
  defmacro __using__(_opts) do
    quote do
      import Kernel,              except: [ def: 2 ]
      import unquote(__MODULE__),   only: [ def: 2 ]
    end
  end
end

defmodule Test do
  use Tracer

  def echo_large_list(list)
  when length(list) > 2 do
    list
  end
end

iex

iex(1)> c "tracer_6.ex"
tracer_6.ex:1: warning: redefining module Tracer
tracer_6.ex:26: warning: redefining module Test
[Test, Tracer]
iex(2)> Test.echo_large_list([1])
** (FunctionClauseError) no function clause matching in Test.echo_large_list/1
    tracer_6.ex:29: Test.echo_large_list/1
iex(2)> Test.echo_large_list([1,2,3])
[1, 2, 3]

Not necessarily part of this exercise, but that this still doesn’t give feature parity with standard Elixir def. If we try to dispatch with guard clauses, the above solution fails:

defmodule Test do
  use Tracer

  def echo_large_list(list)
  when length(list) > 3 do
    list
  end

  def echo_large_list(list)
  when length(list) > 2 do
    "Holy cats!"
  end
end

iex

iex(1)> c "tracer_6.ex"
tracer_6.ex:1: warning: redefining module Tracer
tracer_6.ex:26: warning: redefining module Test
tracer_6.ex:34: warning: this clause cannot match because a previous clause at line 29 always matches
[Test, Tracer]
iex(2)> Test.echo_large_list([1,2,3])
** (FunctionClauseError) no function clause matching in Test.echo_large_list/1
    tracer_6.ex:29: Test.echo_large_list/1
iex(2)> Test.echo_large_list([1,2,3,4])
[1, 2, 3, 4]

I didn’t dig deep enough into how Elixir does this to be able to replicate it in our Tracer.def. Maybe someone else has some ideas on this?

Generic-user-small
24 Jan 2016, 13:44
Stefan Chrobot (13 posts)

Luckily, Kernel.def will handle the guard clause, so it’s enough to pass the whole outer definition to it:

defmodule Tracer do
  def dump_args(args) do
    args |> Enum.map(&inspect/1) |> Enum.join(", ")
  end

  def dump_defn(name, args) do
    "#{name}(#{dump_args(args)})"
  end

  defmacro def(definition={:when, _, [nested_definition={name, _, args}, _guard_clause]}, do: content) do
    def_impl(definition, name, args, content)
  end

  defmacro def(definition={name, _, args}, do: content) do
    def_impl(definition, name, args, content)
  end

  defp def_impl(definition, name, args, content) do
    quote do
      Kernel.def(unquote(definition)) do
        IO.puts "==> call:   #{Tracer.dump_defn(unquote(name), unquote(args))}"
        result = unquote(content)
        IO.puts "<== result: #{result}"
        result
      end
    end
  end

  defmacro __using__(_opts) do
    quote do
      import Kernel, except: [def: 2]
      import unquote(__MODULE__), only: [def: 2]
    end
  end
end

defmodule Test do
  use Tracer
  def puts_sum_three(a,b,c), do: IO.inspect(a+b+c)
  def add_list(list),        do: Enum.reduce(list, 0, &(&1+&2))
  def is_big(x) when is_number(x) and x > 10, do: true
end

Test.puts_sum_three(1,2,3)
Test.add_list([5,6,7,8])
Test.is_big(20)
Test.is_big(10) # fails with proper error
You must be logged in to comment