Fl4m3Ph03n1x

Fl4m3Ph03n1x

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")?

Showing Posts 1 to 9

LostKobrakai

LostKobrakai

That’s not possible with the uppercase first letter. Any call to a macro or function needs to start with a non uppercase letter.

Non-qualified calls, such as add(1, 2) , must start with an underscore or a Unicode letter that is not in uppercase or titlecase.

Qualified calls, such as Math.add(1, 2) , must start with an underscore or a Unicode letter that is not in uppercase or titlecase.

Syntax reference

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Can’t a Macro generate a Module’s Name dynamically?

Let’s say, if I have a struct, couldn’t a Macro deftype, given an Uppercase name, dynamically generate this struct for me?

LostKobrakai

LostKobrakai

You can generate modules, but you cannot “call” modules. You can only call macros or functions. So Name("…") is not valid elixir syntax – Name.new("…") is.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Right!
A revision of my basic idea is needed !

defmodule Test do
  alias User
  import User.Name

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

This should possible, right ?

lud

lud

Yes.

Start by writing the Name module by hand. That will show you what your macro would have to generate.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Here, I have defined a couple of hypothetical types, Name and Age. Their respective modules would be:

name

defmodule Name do
  @opaque t :: {:name, String.t()}

  @spec new(val :: String.t()) :: t
  def new(val) when is_binary(val), do: {:name, val}

  @spec extract(name :: t) :: String.t()
  def extract({:name, val}), do: val

  @spec name?(data :: any) :: boolean()
  def name?({:name, val}) when is_binary(val), do: true
  def name?(_data), do: false

  @spec is_name(data :: any) ::
          {:__block__ | {:., [], [:andalso | :erlang, ...]}, [],
           [{:= | {any, any, any}, [], [...]}, ...]}
  defguard is_name(value)
           when is_tuple(value) and elem(value, 0) == :name and is_binary(elem(value, 1))
end

which would be generated by invoking:

deftype Name, String.t() 

age

defmodule Age do
  @opaque t :: {:age, non_neg_integer()}

  @spec new(val :: non_neg_integer()) :: t
  def new(val) when is_integer(val), do: {:age, val}

  @spec extract(name :: t) :: non_neg_integer()
  def extract({:age, val}), do: val

  @spec age?(data :: any) :: boolean()
  def age?({:age, val}) when is_integer(val), do: true
  def age?(_data), do: false

  @spec is_age(data :: any) ::
          {:__block__ | {:., [], [:andalso | :erlang, ...]}, [],
           [{:= | {any, any, any}, [], [...]}, ...]}
  defguard is_age(value)
           when is_tuple(value) and elem(value, 0) == :age and is_integer(elem(value, 1))
end

which would be generated by using:

deftype Age, non_neg_integer() 

Do note a few things:

  • The name of the module is used to create functions and guards
  • We pass the value of the type in the macro

Would it be possible to achieve this, using macros?
If so, how?

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Probably. Mutually recursive types however may well be difficult to do with this.

A lot of macro work. I’d start with the Metaprogramming Elixir book.

As an aside though: I’d seriously consider looking at Gleam. It’s a BEAM language w/ static types. If you want to push forward the boundaries of what will be possible with types on the BEAM I imagine that work in that ecosystem is going to be more fruitful than attempting to make it work in Elixir. If Jose tried to make it work and failed, I would think long and hard about taking it on yourself.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

Can you elaborate more on this?

I was hoping there was a quicker way to achieve this goal, one that does not involve reading a full book. My read list is already big enough :stuck_out_tongue:

I absolutely like the idea of Gleam, however the ecosystem is not there yet. You might say that Gleam is compatible with Elixir and Erlang libraries, but then if half of my project is in Gleam and the other half in Elixir, I just don’t see how that can end up well.

I don’t compare myself with people in general, but I was always under the impression that Jose never intended Elixir to be statically typed, or to even have many of the features static type languages have (please correct me if I am wrong).

While I am not trying to one up anyone, I am also not trying to re-invent the wheel. I am, from my humble point of view, trying to bring a concept generalized and well known in many other functional languages into Elixir, adapting it to what some could consider to be “in an Elixir way”.

At the end, there is a good chance you might be correct, and that I am in a fool’s errand. However I will never find out if I don’t try.

Fl4m3Ph03n1x

Fl4m3Ph03n1x OP

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
— 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
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
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
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 & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews