[Proposal] Syntax for overriding map/struct typespec field

61 views
Skip to first unread message

Bernardo Amorim

unread,
Feb 5, 2021, 5:39:13 PM2/5/21
to elixir-lang-core
Hi folks, I'm not even sure if this is possible nor if it was raised before nor if there is already another way of doing something similar. But this is something that I've been thinking a lot recently and I'd like to know if it is possible and desirable. If it is, I can help with a PR later on.

The "problem":

It is usual to when we have a Struct (specially Ecto.Schemas) to define a `@type t()` with the fields. Let's say we have a Struct with a few fields like this:

```
defmodule MyStruct do
  defstruct [:a, :b, :c]
  @type t() :: %__MODULE__{
    a: integer() | nil,
    b: integer() | nil,
    c: integer() | nil
  }
```

Now imagine I want to define a function that receives a struct but requires one of these fields to be non null. What we have to do right now is to:

```
@type my_struct_with_a() :: %MyStruct{
  a: integer(),
  b: integer() | nil,
  c: integer() | nil
}
```

(Ok, maybe in some cases we do not need to redefine all the other fields since they might be irrelevant, but let's assume we actually want to type everything)

The proposal:

For map values when we just want to change one field we can do something like: ```%MyStruct{my_struct | a: 1}```, but for types this is not possible.

My proposal would be to have something like this:

```
@type my_struct_with_a() :: %MyStruct{MyStruct.t() | a: integer()}
```

That would generate a type with all fields copied from `MyStruct.t()` but with `:a` changed to `integer()` instead of `integer() | nil`.

The caveats I see is what to do when the type is not just a map type (maybe it is a sum type, maybe it is not even a map, etc).

What do you folks think about this?

Thanks,
Bernardo Amorim

Louis Pilfold

unread,
Feb 5, 2021, 6:21:36 PM2/5/21
to elixir-lang-core
Hello!

Correct me if I'm wrong but I believe this isn't something that Erlang typespecs support, or at least there is no syntax for it.

What do you see this compiling to? Elixir has to work with the capability of Erlang here.

Cheers,
Louis

--
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-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/b152b04f-3c6b-4052-92aa-d23724ed0f92n%40googlegroups.com.

Bernardo Amorim

unread,
Feb 5, 2021, 7:54:06 PM2/5/21
to elixir-l...@googlegroups.com
I was expecting that to be a compile time transformation that would generate the exact same compiled erlang as
```
@type my_struct_with_a() :: %MyStruct{
  a: integer(),
  b: integer() | nil,
  c: integer() | nil
}
```

But that would require `@type` to be able to get the AST for `MyStuct.t()` during compile time.

You received this message because you are subscribed to a topic in the Google Groups "elixir-lang-core" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elixir-lang-core/gvLUM0asfmI/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CABu8xFCeqgNAOp1o%3D0JJ%2B9JYwF20H9-%2BnD2DLoY%3DuSisyGB8NA%40mail.gmail.com.

José Valim

unread,
Feb 6, 2021, 1:50:41 AM2/6/21
to elixir-l...@googlegroups.com
I completely agree with the needs for this feature but unfortunately it must be implemented upstream on Erlang first.

The reason why is because if we were to expand the initial map within Elixir, it will both be expensive (we may need to augment a remote type, which would require loading and traversing beam files) and add a compile time dependency.

If this is part of Erlang, none of those are required.

Fernando Tapia Rico

unread,
Feb 6, 2021, 3:50:35 AM2/6/21
to elixir-lang-core
I think parameterized types might help a little in this case:

@type t() :: t(integer() | nil)

@type t(a) :: %__MODULE__{
  a: a,
  b: integer() | nil,
  c: integer() | nil
}

@type my_struct_with_a() :: t(integer)



Bernardo Amorim

unread,
Feb 6, 2021, 6:24:06 AM2/6/21
to elixir-l...@googlegroups.com
Thanks for the responses.

I'll find my way on how to suggest this upstream in Erlang then. Hope they see value on that. 

As for the parametric types, that was my initial approach, but This is not a general solution. It is great for some specific examples, but no all. My main use case would be requiring associations to be loaded on Ecto.Schema and the argument list for the type would be really big. 

Maybe if we can have some sort of named attributes(like a keyword list or map plus access syntax and defaults) on types would solve this problem. But my bet is that this would also require Erlang upstream support. 

Reply all
Reply to author
Forward
0 new messages