Would it be possible to create @sp as an alias of @spec?

I’d like to write my specs like this so that they line up with the function, Haskell-style:

  @sp is_prime(integer) :: boolean
  def is_prime(n) do
   #...
  end

(I find that a lot easier to read when scanning down a screen of function definitions.)

But is there some easy way to create a new @sp which would be a synonym for @spec?

How about specs for private functions?

it’s possible. You have two options, both have problems in terms of being idiomatic Elixir.

  1. with a use ShortSpec statement. You can write a module ShortSpec that defines the __using__ macro which makes @sp an accumulating module attribute, then inserts a @before_compile ShortSpec directive. You then define a before_compile macro in ShortSpec which reads the calling module’s attributes and unrolls all of the @sp values into @spec statements.
  2. with import Kernel, except: ["@": 1] and import ShortSpec, only: ["@": 1] and then write an @ macro in shortspec that overloads :sp as a special case.

I feel like the only objection about option #1 is that “you shouldn’t spuriously use use statements”. The substantive reason is that if you’re ever going to work on a team, this reduces readability because people won’t immediately know what your non-idiomatic @sp is.

Option #2 feels really dirty because overloading operators in Elixir is very not cool without a damn good reason, and it’s doubly bad since @ is core, and all the substantive objections about #1

3 Likes

Additionally to @ityonemo answer, depending on your editor, you can just change the way that value is displayed on screen instead of hacks.

5 Likes

That’s a really good point.

My dream is to have a pre-processor or macro that could allow one unified syntax:

  def is_prime(n: integer) :: boolean do
   #...
  end

I’m personally not a fan of inline types. Even your example is already 40-ish characters wide, and that’s with a short function, non-descriptive argument naming and an arity of one, never mind having any pattern matching.

2 Likes

That’s not a tall order. Go for it!

Yes, this is possible. Here is the minimal viable example.

.formatter.exs

[
  inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"],
  export: [
    locals_without_parens: [defs: 2]
  ]
]

custom_spec.ex

defmodule CustomSpec do
  defmacro defs({:"::", _, [{fun, _, [args_spec]}, {ret_spec, _, nil}]}, do: block) do
    args = for {arg, _spec} <- args_spec, do: Macro.var(arg, nil)
    args_spec = for {_arg, spec} <- args_spec, do: Macro.var(spec, nil)

    quote do
      @spec unquote(fun)(unquote_splicing(args_spec)) :: unquote(ret_spec)
      def unquote(fun)(unquote_splicing(args)) do
        unquote(block)
      end
    end
  end
end

custom_spec_test.ex

defmodule Foo do
  import CustomSpec

  defs is_forty_two(n: integer) :: boolean do
    n == 42
  end
end

iex

iex(5)> Foo.is_forty_two 42
#⇒ true
iex(6)> Foo.is_forty_two 43
#⇒ false

Now one might make it working for functions taking no block, and also play with unimporting Kernel.def and delegating def to an explicit call to Kernel.def.

3 Likes

Awesome! I can learn a lot from that.

I am still positive one should not do that, though.

WARNING, the functions argument are a valid pattern match! is_prime(2) will crash with a FunctionClauseError, while is_prime(n: 2) will not.

Annotating types inline in more complex pattern matches can be quite tedious, thats the main reason why they are defined in an extra place and not inline.

Of course Kernel.def should be explicitly unimported. OtherModule.def macro is to be explicitly imported and implemented roughly as shown in my answer, delegating to Kernel.def.

Yes of course, but I wouldn’t reuse currently valid pattern matching and give it a new meaning, it would just confuse people.

That’s the exact reason I used defs in my answer and did not explore further to all this unimport-import-delegate joggling :slight_smile:

You should be able to do it by overloading ::, which can’t be confused.

1 Like

slightly different (adding guards) but this has been bugging me for a while so I scratched this itch:

defmodule TypedHeadersTest do
  use ExUnit.Case

  defmodule BasicTypes do
    use TypedHeaders

    def int_identity(value :: integer) :: integer do
      value
    end

  end

  test "integer headers" do
    assert 47 == BasicTypes.int_identity(47)
    assert_raise FunctionClauseError, fn ->
      BasicTypes.int_identity("not_an_integer")
    end
  end
end

here’s the implementation: https://github.com/ityonemo/typed_headers/blob/master/lib/typed_headers.redef.ex

Adding auto-typespeccing would be pretty trivial (I’ve already implemented this as a part of zigler)

Is this something people would use?

2 Likes

That’s really cool. The guards are a second way that we’re encoding the same information - the types of the parameters. I’d really like to reduce the boilerplate.