Is there a workaround to the inner anonymous function of a closure not being able have a default parameter or clauses with different arities

I’m trying to create a closure that takes in a html tag such as “p” or “h1” and returns an anonymous function that can take a string (to go between the tags) and an optional string of tag attributes such as “style=“color: green;””.

I have tried to do this with an optional parameter and see that this can’t be done and I have also tried clauses with different arities and see this can’t also be done.

Here is my effort with an optional parameter:

defmodule Wrapper do
 
  def html(tag) do
    fn
      (str, attr \\ "" )  ->  "<#{tag} #{attr}>" <> str <> "<#{tag}>"
    end
  end
end

And here is my effort with two arities in the anonymous function

defmodule Wrapper do
 
  def html(tag) do
    fn
      (str, attr)  ->  "<#{tag} #{attr}>" <> str <> "<#{tag}>"
      (str) -> "<#{tag}>" <> str <> "<#{tag}>"
    end
  end
end

And used like:

h1 = Wrapper.html("h1" )
h1.("I'm a green h1 title", "style=\"color: green;\"") |> IO.puts
h1.("I'm a title")  |> IO.puts

Is there another way of doing what I need here? I don’t actually need this other than learning.


Edited: spelling

I do not know the answer to your question but I think in Elixir people tend to do more macros than high-order functions. Macros should run faster too.

For example, you can do:

  def element(s, tag, text, attrs) when is_binary(text) do
    start_tag = "<#{tag}#{attr_string(attrs)}>"
    end_tag = "</#{tag}>\n"
    [end_tag | text([start_tag | s], text)]
  end

[:div, :h1]
  |> Enum.each(fn k ->
    @doc ~s"""
    build non-void element #{to_string(k)}
    """
    defmacro unquote(k)(s, inner, attrs \\ []) do
      str = to_string(unquote(k))

      quote do
        element(unquote(s), unquote(str), unquote(inner), unquote(attrs))
      end
    end
  end)

This code is a snippet from html_writer that more or less do what you are trying to do.

2 Likes

Thanks, interesting. I haven’t got to Macros yet in my Elixir studies but this looks like it will be good to come back to when I do.

A simple idea could be to “simulate” different arities with tuples:

defmodule Wrapper do

 def html(tag) do
   fn
     {str, attr}  ->  "<#{tag} #{attr}>" <> str <> "<#{tag}>"
     str -> "<#{tag}>" <> str <> "<#{tag}>"
   end
 end
end

Wrapper.html("p").("this is a paragraph")
Wrapper.html("p").({"this is a paragraph with style", "style=\"color: green;\""})
2 Likes

This is fantastic! This will also help me with a logger I’m creating too.
It’s such a clean solution. I’m going to play with that in the morning. Thanks

1 Like

When you define a regular function with default values, what are are actually doing is defining more functions with lesser arities.
so:

def x(a, b, c \\ :c),
  do: {a, b, c}

what is does in reality is

def x(a, b),
  do: x(a, b, :c)

def x(a, b, c),
  do: {a, b, c}

So for Elixir x/2 and x/3 are two different functions that just happen to have the same name (well, the compiler does some annotation so you can know they have been defined by same clause).

So if anonymous functions allowed default arguments, what is the variable that they will be assigned to the other functions of lesser arirty?

For the second part, functions can only have a fixed numbers of arities,
so you cannot have have anonymous functions either with more than one arity (you might have seen this in the error thrown by the compiler).
You can make up for this with tuples and keyword lists.

My take builds on the idea of @trisolaran

defmodule Wrapper do
  def html(tag) when is_binary(tag) do
    fn
      (str) when is_binary(str) ->
        "<#{tag}>#{str}</#{tag}>"

      ({str, attr}) when is_list(attr) ->
        "<#{tag} #{join_attr(attr)}>" <> str <> "</#{tag}>"
    end
  end

  def join_attr(attr, level \\ 1) do
    list = 
      for {key, value} <- attr do
        cond do
          is_list(value) ->
            "#{key}=\"#{Wrapper.join_attr(value, 2)}\""

          level == 1 ->
            "#{key}=\"#{value}\""

          level == 2 ->
            "#{key}: #{value}"  
        end
      end

    case level do
      1 -> Enum.join(list, " ")
      2 -> Enum.join(list, "; ")
    end
  end
end

In IEx:

iex(44)> h1 = Wrapper.html("h1")
#Function<0.76550479/1 in Wrapper.html/1>

iex(45)> h1.("Wow")
"<h1>Wow</h1>"

iex(46)> h1.({"Title", style: "color: red; font-size: 10em", title: "The title"})
"<h1 style=\"color: red; font-size: 10em\" title=\"The title\">Title</h1>"

iex(47)> h1.({"Title", style: [color: "red", "font-size": "10em"], title: "The title"})
"<h1 style=\"color: red; font-size: 10em\" title=\"The title\">Title</h1>"
3 Likes

This is great! I’m going to copy it into my editor now to play with it. I can see that what you have done here can be used in many different scenarios.
Thank you for helping me.

1 Like