Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to recreate erlang's :math functions as elixir macros?

I am in the process of creating a macro that will calculate the distance between two sets of lat-long values.

iex()> calc_distance(posA, posB)
2  # distance is in km

At the moment this is working similar to a regular function. The reason I would like it to become a macro is so that I can use it in a guard clause. e.g.

fn(posA, posB) when calc_distance(posA, posB) < 10 -> "close enough" end

However, for the macro to be used in a guard clause it has to "follow the rules". This means that many functions and operators are not allows to be used.

My initial macro looked like so...

defmacro calc_distance(ll1, ll2) do
  quote do
    lat1 = elem(unquote(ll1), 0)
    long1 = elem(unquote(ll1), 1)
    lat2 = elem(unquote(ll2), 0)
    long2 = elem(unquote(ll2), 1)

    v = :math.pi / 180
    r = 6372.8

    dlat  = :math.sin((lat2 - lat1) * v / 2)
    dlong = :math.sin((long2 - long1) * v / 2)
    a = dlat * dlat + dlong * dlong * :math.cos(lat1 * v) * :math.cos(lat2 * v)
    res = r * 2 * :math.asin(:math.sqrt(a))
    res
  end
end

I have begun making it "guard clause friendly" by removing all variables that were being defined in the macro.

defmacro calc_distance(ll1, ll2) do
  quote do
    :math.sin((elem(unquote(ll2), 1) - elem(unquote(ll1), 1)) * (3.141592653589793 / 180) / 2)
    |> square()
    |> Kernel.*(:math.cos(elem(unquote(ll1), 0) * (3.141592653589793 / 180)))
    |> Kernel.*(:math.cos(elem(unquote(ll2), 0) * (3.141592653589793 / 180)))
    |> Kernel.+(square(:math.sin((elem(unquote(ll2), 0) - elem(unquote(ll1), 0)) * (3.141592653589793 / 180) / 2)))
    |> :math.sqrt()
    |> :math.asin()
    |> Kernel.*(2)
    |> Kernel.*(6372.8)
  end
end

This still works as a macro but is still giving an error when I try to use it as a guard clause because of the :math functions being used.

If I could write my own version of this functions as macros this would solve the issue.

Does anyone know if this is possible? If so, how might I go about this?

like image 391
RobStallion Avatar asked Mar 06 '19 14:03

RobStallion


1 Answers

No, it is not possible to implement this as a guard test.

Or well, it is possible if you allow for loss of accuracy: this approximation of the sine function could be implemented using only operations allowed in guards.

But most likely, in your program accuracy is a higher priority than saving a few lines of code. In this case, I would probably make my function call call_distance and pass the result as a parameter to another function, which could use guard tests on the result:

def my_function(ll1, ll2) do
    my_function(ll1, ll2, calc_distance(ll1, ll2))
end

defp my_function(ll1, ll2, distance) when distance < 10 do
    "close enough"
end
defp my_function(ll1, ll2, distance) do
    "too far"
end
like image 159
legoscia Avatar answered Sep 26 '22 13:09

legoscia