andreyvolokitin

andreyvolokitin

Best practice for reusing typespecs

  1. If we want to introduce common reusable type, should we introduce separate Types module like so?:
defmodule App.Shared.Types do
  @moduledoc """
  Common reusable types
  """

  @type generic_id :: non_neg_integer() | String.t()
end

  1. Also if we have a struct type defined for some entity, and then we want to reuse a type of a field from that struct — can we directly refer to the field type using something like Struct.t().field() (this and [:field] notation does not work though)? Or reusing field types like this is impossible and we need to define custom public type within a struct module, and then reuse this type for a struct type and in other modules?

Marked As Solved

christhekeele

christhekeele

I would encourage you to think hard about what module should own a type before placing it in a catch-all drawer. Typespec types are used at function boundaries: inputs and output. Modules tend to group functions around specific input/output themes. So if you have a type that doesn’t feel like it has a clear module owner, you probably have a leaky abstraction popping up all over your code base, involved in perhaps too many function input/outputs. It would suggest you are missing a module to handle that type in particular.

Of course that is idealistic and impractical in many applications. I encourage placing these sorts of global types at the toplevel module of your project; ex: App.generic_id. If they are truly global to your domain, they will read well there. Otherwise, as you carve out a niche for them in future modules it will be easy to shift their location.

Correct, you cannot index into struct type’s field types. What you propose instead is fine, flexible, and common:

defmodule App.Entity do
  defstruct [:id, :data]
  @type id :: non_neg_integer() | String.t()
  @type data :: any()
  @type t :: %__MODULE__{
    id: id,
    data: data
  }
end

Again this idea of which module should own the type comes up. In the example above, we’re saying that App.Entity really owns what an id even means, and other modules referencing that type are using its understanding. You could just as easily define (for example) an App.id type and use it inside the App.Entity.t if you think that somebody else really determines what a functioning id should look like in your system.

Also Liked

dimitarvp

dimitarvp

I am not sure if we’re talking actual types or the fields themselves?

If it’s just the types, that’s easy enough. Following your example the user module would look like this:

defmodule App.UserSchema do
  alias App.Shared.Types

  @type user_result :: {:ok, Types.generic_id()} | {:error, any()}
end
andreyvolokitin

andreyvolokitin

Not sure about inheritance, but since we only can define types within modules, means we need to use modules as containers for types

My intent is to only write truly reusable things in one place, maybe meaningfully grouped in multiple places, in order to prevent “bad” duplication (aka knowledge duplication), as opposed to “ok”/coincidental duplication, which I’m ok with. So it seems like these types are better reside somewhere meaningful

E.g. when I work with data which has specific shape/types defined for this data, then I handle this data in some other place — it is useful to be able to refer to a type of a specific field for this data and not to duplicate the type and then keep it in sync

For when to write typespecs — I figured it’s a whole new topic, which I’ll investigate further, e.g. I read it is recommended to only write typespecs for public functions

That’s practically what I imagined

Actual types. This is what I expected, but wanted to confirm. The 2nd case is more vague though: we already export struct type, but we can not refer to a field type of this struct, this requires defining it in a separate public type near this struct (but maybe in practice it is not an issue at all)

Great points! For sure I’ll be keeping details to where they belong

Eiji

Eiji

Try to imagine similar way of thinking … Would you like to write dozens or hundreds of Ecto.Schema just to have them in one place? For sure there is no rule to cover all edge cases, but in general we rather avoid such practices. :thinking:

Firstly all struct related types should be defined in struct’s module. It’s no shame to have a public type for a single field just to reuse said type somewhere else as long as we speak about same data i.e. it’s fine to use field’s type somewhere in code especially when we write typespec for private functions for dialyzer tool or so. :see_no_evil:

However using same type just because it’s typespec matches specific needs (like a generic keyword() type) is incorrect as developer working with your library would think that only data from said struct are seen as valid / supported. :crazy_face:

# Here we have struct and all related typespec in one place.
# Structs may be big for sure, but definitely not like all types in one place.
defmodule MyLib.SomeStruct do
  # …

  defstruct # …

  @typedoc "…"
  @type t :: %__MODULE__{
    some_field: some_field(),
    # …
  }

  @typedoc "…"
  @type some_field :: # …

  # …
end
# Generally I recommend to write types as high as possible
# i.e. it would be weird if a parent module would use a type from sub module
# as the data is not strictly related to anything (like in structs)
# so the type should be defined in highest parent module which uses it
# or a parent module which groups sub modules using said type
defmodule MyLib.Utils do
  # …

  @typedoc "…"
  @type commonly_used_data_type_not_related_to_any_struct :: # …

  # …
end

In example above I would move the type definition from MyLib.Utils to MyLib if MyLib or MyLib.OtherModule would use it. Otherwise it’s in good place. :+1:

Of course there may be an edge case. Let’s say that you document an API and then Utils (more generic code) would use types from it.

defmodule MyApp.Utils do
  # …

  @spec some_func(MyApp.API.V1.APIObjectNotRelatedToAnyStruct.type_name()) :: # …

  # …
end

That’s why there is no one rule to write typespec. Generally you put types in order:

  1. If it’s related to a struct then in struct module
  2. If it’s related to some specific module like when documenting API which does not need it’s struct
  3. In top Utils-like module as in above example

Where Next?

Popular in Questions Top

jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42716 114
New

We're in Beta

About us Mission Statement