Introducing is_kind/2 and guard-safe operators: is, is_not, is_any, are, are_not, are_any

134 views
Skip to first unread message

eksperimental

unread,
Aug 11, 2016, 11:31:12 AM8/11/16
to elixir-l...@googlegroups.com
Hi everyone in this list,

I hope it has a good reception among the community, since it has the
potential to change the way we write functions guards in a very positive
and more natural way.

Guards clauses are a key feature in Elixir. Researching
how to make it easier for developers to define guards, has led me to
two enhancement proposal. This is the first one, which will allow
developers to write guards, guard safe macros and other clauses in a
more natural and succinct way.

All the following macros are allowed in guards:
- `is_kind(term, kind)` determines if a given `term` is of a certain
`kind`.
- `term is kinds` determines if `term` is each kind in `kinds`.
- `term is_not kinds` determines if `term` is not of any of the `kinds`.
- `term is_any kinds` determines if `term` is of any of the `kinds`.
- `terms are kinds` determines if every term in list `terms` is of
every kind in `kinds`.
- `terms are_not kinds` determines if every item in list `terms` is not
of `kind`.
- `terms are_any kinds` determines if every term in list `terms` is not
of any of the `kinds`.

Allowing us to write functions guards as regular code, that otherwise
it would take a really long lines of code:

def check(letter) when letter is :char, do: true

iex> [100, 200] are [:even, {:>=, 100}]
true

write expressions in a more natural way:
iex> term is_not nil

as opposed to
iex> not is_nil(term)

For a list of all supported kinds, see the list:
https://gist.github.com/eksperimental/a6df4348e9675109e49ccf4e34101bfe#list-of-supported-kinds-by-is_kind2

Here's the proposal:
https://gist.github.com/eksperimental/a6df4348e9675109e49ccf4e34101bfe

and here the its full implementation:
https://github.com/eksperimental/elixir/tree/is-kind

Looking forwards to hear your opinions

José Valim

unread,
Aug 11, 2016, 11:47:14 AM8/11/16
to elixir-l...@googlegroups.com
Thank you Eksperimental for a great and detailed proposal with working implementation.

My biggest concern is that the "kinds" are not extensible. We are replacing something that is extensible (defining functions) with an atom-tuple based lookup that is not. Even if guards are somewhat limited, someone could implement is_mfa/1, is_even/1 and is_odd/1 in their codebases today.

Therefore, in your proposal, how could someone add new kinds? If two libraries define conflicting kinds, how can we resolve such conflicts? One option is to make {:kind, 1} to always expand to is_kind(term, 1) but, if that is the case, I would prefer to add defguard that makes it simpler to define guards as regular macros without the kind indirection. I believe we had a defguard proposal flying around at some point.

This proposal also blurries the line between guards and @specs. I would love to have an unified view but I am not sure how such would eventually work and I am worried that any step taken now may make it harder to move to another direction in the future.


José Valim
Skype: jv.ptec
Founder and Director of R&D


--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/20160811223043.7a17eb02.eksperimental%40autistici.org.
For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
Aug 11, 2016, 11:51:46 AM8/11/16
to elixir-l...@googlegroups.com
For future reference, the old defguard proposal: https://github.com/elixir-lang/elixir/issues/2469



José Valim
Skype: jv.ptec
Founder and Director of R&D

Wiebe-Marten Wijnja

unread,
Aug 11, 2016, 1:29:53 PM8/11/16
to elixir-lang-core, eksper...@autistici.org
This is a very interesting proposal!

I really like the flexibility of `is` and `is_any`. I think that
def something_magical(x) when is_any(x, [:integer, :float, :list, :map]) do
  #...
end
reads a whole lot better than
def something_magical(x) when is_integer(x) or is_float(x) or is_list(x) or is_map(x) do
 
#...
end

That being said, I think you're taking it a little too far with specifications like {:list, 1} that check if something is a list having length one. I think that it is clearer to keep these separated. The same goes for the comparison ones like  `{:<=, term}`. Just using `x <=  term` in the guard clause seems clearer.

I like the non-tuple kinds in the 'basic and built-in types' list. I think that things like 'odd', 'even', comparisons and most of the convenience ones are better to be written in their normal longer form.



Finally, when I started reading your post, I expected the kinds to be the module names of the built-in Elixir types that are also used when you want to implement a protocol for a built-in type, like Integer, Float, PID, Tuple, etc.
Of course, when I looked at the documentation, I found out that the reason you didn't was to match @specs more closely, and because there are things like `:zero_or_neg_float` for which there isn't a clear module name.

I think that something like
is_any(x, [List, Map, Tuple])
would be a really interesting and readable guard syntax.

eksperimental

unread,
Aug 12, 2016, 1:23:48 PM8/12/16
to elixir-l...@googlegroups.com
thank you for your answer José,

I can look into how we can add new kinds (I will need help with
defining how to deal with conflicting kinds).
I think the defguard approach is reasonable, but I thought it was
ditched for some reason.

What we could do is create a defkind macro that uses defguard, but
creates kinds only (since defguard can create any kind of guard).

Is there any propototype already, besides
https://gist.github.com/christhekeele/8284977
and what do you think about his approach?

thank you.

On Thu, 11 Aug 2016 17:46:52 +0200
José Valim <jose....@plataformatec.com.br> wrote:

> Thank you Eksperimental for a great and detailed proposal with working
> implementation.
>
> My biggest concern is that the "kinds" are not extensible. We are
> replacing something that is extensible (defining functions) with an
> atom-tuple based lookup that is not. Even if guards are somewhat
> limited, someone could implement is_mfa/1, is_even/1 and is_odd/1 in
> their codebases today.
>
> Therefore, in your proposal, how could someone add new kinds? If two
> libraries define conflicting kinds, how can we resolve such
> conflicts? One option is to make {:kind, 1} to always expand to
> is_kind(term, 1) but, if that is the case, I would prefer to add
> defguard that makes it simpler to define guards as regular macros
> without the kind indirection. I believe we had a defguard proposal
> flying around at some point.
>
> This proposal also blurries the line between guards and @specs. I
> would love to have an unified view but I am not sure how such would
> eventually work and I am worried that any step taken now may make it
> harder to move to another direction in the future.
>
>
> *José Valim*
> > send an email to elixir-lang-co...@googlegroups.com.

José Valim

unread,
Aug 12, 2016, 2:24:54 PM8/12/16
to elixir-l...@googlegroups.com
thank you for your answer José,

I can look into how we can add new kinds (I will need help with
defining how to deal with conflicting kinds).
I think the defguard approach is reasonable, but I thought it was
ditched for some reason.

What we could do is create a defkind macro that uses defguard, but
creates kinds only (since defguard can create any kind of guard).

I think the concern runs a little bit deeper: if we have defkind, that is based on defguard, are they then like any other macro? Would we also manage importing and conflicts with import? If so, why don't we use them like any other macro?

When reading your proposal, I effectively broke it two parts:

1. A huge collection of kinds, which could be implemented as macros. Imagine if we had is_mfa/1, is_list/2 and so on in the stdlib or on a Guards module.

2. A set of operators that easily apply a set of kinds (or macros) on the rhs to the lhs.

Once we break it apart, I have the following feedback:

1. Instead of having a large collection of guards in the stdlib maybe we should make it easier to define your own guards since most times you can give it better names that are specific to your domain

2. What is the benefit of using "is/are" instead of multiple "and/or" calls? I am aware the former is more succinct but remember that if we have multiple and/or calls, then we can always create a new guard with defguard

In other words, instead of having a general solution with a bunch of guards in the stdlib alongside some pre-defined combinations, I would prefer to have a composable solution that is built from small blocks because folks can extend it to their domain without forcing Elixir to add more kinds.

Let me try to be more concrete. With the proposal above, I'd be tempted to write:

defmodule DateTime
  def new(..., seconds) when seconds is [{:gte, 0}, {:lte, 60}] do

But the best solution would most likely be:

defmoduel DateTime
  defguard is_seconds(seconds) when seconds >= 0 and seconds <= 60

  def new(..., seconds) when is_seconds(seconds)
end

Finally, I just want to say that, although I am not convinced on the "kind" mechanism, I love this proposal because it is asking the right questions and it makes me realize I could be writing better code. So thank you for that!

eksperimental

unread,
Aug 12, 2016, 2:27:36 PM8/12/16
to elixir-l...@googlegroups.com
Thank you Wiebe-Marten for your input,
i would like to mention that "is, is_not, is_any, are, are_not,
are_any" are all operators.

So functions are written in a more idiomatic way.

def something_magical(x) when x is_any
[:integer, :float, :list, :map]) do
...
end

regarding kinds that use the tuple-form, it think they are powerful,
and can simplify code a lot. common cases {:function, 1}, {:tuple, 2}

when using is* functions, it's debatable whether it's clearer to go
with the regular "is_" functions,
but look at this example.

def something_special(a, b, c)
when [a, b, c] are_any [:map, {:function, 1}, {:tuple, 2}]) do
...
end

it would look like this if you want to do it the traditional way,
since you cannot associate them inside the are_any operator.

def something_special(a, b, c)
when (a is :map or is_function(a, 1) or (a is :tuple and tuple_size(a) == 2)) and
(b is :map or is_function(b, 1) or (b is :tuple and tuple_size(b) == 2)) and
(c is :map or is_function(c, 1) or (c is :tuple and tuple_size(c) == 2)) do
...
end

sames happens when you need to compare values using {:gt, 10}, or {:===, 10}

Regarding choosing Module names over atoms for kind names, I think the closer we stay to
typespecs the better and easier.

Paul Schoenfelder

unread,
Aug 12, 2016, 3:02:56 PM8/12/16
to elixir-l...@googlegroups.com
Just to throw in my two cents, here's how I define my own custom guards in Timex: https://github.com/bitwalker/timex/blob/master/lib/timex/macros.ex#L96

I'm not sure that this proposal simplifies anything for me really, but maybe I'm misunderstanding the benefit.

Paul

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/20160813012725.77f2ad33.eksperimental%40autistici.org.

José Valim

unread,
Aug 12, 2016, 3:11:21 PM8/12/16
to elixir-l...@googlegroups.com
Paul, the main problem is that if you want to use that macro outside of a guard, n will be unquoted twice which could have side-effects.



José Valim
Skype: jv.ptec
Founder and Director of R&D

eksperimental

unread,
Aug 12, 2016, 3:22:16 PM8/12/16
to elixir-l...@googlegroups.com
the Comparison group of kinds was the last group and I just included just to see how much i could
push guard,

But the rest of the groups are less controversial.

- Basic and built-in types
- Additional: Derived from is_* functions
- Additional: Literal numbers

and maybe it is arguable if we need them all of them,
- Additional: Convenience

Would it be possible to have the best of both worlds here?
Allow users to define their own guards/kinds, and be able to call them from is/are functions, and
offering a default set of kinds/guards like the ones listed above (or whatever we agree are the
minimum needed to be supported by the language).

defmodule DateTime

defkind second(seconds) when seconds >= 0 and seconds <= 60

def new(..., seconds) when seconds is :second

end

The benefit of are* functions is that it reduces the amount in a significant way when you have
several items on the left and several kinds on the right hand side.

also having a defined list of default kinds, helps us to translate directly from the typespecs into
guard definitions.

Paul Schoenfelder

unread,
Aug 12, 2016, 4:22:26 PM8/12/16
to elixir-l...@googlegroups.com
Hey José, could you give an example of what you mean? At least with those guards, using them in function bodies appears to work just fine, is it because they are working on simple types (i.e. integer, tuple)? If it does fall down in the general case, I can definitely see where that would make it a non-workable solution, in the case of Timex I'm only using those macros in function guards anyway, but some tests with those macros locally seems to work as expected whether in a function guard or used in the body like a function.

Paul

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/20160813022209.5a991c28.eksperimental%40autistici.org.

José Valim

unread,
Aug 12, 2016, 4:29:20 PM8/12/16
to elixir-l...@googlegroups.com
"is_positive_integer IO.inspect(1)" in the macro you have posted will inspect the value twice when used in the function body. "is_positive_integer send(self(), 1)" would send the message to self twice and so on.




José Valim
Skype: jv.ptec
Founder and Director of R&D

Paul Schoenfelder

unread,
Aug 12, 2016, 4:43:09 PM8/12/16
to elixir-l...@googlegroups.com
Ah sure enough :), figured there was something I was missing, thanks for the examples!

Paul

eksperimental

unread,
Jun 1, 2017, 8:29:42 PM6/1/17
to elixir-l...@googlegroups.com
I'm trying to work on this one,
I have a working solution, what I did was to create a module called Guard,
and this module gets automatically required same as Kernel, Kernel.SpecialForms and
Kernel.Typespecs.

But I have realized that there is a limitation, and is when the macros are not passed a variable,
but a reference to it,

let's say:
kind = {:map, 1}
is_kind(%{foo: :bar}, kind),

is there trick to evaluate kind inside the macro that is valid in

cond do
is_atom(kind) ->
quote do: ....

is_tuple(kind) and is_atom(:erlang.element(1, kind)) ->
quote do: ...

but without putting the logic inside a quote (which will be invalid in a guard),
and unquoting kind to get the value from its calling environment.

thank you.

José Valim

unread,
Jun 2, 2017, 1:56:54 AM6/2/17
to elixir-l...@googlegroups.com
Hi Eksperimental,

Maybe I was not clear enough in my previous comment but it is unlikely we would accept is_kind. I think we should provide a mechanism for making it easier to build your own guards, rather than a custom solution with custom lookups and so on.

That said, if you are implementing this for fun, if you want to support dynamic operations then yes you need to emit a really large quote that compiles to guards and that could be expensive. That's why some guards, such as in/2, expect literal values at compile time.



José Valim
Skype: jv.ptec
Founder and Director of R&D
--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/20170602072926.530696ae.eksperimental%40autistici.org.
Reply all
Reply to author
Forward
0 new messages