Qqwy / elixir-type_check

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.
MIT License
517 stars 25 forks source link

Strategy for using type-check as an optional dependency #143

Closed zachallaun closed 1 year ago

zachallaun commented 2 years ago

First off, thanks very much for the effort that’s gone into this project. I think it’s really valuable and helpful and hope to see it continue to thrive.

I wanted to start a conversation about possible strategies for using TypeCheck as a library author with TypeCheck as an optional dependency. I’d love to be able to use TypeCheck as a tool for development aid and for my own projects, but I’m hesitant to “foist it” on others (even if it can be configured to be “turned off”, I rather not impose the dependency).

This is somewhat tricky, I think, as unlike most optional dependencies that are confined to a single module, TypeCheck is generally spread throughout all code.

I’d love to get your thoughts on possible strategies to make this work. One option would be a minimal wrapper module that “calls out” to TypeCheck if the dependency is available, and implements no-ops otherwise. But TypeCheck does some funky stuff with @ that may make it a little trickier to wrap?

This may benefit from making some of the TypeCheck’s “internals” a part of the public API (basically just officially exposing some of the helpers that the TypeCheck macros call out to, so that they can be more easily wrapped).

Anyways, just thought I’d bring it up and see what you think!

Qqwy commented 2 years ago

Hi there!

It is possible to create your own module that contains an implementation of __using__ such that it:

It might look something like:

defmodule YourLibrary.TypeCheck do
  case Code.ensure_compiled(TypeCheck) do
    {:module, _} ->
      defmacro __using__(options) do
        quote do
          use TypeCheck, unquote(options)
        end
      end

    {:error, _} ->
      defmacro __using__(options) do
        quote do
          import Kernel, except: [@: 1]
          import YourLibrary.TypeCheck, only: [@: 1]
        end
      end

      import Kernel, except: [@: 1]

      defmacro @ast do
        case ast do
          {name, meta, expr} when name in ~w[type! typep! opaque! spec!]a ->
            name = name |> Atom.to_string() |> String.trim_trailing() |> String.to_existing_atom()

            quote do
              Kernel.@(unquote({name, meta, expr}))
            end

          other ->
            quote do
              Kernel.@(unquote(ast))
            end
        end
      end
  end
end

(I have not tested this code but it should give you a good start to get this working if you really desire to.)

Be warned that this will only work as long as you are not using any of the extra types or features TypeCheck provides over what is supported by Dialyzer/the BEAM itself. Otherwise you'll get compile-time errors whenever you're trying to build the library without the optional dependency. And this is the reason that I don't really recommend this over including it as proper dependency but configuring it to be off by default.

Qqwy commented 1 year ago

Closing this issue now. Feel free to open a new one if you have any more questions related to this or other things you encounter while using the library :green_heart: !

zachallaun commented 1 year ago

Very understandable! Sorry I never got back to you on this but I really appreciate the pattern and direction!

On Fri, Oct 14, 2022 at 6:42 PM Qqwy @.***> wrote:

Closing this issue now. Feel free to open a new one if you have any more questions related to this or other things you encounter while using the library 💚 !

— Reply to this email directly, view it on GitHub https://github.com/Qqwy/elixir-type_check/issues/143#issuecomment-1279554065, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAD3BAWQ62ADZO2PUCLTZPLWDHOTPANCNFSM6AAAAAAQHWPMFU . You are receiving this because you authored the thread.Message ID: @.***>