DaAnalyst

DaAnalyst

Hi,

Is there any way to specify a map or a structure extension using typespecs, e.g.:

@type map1() :: %{
  field1: String.t(),
  field2: atom()
}

@type map2() :: %{
  map1() |
  field3: integer(),
  field4: integer()
}

Naturally, the above wouldn’t compile, but is there any way to achieve the same effect? If not, how about adding this feature to typespecs?

TIA

Showing Posts 1 to 8

kokolegorille

kokolegorille

Not really the answer You are looking for, but maybe typecheck can help…

Qqwy

Qqwy

TypeCheck Core Team

In Elixir’s typespec syntax itself, this is not possible. There is no notion in Elixir/Erlang’s typespecs of ‘a couple of keys’ that you add to the specification of another map.

You have the choice of adding one or multiple optional(key_type) => value_type() at the end (where key_type might be a single atom), but a key-value pair like this (e.g. optional(key_type) => value_type() ) does not have a type on its own and cannot be embedded on its own.

Since Elixir’s typespecs have feature-parity with Erlang’s typespecs (there is a little bit of extra syntactic sugar but they are 100% compatible) I do not think a feature like this will ever be added.

As @kokolegorille already pointed out, there are ways to add (runtime) type-checks to your code that might be able to do checks like this.
Currently in the TypeCheck library the notion of a key-value pair also does not exist as its own structure (it is an interesting feature though, PR’s are very welcome :upside_down_face: ), but it is fully possible to build it yourself in user code and integrate it with the rest of TypeCheck.

Qqwy

Qqwy

TypeCheck Core Team

The best you can do in Elixir itself, is something crazy like this (I do not recommend doing this!):

defmodule ExtendedMap do
  @duplicated_types (
    quote do
      [
        field1: String.t(),
        field2: atom()
      ]
    end)

  @type map1() :: %{
    unquote_splicing(@duplicated_types)
  }

  @type map2() :: %{
    unquote_splicing(@duplicated_types),
    field3: integer(),
    field4: integer()
  }
end

So we are able to inject type snippets into other types using quote/unquote.
However, since most types are not valid as values, we also need to have the extra quote around the list. Also, the parentheses around this quote are not optional, because of a parsing precedence edge case in Elixir, making it even more clear that we’re in unexplored waters here: You’re not intended to write this kind of code.

So: This seems to me a clear situation in which the ‘cure’ is worse than the ‘disease’.

DaAnalyst

DaAnalyst OP

The cure indeed looks worse than the disease, but thank you for going into length with this.

Erlang feature-parity be damned, I still think such a basic tool for reusing base structure fields elsewhere should be made available. For instance, my “defextends” macro helps me “extend” elixir structures without needing to copy the “inherited” fields and it proved to be very useful, but I still lack a tool for writing the typespecs.

As for TypeCheck, I don’t think it addresses this problem at all.

Thanks

Qqwy

Qqwy

TypeCheck Core Team

In that case I might be misunderstanding your use case. I thought your scenario was that you want to extend the typespec of one struct with some of the fields of the typespec of another struct. TypeCheck is able to facilitate that, and create the final ‘Elixir-compatible’ typespecs to be shown in the documentation and e.g. passed to Dialyzer for you.
But if I’m not correctly paraphrasing your scenario, please do explain :blush:.

DaAnalyst

DaAnalyst OP

I took a quick look at the TypeCheck docs and I couldn’t find any mention of extending/inheriting from a structure or a map. Can you please provide a snippet of how it may be done using TypeCheck?

Thanks

Qqwy

Qqwy

TypeCheck Core Team

Thank you for this question! I attempted to do it, but I did encounter a problem.
In essence, creating functions that take TypeCheck type-structs as input and generate other type-structs as output is ‘easy’:

  def map_union(lhs, rhs) do
    lhs_keypairs = extract_keypairs(lhs)
    rhs_keypairs = extract_keypairs(rhs)

    TypeCheck.Builtin.fixed_map(TypeCheck.Builtin.fixed_list(lhs_keypairs ++ rhs_keypairs))
  end

  defp extract_keypairs(list) when is_list(list) do
    list
  end

  defp extract_keypairs(list = %TypeCheck.Builtin.FixedList{}) do
    list.element_types
  end

  defp extract_keypairs(map = %TypeCheck.Builtin.FixedMap{}) do
    map.keypairs
    |> Enum.map(fn {key_atom, val_type} ->
      TypeCheck.Builtin.fixed_tuple([TypeCheck.Builtin.literal(key_atom), val_type])
    end)
  end

and indeed, if we were to add this to the TypeCheck.Builtin module, it would work as intended, and you could e.g. write:

defmodule ReusedKeys do
  use TypeCheck

  @type! map1 :: %{field1: binary(), field2: binary()}
  @type! map2 :: map_union(map1, [field3: binary()])
end

However, currently there is no easy way for a user to make their own custom “type-level” functions available for usage in the types, as type specifications are evaluated in a slightly special context because of implementation reasons.

I’ve created an issue on the TypeCheck repository to tackle this.

So to answer your question: No, it is currently not possible, but will be "soon :tm: ".

DaAnalyst

DaAnalyst OP

Thanks! Will be glad to check it out when you add the feature. Please, make the use as neat as possible.

Another thing. As you can imagine, the requirement for a map/struct extension often regards inter-module references e.g.:

defmodule Base do
  @type t() :: %__MODULE__{
                 field1: binary(),
                 field2: binary()
               }
  defstruct field1: nil,
            field2: nil
end

defmodule Extension do
  @type! t() :: subtype( %__MODULE__,
                         Base.t(), # may be a list if multiple extension
                         field3: integer(),
                         field4: integer())
  defextends Base,
             field3: 1,
             field4: 0
end
— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
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
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
matt-savvy
Anyone here using Honeybadger? My Honeybadger account is being overwhelmed with noise from some bots. Seeing a lot of Bandit.HTTPError...
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

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
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
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews