Ecto: Ecto.Model typespec

Created on 4 Feb 2015  Â·  16Comments  Â·  Source: elixir-ecto/ecto

I have a MyModel model where I use Ecto.Model. In the MyModel module, I also keep a bunch of functions that operate on instances of MyModel.
When I write the @specs for those functions, I'd like to directly use the type t when dealing with an instance; it would be cool to have the MyModel.t type automatically defined from the schema data.

I'm not sure this is something that can be done (dynamic typespecs generation) and that's why I'm opening this issue. I'd like to give this a try if it's possible :smiley:

Discussion

Most helpful comment

I took a stab at this and an incomplete implementation was pretty easy but there's more to be done.

Here's what I got so far:

defmodule Permalink do
  @behaviour Ecto.Type

  @type t() :: String.t() # more on this below

  def typespec() do
    quote do: String.t()
  end

  def type(), do: :string
  def cast(term), do: {:ok, term}
  def dump(term), do: {:ok, term}
  def load(term), do: {:ok, term}
end

defmodule Post do
  use Ecto.Schema
  import EctoTypespec

  typed_schema "posts" do
    field :title, :string
    field :published, :boolean
    field :permalink, Permalink
  end
end
iex> t Post
@type t() :: %{
        id: integer(),
        title: String.t(),
        published: boolean(),
        permalink: String.t()
      }

instead of having another callback on Ecto.Type, is it possible to leverage a convention that custom type defines @type t()? I couldn't leverage Code.Typespec.fetch_specs because beam was not yet available. (and that Code.Typespec API is private anyway)

If we can use @type t() convention then it will work nicely for associations and embeds, e.g. on has_many :comment, Comment we could infer comments :: [Comment.t()] | NotLoaded.t()

I think treating non-specified custom types as any() is not too bad. People who care about types will define @type t() on custom type, library authors shipping custom types should too, and for people that don't care about them any() is fine.

Even if this doesn't make it to Ecto, I'd appreciate any help with reading @type t() from custom types etc during compilation, so that I can publish this as a library. Thanks!


edit: never mind about not being able to use Code.Typespec, if modules are in separate files it works.

edit2: actually, instead of "inferring" permalink: String.t(), we should do: permalink: Permalink.t() - this way we avoid compile-time dependency, which is especially important for associations.

All 16 comments

It would be nice to automatically generate the spec with the types from the schema. However that would require extending Ecto.Type to add yet another function. Thoughts? /cc @drewolson @ericmj

My experience with type specs is very limited so I'll bow out of this
discussion.

On Wednesday, February 4, 2015, José Valim [email protected] wrote:

It would be nice to automatically generate the spec with the types from
the schema. However that would require extending Ecto.Type to add yet
another function. Thoughts? /cc @drewolson https://github.com/drewolson
@ericmj https://github.com/ericmj

—
Reply to this email directly or view it on GitHub
https://github.com/elixir-lang/ecto/issues/425#issuecomment-72861019.

I'm -1 because the callback added to Ecto.Type would have to return a quote, so people implementin it need to learn about quoting and typespecs. We could make the callback optional but I'm not sure if that's better.

@whatyouhide It's easy to create a type for your model to use in typespecs. Just do @type t :: %__MODULE__{}.

@ericmj yes, that would work too but we wouldn't have any type of specs about the types of the fields in the struct; this is a bummer because we have those things in the schema definition. However, when I opened the issue I didn't think of custom Ecto.Types at all actually.

That said, maybe making the callback optional but allowing people to specify the type via a quoted callback is not such a bad idea. Let's see what others think about this maybe?

Right, that's exactly the advantage of moving it to Ecto.Type. We already give the type information when mounting the schema.

@ericmj your suggestion won't have any type information though, just lay out the struct structure.

@josevalim I think even just laying out the struct info is a good starting point. I also think that most people would implement the callback in Ecto.Type if we give good documentation on how to do it; it doesn't sound like a very complicated task if you have a "template" to look at.

I am closing this. It would be nice to have but currently is low priority.

I know this is a bit outdated, but I was wondering whether or not there was progress on this?

We have an app that uses Ecto and OTP, and we would really like to be able to spec our GenServer related inputs as User.id type etc. How would one do that right now?

The current solution would be to write the type by hand. We don't have any plans to add automatic type generation to ecto schemas.

Alternatively it's possible to write a wrapper macro around schema to do this automatically.

I took a stab at this and an incomplete implementation was pretty easy but there's more to be done.

Here's what I got so far:

defmodule Permalink do
  @behaviour Ecto.Type

  @type t() :: String.t() # more on this below

  def typespec() do
    quote do: String.t()
  end

  def type(), do: :string
  def cast(term), do: {:ok, term}
  def dump(term), do: {:ok, term}
  def load(term), do: {:ok, term}
end

defmodule Post do
  use Ecto.Schema
  import EctoTypespec

  typed_schema "posts" do
    field :title, :string
    field :published, :boolean
    field :permalink, Permalink
  end
end
iex> t Post
@type t() :: %{
        id: integer(),
        title: String.t(),
        published: boolean(),
        permalink: String.t()
      }

instead of having another callback on Ecto.Type, is it possible to leverage a convention that custom type defines @type t()? I couldn't leverage Code.Typespec.fetch_specs because beam was not yet available. (and that Code.Typespec API is private anyway)

If we can use @type t() convention then it will work nicely for associations and embeds, e.g. on has_many :comment, Comment we could infer comments :: [Comment.t()] | NotLoaded.t()

I think treating non-specified custom types as any() is not too bad. People who care about types will define @type t() on custom type, library authors shipping custom types should too, and for people that don't care about them any() is fine.

Even if this doesn't make it to Ecto, I'd appreciate any help with reading @type t() from custom types etc during compilation, so that I can publish this as a library. Thanks!


edit: never mind about not being able to use Code.Typespec, if modules are in separate files it works.

edit2: actually, instead of "inferring" permalink: String.t(), we should do: permalink: Permalink.t() - this way we avoid compile-time dependency, which is especially important for associations.

I've updated the gist with associations and embeds support, I think this is pretty close. Let me know if you'd re-consider having this in core! cc @josevalim @ericmj @michalmuskala

The approach that uses Code.Typespec to check if there's @type t() is unreliable, it doesn't work when compiling modules for the first time, so may need to improve that or go back to function callback.

The proper way to check if a type is defined is by using Kernel.TypeSpec.defines_types? but it is not public API, which I think is going to be a blocker for us to adopt this. :(

I think it might be actually fine to make it to always use type_module.t() for custom types. The worst that might do is make dialyzer complain about the type not being defined and treat it as any, which I think is fine in that case.

@michalmuskala yeah, I also thought about this but haven't mentioned it because it's a bit dirty to create not quite correct code :) but it's super pragmatic so I think that would be a good option. It's a niche use case but I've heard folks are generating docs for their echo schemas, so this way ex_doc could autolink to custom type module too which would be quite nice. (Even if remote type doesn't exist.)

@josevalim are there plans to have public API for getting typespecs? Besides this use case, it would be nice for ex_doc as well not to rely on internals.

Btw, another nice thing about automatically creating these typespecs is they'll be used by StreamData types GSoC thing.

@wojtekmach there are two APIs here: kernel.typespec and code.typespec, our plan is to make the latter public, because it works on compiled beams and is fairly standard. the first one though is what you need here and it is a bit more about the internals, so we are not quite sure what to do there.

@josevalim yup thanks! If we find a way to move forward, this should be helpful: https://github.com/elixir-ecto/ecto/compare/master...wojtekmach:wm-schema-typespecs. It follows Michał's suggestion.

I'm not sure if this is really needed, but we may want to allow overwriting the generated @type t() on the schema, which requires even more internals. There's a failing test about that on the branch.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbence picture jbence  Â·  3Comments

stavro picture stavro  Â·  4Comments

kelostrada picture kelostrada  Â·  3Comments

jonasschmidt picture jonasschmidt  Â·  4Comments

tverlaan picture tverlaan  Â·  3Comments