Added {:type_check, "~> 0.2.3"} to mix.exs, ran deps.get, deps.compile and . . .
iex(2)> Sandbox.foo("bing", "bang", "bow")
** (TypeCheck.TypeError) The call `foo("bing", "bang", "bow")` does not adhere to spec `foo(binary(), binary(), float()) :: float()`. Reason:
parameter no. 3:
`"bow"` is not a float.
(arbit 0.1.0) lib/Sandbox.ex:1: Sandbox.foo/3
@Qqwy - I’ve been thinking about TypeCheck in regards to remote types and wondering about your thoughts. I use TypeCheck for the following:
As a runtime checker for builtin elixir types. They are not all there, but most are.
As a way to write tighter custom requirements than can be written via guards or regular specs.
As a way to automatically generate traditional specs for compile-time.
I know it provides generators and manual compile time checking, but I just haven’t used them yet.
The one sticking point for me has probably been interoperability with libraries/modules that do not use TypeCheck. Referencing a remote type, of course, results in a compile-time error. This means that not only can the remote type not be type checked, but it can’t generate a traditional typespec. I can write a traditional typespec, but now I have a mix of @spec and @spec! in my codebase. I’d rather just write @spec! everywhere.
You mention thinking about some sort override configuration for remote types. It does make me wonder though if TypeCheck should/can just double down on runtime checking and ignore remote types altogether. Basically, if TypeCheck encounters something like Ecto.UUID.t() and doesn’t have a known way to handle it it is ignored, BUT is still included in the generated traditional typespecs. It handles the things it can and doesn’t harm the things it can’t.
Perhaps it is just not a concern of TypeCheck to deal with this and that is okay. TypeCheck would still be providing value in that case by generating the traditional spec for tools dedicated to compiled spec checking like Dialyzer and Selectrix.
The obvious caveat here is that people may assume TypeCheck is checking things that it is not and silently ignoring them. First, I just don’t know that this is that big of a deal assuming the documentation prominently mentions that remote types are not checked. Second, it is always possible for one to write custom TypeCheck-compatible types or even create TypeCheck support packages for common libraries. Lastly, I’m just thinking the value gained by ignoring checks (but generating traditional specs) outweighs the current situation where you can’t reference them at all.
I don’t know if this is doable or something you would want to consider, but throwing it out here in case it never seemed like an option.
It is a valid concern. My opinion on the matter is to prefer explicit overrides over ‘implicitly doing nothing’, because the latter has great bug-hiding potential. That said, I definitely want to make it simple for people to opt-out by e.g. specifying that a particular remote type should be treated equivalent to any(). (But this should not be the default).
Now obviously the current situation in which remote types cannot properly be used at all is painful. I hope to resolve this as soon as possible (I have some time in the next few weeks to work on this, luckily).
I’m very interested in what this looks like! Please share an example!
Fair enough. Just being able to reference remote types will be excellent. I guess I’m picturing a world where something like Selectrix provides an extra level of protection to avoid those hidden bugs, but I can appreciate wanting to flag those issues during run-time via a bit of additional configuration.
I just mean that under the hood TypeCheck is generating traditional typespecs that something like Dialyzer or Selectrix could theoretically use. Right? I don’t use Dialyzer, but that was my (perhaps mistaken) assumption.
defmodule Sandbox do
use TypeCheck
@spec! foo(binary, binary | atom, float) :: float
def foo(arg1, arg2, arg3) do
#some process
end
end
iex(4)> Sandbox.foo("bing", :ok, 5.712)
** (TypeCheck.TypeError) The result of calling `foo("bing", :ok, 5.712)` does not adhere to spec `foo(binary(), binary() | atom(), float()) :: float()`. Reason:
Returned result:
`:ok` is not a float.
lib/Sandbox.ex:1: Sandbox.foo/3
As you can see, arg2 can be either a string or an atom. What’s the proper TypeCheck syntax to get this to spec to pass?
defmodule Sandbox do
use TypeCheck
@spec! main(list, binary) :: binary | list
def main(arg1, arg2) when arg1 == [] do
# some process
end
@spec! main(list, binary) :: list
def main(arg1, arg2) when arg1 != [] do
# some process
end
end
iex(5)> recompile
Compiling 1 file (.ex)
warning: this clause for __type_check_spec_for_main/2__/0 cannot match because a previous clause at line 1 always matches
lib/type_check/spec.ex:1
You only need a single @spec! declaration to cover multiple function heads of the same name and arity. You’re getting a warning there because the second declaration of @spec! main can’t possibly match anyway as any list would already have matched on the first @spec! main declaration.
defmodule Print do
use TypeCheck
def utc_time, do: DateTime.utc_now()
@spec! higlight(binary | number | list | atom) :: atom
def highlight(message) do
timestamp = @blue <> "#{utc_time()}-"
text = @light_green <> "#{message}"
IO.puts("#{timestamp} #{text} \n")
end
end
Still getting my feet wet but, so far, I absolutely love TypeCheck. Ok, so, what’s going on with this error message and how do I keeping it from cropping up again in the future?
@Qqwy, my friend, let me first start off by saying how much I absolutely love TypeCheck! I’m an Elixir noob (started learning it on my own last year with ZERO programming background) and I don’t know how I’ve gotten as far I have without it. It’s absolutely amazing!!
Your focus on generating user friendly error messages is one of its very best features! With that in mind I’d like to offer up the following suggestion: It would be very helpful if the error message were restructured to something along the lines of-
** (TypeCheck.TypeError) The call `THE_FUNCTION_EVALUATED/arity' does not adhere to spec `THE_@spec!'. Reason: parameter no. PARAMETER_NUMBER (or result) is not a 'TYPE_SPECIFIED_IN_THE_@spec!'
Details: 'COPY_OF_THE_PARAMETER_DATA_THAT_TRIGGERED_THE TypeError'.
The advantage of such a format is that it provides the most actionable information right from the jump. (My project manipulates very dense data sets so whenever there’s a TypeError I have to wade through literally pages and pages of an error report to figure out exactly what went wrong.)
Interesting! Adding function/arity at the top (including its line number and filename if available) and specifying which parameter (or the result) failed before printing them might indeed be an improvement for when functions are called with arguments that are very large.
I’ll give the matter some thought .
Version 0.3.0 has been released, which contains the improved error formatter output. (details can be found in this PR) as well as a small bugfix for types using tuple/1.
Is there anything unique about TypeCheck that might stop me from writing a macro that generates a function with a @spec!?
I’m experimenting with a few macros for functions I write all over the place and was hoping to include specs. This example (which I import) works fine except when I add the @spec!. Is it because it is a macro in a macro?
defmacro crud_list(resource) do
quote bind_quoted: [resource: resource] do
@spec! unquote(:"list_#{resource.plural_name}")(query_pipe()) ::
list(unquote(resource.schema).t())
def unquote(:"list_#{resource.plural_name}")(queries \\ & &1) do
unquote(resource.schema)
|> queries.()
|> unquote(resource.repo).all()
end
end
end
There should not be.
While TypeCheck does need to jump through some tricky hoops to make all of the syntax and semantics work and be desugared at compile-time, wrapping specs in macros is a use case which, if at all possible, I’d like TypeCheck to be able to handle properly.
If you could open an issue on GitHub that explains the code you are trying to compile and the errors you encounter, then I’ll take a look at it ASAP .
defmodule Example do
use TypedStruct
use TypeCheck
typedstruct module: Options do
field :name, String.t()
field :age, integer()
end
@spec! init(Example.Options.t()) :: :ok
def init(opts) do
:ok
end
end
over
defmodule Example do
use TypeCheck
defmodule Options do
defstruct [:name, :age]
@type! t :: %Options{name: binary, age: integer}
end
@spec! init(Example.Options.t()) :: :ok
def init(opts) do
:ok
end
end