IvanRublev / Domo

A library to validate values of nested structs with their type spec t() and associated precondition functions

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to properly represent [a | b]?

bamorim opened this issue · comments

First of all, this library is an amazing work. Having worked with a few type-related libraries on Elixir of my own, I know how hard it is to pull something like this. I started evaluating using Domo to validate some complex nested structures here at work but found a problem that makes using it a little bit harder. I have found a workaround, so nothing is blocking us.

This might not be necessarily a bug or an issue (it can be just that this is a known limitation and no plans to fixing it exist due to the complexity), it is more a conversation starter sharing some of my experience using it.

Currently, whenever we have a type [a | b] Domo treats it as [a] | [b].

Note: For me, this was not totally clear from the docs, but maybe it is the same as the limitation regarding parametric types?

That is, the following data structure

def Apple do
  use Domo
  defstruct []
  @type t() :: %__MODULE__{}
end

def Orange do
  use Domo
  defstruct []
  @type t() :: %__MODULE__{}
end

def FruitBasket do
  use Domo
  defstruct [fruits: []]
  @type t() :: %__MODULE__{fruits: [Apple.t() | Orange.t()]}
end

Will result in

assert {:ok, _} = FruitBasket.ensure_type(%FruitBasket{fruits: [%Apple{}, %Apple{}]})
assert {:ok, _} = FruitBasket.ensure_type(%FruitBasket{fruits: [%Orange{}, %Orange{}]})
assert {:error, _} = FruitBasket.ensure_type(%FruitBasket{fruits: [%Apple{}, %Orange{}]})

My current workaround is to do this weird thing (I'm actually doing something slightly different to propagate the errors, but it is similar):

# Apple and Orange the same

defmodule FruitValidator do
  use Domo
  defstruct [:fruit]
  @type t() :: %__MODULE__{fruit: Apple.t() | Orange.t()}
  
  def valid?(fruit) do
    case ensure_type(%__MODULE__{fruit: fruit}) do
      {:ok, _} -> true
      _ -> false
    end
  end
end

def FruitBasket do
  use Domo
  defstruct [fruits: []]
  
  @opaque fruit() :: map()
  precond(fruit: &FruitValidator.valid?/1)
  
  @type t() :: %__MODULE__{fruits: [fruit()]}
end

So, in the end I'm almost just using Domo underneath but the ergonomics are just much worse. Is that something you think is easily integrated into Domo itself? Do you have a better suggestion on how to deal with this?

Hmm. Indeed Domo treats [a | b] as [a] | [b]. https://github.com/IvanRublev/Domo/blob/master/test/domo/type_ensurer_factory/resolver/or_test.exs#L200

I agree the better will be [a | b] -> [ all possible combinations of a and b ].

I think I'll be able to have detal look at it on the first week of November. So if you find how to update it earlier PRs are welcome!

P.S. There is a combine_or_args helper function which used to generate all possible combinations of types.

I'll try to see if I can figure out a way myself, but probably will only have time for that early November as well, but will give another shot at the source code before that to see if I can spot any obvious way to represent that.

Thank you 🚀

I gave it a shot today, but there is still a long way to go I guess. Here is the WIP commit: 231f072

What is missing is mainly is how to combine preconditions.

Also, I think I messed up something related to kwlists with only one element, but I guess that's a start.

@IvanRublev Hey! Is there any update/progress on this issue?

Hey @rslota @bamorim, as they say, it's better later than never 😄

Please have a look at version 1.5.15 which supports sum types well, and in particular as a list element.