How to not import `with` from Kernel so it can be redefined

Because the majority of elixir is written as macro’s I am aware it is possible to exclude them from import and then define your own versions.

i.e.

# iex

import Kernel, except: [+: 2]
# Kernel

2 + 5
# ** (CompileError) iex:2: undefined function +/2

The same method does not work for with, is there anyway to overwrite the native with construct?

# iex

import Kernel, except: [with: 2]
# Kernel

# with still available??
iex(2)> with a = 5, do: a
# 5

1 Like

with is a special form and it is defined in Kernel.SpecialForms. It is not possible to override special forms. They are “special” because they cannot be implemented on top of existing constructs, they are required to bootstrap the language.

5 Likes

Ok so in summary, with is a special form and not a macro. I assume there must be some property of with that prevents it from being defined as a macro? What would that be?

1 Like

As a side note: What you can do is create a macro that reads in AST including with used in a custom way. As long as its syntax will look similar enough to the way the built-in with is used, this will work, and your macro can alter the AST in any way it sees fit (such as rewriting the passed with to what you want it to represent).
Of course, an important note to make, is that writing macros like these somewhat go against the explicit nature of idiomatic Elixir; simply using a different name for the thing you want to define is almost always a better idea.

3 Likes

Can happily agree with this as a solution, and the one I will probably use. However now I have started looking I am interested in solving the hard way purely for curiosity. My alternative with is defined here I think because it doesn’t have commas it might be quite a job to manipulate the ast as you suggest

1 Like

@Crowdhailer Yes, exactly. All special forms have one or more properties that would make it impossible or undesirable to be implemented as a macro. For with, it accepts a variable number of arguments, which is a feature that is not available to any macro.

The term came from lisps, which also have special forms.

There is a very simple variation of this and not as invasive which is to prefix the with special form with a macro and that way you can emulate var args:

async with left <- right,
          foo <- bar do
  {left, foo}
end
2 Likes

Thanks, I hadn’t thought of that solution. Although at that level I think the cleanest might to simply leave my version under its module namespace

MyMod.with left <- right,
          foo <- bar do
  {left, foo}
end
1 Like

That won’t work as varargs though. :sweat_smile:

1 Like

My redefinition of with doesn’t use variable args, (fortuitously I guess) and is therefore implementable as a macro.

2 Likes

Then yes!

2 Likes

I’ve been curious why some things, like with, are special forms when they can fairly easily be done via macro’s? Take the happy library, that gives you a happy_path macro, which is basically with (implemented internally via nested cases). Wouldn’t making with a macro have been easier? Or why was it made a special form in the first place? The variable arguments is not really necessary as everything can be done within and inside of a block like happy did?

As an example from online take this Elixir standard with:

with {:ok, binary} <- File.read(path),
     {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
     do: {:ok, wrap(data)}

Or perhaps compare to this:

with do
  {:ok, binary} <- File.read(path)
  {:ok, data} <- :beam_lib.chunks(binary, :abstract_code)
  {:ok, wrap(data)}
end

First of all the elixir way as it is currently is really really hated by auto-indentors in emacs/vi/atom. Second the first method does not fit with the style of case and other things. Third the second method has a block and reads like normal code but with weird <- for matching, but no weird comma’s needed, the last expression is what is returned, like everywhere else in Elixir. Fourth, you can even pass in options easily, like perhaps an extension that allows you to pass in an unhandled error handler or so:

with unhandled_errors: &handle_unhandled_errors(conn, &1), do
  {:ok, binary} <- File.read(path)
  {:ok, data} <- :beam_lib.chunks(binary, :abstract_code)
  {:ok, wrap(data)}
end

Which is also consistent with other things in Elixir.

There are other reasons too but I’m getting about a frame every 4 seconds while typing this in either Chrome or Edge (I really really hate Windows) so I’ve no clue what I’ve even typed yet as it has not shown up yet. ^.^

1 Like

Because we (the Elixir team) find the with syntax more natural than the one you proposed. There is nowhere in Elixir where we perform block rewriting (such as rewriting the contents inside do) and the with syntax was designed to mirror comprehensions. Folks may disagree on which one feels more natural but that’s the answer nonetheless. :slight_smile:

Options are supported in both syntaxes (see for comprehensions that also support options) and hopefully someone can work on fixes for editors.

2 Likes

Oh I hope so. ^.^

Right now I write with’s like this so that highlighting and identing works properly. :slight_smile:

with(
  {:ok, binary} <- File.read(path),
  {:ok, data} <- :beam_lib.chunks(binary, :abstract_code),
  do: {:ok, wrap(data)}
)

Though I still question why something could even be multi-arity at all, I think that is why the , in with and for looks so very unnatural to me. o.O

2 Likes

I also thought about this variation, but it returns ** (CompileError) missing :do option in "with". Is there anything I am missing? Thanks!

Perhaps the do … end?

Without showing more code, we can’t tell, but I think after we had half a year of silence in this thread, you should open a new thread for this.

I found it.

I was defining the async macro as async/1. However, after inspecting the AST, I saw that the do block is passed to the assign macro instead of the with special form. Changing my macro to async/2 fixed the problem.

1 Like

The reason why is that the ‘do’ binds to the macro call since you do not have parenthesis. Putting parenthesis around the entire inner of the macro will work as expected, or using a do expression in a parenthesized with would too. But yep, I use this style quite a lot. ^.^;

Here is the final result https://github.com/fertapric/async_with

It’s an example of prefixing the with special form with a macro, hope it helps!

2 Likes