Fl4m3Ph03n1x

Fl4m3Ph03n1x

How to define Macro for a new Type?

Background

So, I am playing around with a concept named “NewType” and I am taking inspiration from languages like F# and Scala.

My objective, for learning purposes mostly, is to build a macro that makes creating this abstraction something that takes no more than a single line of code.

Intended usage

I would like to create a macro that allows me to do something like this:

defmodule User do
  require NewType # an absolutely original name for the macro :D

  deftype Name, String.t() # Usage of said macro. Here I am defining a new type called "Name"

  @enforce_keys [:name, :age]
  defstruct [:name, :age]
  @type t :: %__MODULE__{
          name: Name.t,
          age: integer()
        }

  @spec new(Name.t, integer) :: User.t
  def new(name, age), do: %User{name: name, age, age}  
end

And now, here is how I could create a User:

defmodule Test do
  alias User
  import User.Name

  @spec run :: User.t
  def run do
    name = Name("John")
    User.new(name, 25)
  end
end

How to implement this interface?

This interface might remind you a little of the Record interface. That’s because I think its API has some good ideas I would like to explore.

So, as a starting point I tried reading the source code for Record, but I was not really able to pick it up and use it to create an implementation for my use case, mainly because I don’t need/want to interface with Erlang records at all.

So, an implementation possibility would be to, under the hood, turn this into a tuple:

defmodule NewType do
  defmacro new(name, val) do
    quote do
      NewType.to_tuple(unquote(name), unquote(val))
    end
  end

  def to_tuple(name, val), do: {String.to_atom(name), val}
end

However, this is miles away from the interface I want to create …

Questions

  1. Using Elixir macros, is it possible to create the API I am aiming for?
  2. How can I change my code to achieve something like Name("John")?

Marked As Solved

Fl4m3Ph03n1x

Fl4m3Ph03n1x

My Answer

After reading more about macros in Elixir, talking to the community and reading about NewType, I have refined my ideas. While the exact implementation of my original idea is not possible, with some changes you can still get the core benefit of NewType.

Changes to original idea

  • No usage of Name("John") syntax. As explained in this post this syntax is not valid in Elixir.
  • No defguard. Because the type is @opaque it is not possible to have a guard that analyses the internal structure of the data without having dialyzer complaining. Since the main goal here is to have Dialyzer help me detect issues, and since the internal structure of the opaque data can only be analyzed by functions that belong to the module itself, this means this idea is not possible.
  • No verification on data type when invoking new. Originally I thought about having some verification mechanism, but this is not necessary, since dialyzer will let the user know if the user is invoking new with an incorrect parameter.
  • No self-generated functions. Instead of having Age.age? or Name.name? I have opted for the more general NewType.is_type?/2, which will accomplish the same and is more general.

Code

With these changes in mind, this is the macro I came up with:

defmodule NewType do
  defmacro deftype(name, type) do
    quote do
      defmodule unquote(name) do
        @opaque t :: {unquote(name), unquote(type)}

        @spec new(value :: unquote(type)) :: t
        def new(value), do: {unquote(name), value}

        @spec extract(new_type :: t) :: unquote(type)
        def extract({unquote(name), value}), do: value
      end
    end
  end

  @spec is_type?(data :: {atom, any}, new_type :: atom) :: boolean
  def is_type?({type, _data}, new_type) when type == new_type, do: true
  def is_type?(_data, _new_type), do: false
end

Which can be used like:

type.ex:

defmodule Type do
  import NewType

  deftype Name, String.t()
end

test.ex:

defmodule Test do
  alias Type.Name

  @spec print(Name.t()) :: binary
  def print(name), do: Name.extract(name)

  def run do
    arg = 1
    name = Name.new(arg) # dialyzer detects error !
    {:ok, name}
  end
end

Also Liked

dimitarvp

dimitarvp

Sorry to comment on the previous idea – but that’s way too global. In a moderately big project you can have 5 different kinds of names. So to implement this properly you’d likely have to scope this somehow e.g. with module names (which are also atoms as you know):

defmodule MyApp.Types.Address.Name do
  def name?({__MODULE__, val}) when is_binary(val), do: true

In many languages, internally types get all sorts of fancy prefixes. That’s important. You don’t want only one Name to be ever allowed in your project.

Elixir offers an even neater way of doing this:

def is_type?({type, _data}, type), do: true # note the repeated `type` variable
def is_type?(_data, _new_type), do: false

Examples:

is_type?({:x, "something"}, :x} # returns true
is_type?({:x, "something"}, :y} # returns false

OvermindDL1

OvermindDL1

Elixir doesn’t really have type dispatch though as it has no static type system, so I’m unsure what any kind of NewType could accomplish with this short of just {:mytype, values} or so as the purpose of NewType’s are to specify a unique set of functionality while hiding the original functionality of a specific type.

Fl4m3Ph03n1x

Fl4m3Ph03n1x

This is what I would ideally like to go for, but using a macro. The macro would convert this “type” to a tuple, and then Dialyzer would be able to pick up miss matches.

I understand the pattern of NewType as is commonly defined is mostly applicable to static languages, but that does not mean we can not learn good lessons from it and adapt to Elixir, in a way that accomplishes the same :smiley:

Where Next?

Popular Backend topics Top

dimitarvp
As a part of my new job I’ll have to learn to manage a local k8s cluster. The tools used are microk8s, tilt and helm. I’ll appreciate an...
New
DevotionGeo
I know that -t flag is used along with -i flag for getting an interactive shell. But I cannot digest what the man page for docker run com...
New
joshi
Hey everybody! I’m working on the project that includes import of Oracle data to PostgreSQL. That data comes as Oracle export (expdp) fi...
New
Fl4m3Ph03n1x
Background While playing around with dialyzer, typespecs and currying, I was able to create an example of a false positive in dialyzer. ...
New
conradwt
Hi, I’m building an application that will have support for both the web and mobile. At this time, I’m using PhxGenAuth for authenticatio...
New
JimmyCarterSon
Hello, I am working on a new application with Elixir, Dish_out. I want to see Data I follow this tutorial with Elixir Casts. However, I ...
New
Fl4m3Ph03n1x
Background I have recently been delving into more functional code. My objective right now is to get something similar to the IO Monad (in...
New
sona11
I’m having a difficulty. I want to modify an attribute’s data type from String to Array. { “id”: “trn:tarb:tradingpartner:uuid:00000...
New
Fl4m3Ph03n1x
Background I am a fan of dialyzer and friends (looking at Gradient) and I try to have sepcs in my code as much as I can. To this end, I a...
New
theodor
I have this app where there are a bunch of users that cannot access their account anymore after they updated their Android device to Andr...
New

Other popular topics Top

Devtalk
Hello Devtalk World! Please let us know a little about who you are and where you’re from :nerd_face:
New
PragmaticBookshelf
Stop developing web apps with yesterday’s tools. Today, developers are increasingly adopting Clojure as a web-development platform. See f...
New
PragmaticBookshelf
Ruby, Io, Prolog, Scala, Erlang, Clojure, Haskell. With Seven Languages in Seven Weeks, by Bruce A. Tate, you’ll go beyond the syntax—and...
New
PragmaticBookshelf
Write Elixir tests that you can be proud of. Dive into Elixir’s test philosophy and gain mastery over the terminology and concepts that u...
New
AstonJ
I’ve been hearing quite a lot of comments relating to the sound of a keyboard, with one of the most desirable of these called ‘thock’, he...
New
PragmaticBookshelf
Build highly interactive applications without ever leaving Elixir, the way the experts do. Let LiveView take care of performance, scalabi...
New
AstonJ
Saw this on TikTok of all places! :lol: Anyone heard of them before? Lite:
New
foxtrottwist
A few weeks ago I started using Warp a terminal written in rust. Though in it’s current state of development there are a few caveats (tab...
New
PragmaticBookshelf
Develop, deploy, and debug BEAM applications using BEAMOps: a new paradigm that focuses on scalability, fault tolerance, and owning each ...
New
PragmaticBookshelf
Use advanced functional programming principles, practical Domain-Driven Design techniques, and production-ready Elixir code to build scal...
New