x4lldux / disc_union

Discriminated unions for Elixir
MIT License
64 stars 1 forks source link

Example issue #1

Closed OvermindDL1 closed 8 years ago

OvermindDL1 commented 8 years ago

I noticed in the README.md the line in the code of defunion :ok in any | :error in String.t, and yet the :error case is later matches to atoms, not strings. Does this library put type declarations as appropriate? Is there a way for us to specify when clauses on the constructor so we can enforce types?

x4lldux commented 8 years ago

As described in readme, there are no type checking. Whatever you put there will be for user documentation purely. Putting a when clause would be counter-intuitive in moste cases, because the aim is to have compile time warnings, but it's impossible/extremely- hard to determine the type at compile time in Elixir. On the other hand, you can create a custom constructor which will contain the when clause you want - but since this will be a function, it will report mistakes/errors only at run-time.

I'm working on fully generated typespecs, for each clause, macros and functions generated by defunion, do dialyzer can caught such bugs as you mentioned.

OvermindDL1 commented 8 years ago

As described in readme, there are no type checking.

That is why I had the idea to add some ability to add a when clause to enforce some at-least run-time type checking on the constructors. :-)

Whatever you put there will be for user documentation purely.

The example user documentation is not matching the exampleusage is what I was talking about there. :-)

x4lldux commented 8 years ago

The example user documentation is not matching the exampleusage is what I was talking about there. :-) You mean something like this: defunion :ok in value: any | :error in msg: String.t ?

I'm thinking about that syntax but haven't made my mind yet. Even in current implementation you can still have typespecs and description both:

@type value :: any
@type msg :: String.t
defunion :ok in value | :error in msg
OvermindDL1 commented 8 years ago

As in the example where :error is documented as being a String.t, but a few lines lower :error is only storing atoms. :-)

As for a when syntax, hmm... I know that | is more how functional languages define different types but , might be more idiomatic elixir (regardless of if better or not)? As for when's... Maybe:

defmodule Result do
  defunion do # This also leaves open future 'option' opportunities to be added to the defunion call
    @spec ok(any())
    def ok(value)
    @spec error(String.t)
    def error(msg) when is_binary(msg)
  end
end

Or something like that, just in a way where each union subtype defines what it can store in an elixir function-like head form, thus the number of arguments it takes as well as the 'when' is easier to read for most Elixir programmers. I'm wondering if it would be better to have defunion make its own module instead of injecting into the parent, if so it could be like:

defunion Result do
  def ok(value)
  def error(msg) when is_binary(msg)
end

And I wonder if there is a way to 'enhance' the types, hmm... Let me play with syntax...

defunion Result, ok_type: :any, error_type: :any do # :any would be a 'key' type of no 'when' clause by default
  def ok(value)
  def error(reason)
end

# Then elsewhere like my own custom modules:
defmodule Stuff do
  defunion Result, refines: Result, ok_type: is_map(&1), error_type: is_binary(&1) # No `do` since we are refining it, though we could add a `do` if we wanted to add more cases or helpers perhaps?
end

This way my 'Stuff' module uses its refined Stuff.Result type for its return types. You can easily implement this by making a hidden function on every union that defines its data that another union could then bring in and refine or so. Mostly thinking out-loud but such a refinable union would be quite nice, especially with proper when's being put on and all for additional run-time checks. I checked the syntax and it works. Hmm, ideas...

x4lldux commented 8 years ago

As in the example where :error is documented as being a String.t, but a few lines lower :error is only storing atoms. :-)

I'll add a comment to remind readers that types are not checked.

As for a when syntax, hmm... I know that | is more how functional languages define different types but , might be more idiomatic elixir (regardless of if better or not)?

I was experimenting with using comma, but it turned out that elixir was interpretation as a arguments separator for macro. That would mean there should be defunion/1, defunion/2, defunion/3, etc.. Also, the | symbol is already used for defining the same meaning in typespecs! It's used to define different possible clauses, which is basically what we are doing here. I'm not bent on this syntax, but is composites nicely with current typespecs syntax and syntax of other functional languages that do have summation types.

 def ok(value)

Reusing def is definitely a bad idea. It has a strict meaning, and giving it another one, in context of defunion, would only confuse developers. This syntax also doesn't let using ModuleNames as case tags.

I'm wondering if it would be better to have defunion make its own module instead of injecting into the parent

That would require a defunion/2 be part of the Kernel module, which is always imported.

Regarding types and when gaurds, I've moved the discussion to issue #2, since this issue is mainly about mismatch in the example.

OvermindDL1 commented 8 years ago

The naming of def was just an example, it could be named anything, just too lazy to type out something longer. ^.^

x4lldux commented 8 years ago

Fixed in 9fe3c24d8a40e2a3d7cf561f36e88cec6381b25a.