saleyn

saleyn

How can I create a macro that defines a type using atom that represents the type name? In the macro __type__ below, for the visibility argument :private, I’d like the macro to emit:
@typep t() :: { ... }

def visibility(:public),  do: :type
def visibility(:private), do: :typep
def visibility(:opaque),  do: :opaque

defmacro __type__(types, visibility) do
  attr = visibility(visibility)
  quote bind_quoted: [attr: attr, types: types] do
    Module.put_attribute(__MODULE__, unquote(attr), t() :: {unquote_splicing(types)}
  end
end

Compilation fails with:

== Compilation error in file test/type_test.exs ==
** (CompileError) test/type_test.exs:49: misplaced operator ::/2

The :: operator is typically used in bitstrings to specify types and sizes of segments:

    <<size::32-integer, letter::utf8, rest::binary>>

It is also used in typespecs, such as @type and @spec, to describe inputs and outputs
    ... expanding macro: Mod.__type__/2

Showing Posts 1 to 8

josevalim

josevalim

Creator of Elixir

You want to emit the actual type notation, something like this:

@unquote(attr)(t() :: {unquote_splicing(types)})
saleyn

saleyn OP

defmacro __type__(types, visibility) do
  attr = Mod.visibility(visibility)
  quote bind_quoted: [attr: attr, types: types] do
    @unquote(attr)(t() :: {unquote_splicing(types)})
  end
end

For this invocation I get:

== Compilation error in file test/type_test.exs ==
** (CompileError) test/type_test.exs:79: unquote called outside quote
    (elixir 1.13.2) expanding macro: Kernel.@/1
ityonemo

ityonemo

You can’t mix bind_quoted with unquoutes.

saleyn

saleyn OP

How would you rewrite the above to keep the compiler happy?

Eiji

Eiji

Here you go:

defmodule Example do
  defmacro sample(ast, visibility) do
    # sample visibility handling
    attr =
      case visibility do
        :public -> :type
        :private -> :typep
      end

    # this optional line allows you to use sigils:
    # Example.sample(~w[a b c]a, :private)
    list = Macro.expand(ast, __CALLER__)

    # if unuoqte needs to be used which is the case here
    # then quote cannot have bind_quoted option set
    # as this option sets unquote option to false
    # making unquote/1 calls not work
    quote do
      # @ is a macro which means we need to use unquote here
      # to pass a raw data instead of ast not recognized by said macro
      @unquote(attr)(t() :: {unquote_splicing(list)})
    end
  end
end

Helpful resources:

  1. @/1 macro
  2. quote/2 special form
saleyn

saleyn OP

How would you rewrite it if the first argument to your sample/2 macro is actually the content of a dynamically built attribute? E.g.

def prep_sample() do
  quote do
    Module.register_attribute(__MODULE__, :types, accumulate: true)
  end
  Module.put_attribute(mod, :types, {:a, String.t()})
  Module.put_attribute(mod, :types, {:b, atom()})
  Module.put_attribute(mod, :types, {:c, integer()})
end

And I essentially want to make the type containing the types taken from the types attribute in the “pseudo” code below:

defmacro sample(types, visibility) do
  attr =
    case visibility do
      :public -> :type
      :private -> :typep
    end
  list = for {_,type} <- types, do: type
  quote do
    @unquote(attr)(t() :: {unquote_splicing(list)})
  end
end

def define_type(visibility) do
  prep_sample()
  sample(Module.get_attribute(__MODULE__, :types), visibility)
end
Eiji

Eiji

Your code would not even compile. Also you can put module attributes only when mod is does not yet finished compiling. quote/1 inside a function should be returned (as last expression) when said function is called by macro. Otherwise in function body you would have a quoted expression which would not be evaluated.

Maybe you look for something like this?

defmodule Example do
  defmacro __using__(_opts \\ []) do
    quote do
      import Example, only: [type: 2, using: 1, using: 2]
      Module.register_attribute(__MODULE__, :_types, accumulate: true)
    end
  end

  defmacro type(name, ast) do
    quote bind_quoted: [ast: Macro.escape(ast), name: name] do
      @_types {name, ast}
    end
  end

  defmacro using(opts \\ [visibility: :public], do: block) do
    attr = visibility(opts[:visibility])

    quote bind_quoted: [attr: attr, block: block] do
      _ = block
      list = @_types |> Enum.map(&elem(&1, 1)) |> Enum.reverse()

      case attr do
        :opaque -> @opaque t() :: {unquote_splicing(list)}
        :type -> @type t() :: {unquote_splicing(list)}
        :typep -> @typep t() :: {unquote_splicing(list)}
      end
    end
  end

  defp visibility(:opaque), do: :opaque
  defp visibility(:private), do: :typep
  defp visibility(:public), do: :type
end

defmodule Test do
  use Example

  using visibility: :private do
    type :a, String.t()
    type :b, atom()
    type :c, integer()
  end
end
saleyn

saleyn OP

Thanks Eiji! What prompted this question was my modification of the typed_struct library that adds support of typedrecord, e.g.:

defmodule Person do
  use TypedStruct

  typedrecord :person do
    @typedoc "A person"

    field :name, String.t(),
    field :age,  non_neg_integer(), default: 0
  end
end

The code above will be expanded to:

defmodule Person do
  use Record
  Record.defrecord(:person, name: nil, age: 0)
  @type person :: {:person, String.t, non_neg_integer}
end

There were two places in my implementation that I wanted to improve: this and this to avoid repetition.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
psy-q
I’m trying to set up Emacs with elixir-ls via lsp-mode and credo via Flycheck. This should mostly be preconfigured as Flycheck picks up c...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews