Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type validation via when clause in functions #2

Open
x4lldux opened this issue Sep 1, 2016 · 8 comments
Open

Type validation via when clause in functions #2

x4lldux opened this issue Sep 1, 2016 · 8 comments

Comments

@x4lldux
Copy link
Owner

x4lldux commented Sep 1, 2016

No description provided.

@x4lldux x4lldux mentioned this issue Sep 1, 2016
@x4lldux
Copy link
Owner Author

x4lldux commented Sep 1, 2016

continuation the discussion from #1 with @OvermindDL1

I think types should be checked by dialyzer, especially since there is no way for checking it at compile time by macros. The best we can do, is to properly generate typespecs for each case (which is not being done yet) and for each constructor.

I also think, the most important features of this lib are it's compile-time warnings. I'm afraid adding a feature that would work only in some cases will bring more harm than good (the when clause would work only when using from!/1 constructor but not when using from/1).

Being explicit should be the default, and this is one of Elixir's goals. That's why user can always define their own constructors which will add things like function gaurds and the user will have the notion that functions run at run-time so there will not expect compile-time warnings in this case.

That explicitness is also why I'm currently thinking about making the dynamically built constructors (based on union tag's name) to be disabled by default, and adding more explicit constructor (also a macro) that would work like this:

defmodule  Result do
  use DiscUnion
  @type value() :: any()
  @type error_msg :: String.t
  @type last_value :: any()
  defunion Ok in value | Error in last_value * error_msg
end

defmodule X do
  require Result

  def x do
    Result.c Ok, 123
    Result.c! Error, 0, "FUBAR"
  end
end

The difference between c constructor and from/1 constructors is the former one has varying number of parameters, which depends on number of arguments each case has and the later one is designed mainly for interacting with statements that return a tuple (a defacto main encapsulation technique used in the BEAM). Making the bang-functions overridable, giving users the ability to create their own versions, with what ever validations they want.

@OvermindDL1
Copy link

Hmm I like the explicit constructor, still think a 'when' validation would work well on them to enforce that the right information is stuffed into the type. But I am liking how that syntax works there. I might recommend trying to enforce use Result instead of require that way you can add more macro's at inclusion time if the need arises later (a basic __using__ that just require's it is fine to start with).

And yeah, I am a fan of tagged tuples over map/structs myself, but I'm an old erlang programmer myself. ^.^

@x4lldux
Copy link
Owner Author

x4lldux commented Sep 1, 2016

The biggest problem I have with the guards idea is this:
Result.c! with when gaurd will have consistent validations: both, case checking and when
guard, would be done at run-time.
But Result.c would have a mixed validation:

  • at compile-time: validation of case tag name and number of params
  • at run-time: validation done by the when clause

This mixed validation is where I have mixed feelings (pun intended 😉).

I might recommend trying to enforce use Result instead of require that way you can add more macro's at inclusion time if the need arises later

That's a good idea! Shorter to type and might can be handy someday.

And yeah, I am a fan of tagged tuples over map/structs myself, but I'm an old erlang programmer myself. ^.^

Here we have both! 😄
Struct as a commin type for the entire union and a container for cases, which are tagged tuples. Using structs has also another reason: protocols! This gives the ability to combine unions with libraries like expede/witchcraft, slogsdon/elixir-control or rob-brown/MonadEx.

I will leave this open, because I'm not 100% sure about this yet. Even @josevalim was asking about this in elixir-lang/elixir#925.

@OvermindDL1
Copy link

This mixed validation is where I have mixed feelings (pun intended 😉).

I personally like the mixed validation, as this is a dynamically typed language there is no way to enforce types internally at compile-time, so I take whatever and everything that I can get. :-)

This gives the ability to combine unions with libraries like expede/witchcraft, slogsdon/elixir-control or rob-brown/MonadEx.

Ooo, a couple libraries I've not not come across yet, thanks!

I will leave this open, because I'm not 100% sure about this yet. Even @josevalim was asking about this in elixir-lang/elixir#925.

Fascinating read, interesting to see that he and I are on the same page. What I would like is that the constructors for the types had when clauses that we could optionally apply (thus can fully control what types are inside a given case). That means we have less 'when' matching on the case/match itself since we know that the incoming bound is already correct (no need to when is_integer(something) when we already know it is that by the constructor), plus it is ALWAYS better to die on the constructor instead of the matcher (so we know 'who' is at fault, instead of just that a fault happened). Excepting Elixir becoming fully typed (oh I wish!) I am instead a fan of putting when clauses on near everything. Hmm...

@x4lldux
Copy link
Owner Author

x4lldux commented Sep 1, 2016

Excepting Elixir becoming fully typed (oh I wish!)

Probably won't happen. Better check out j14159/mlfe, but they will have one big problem: static typing on messages from outside.

That means we have less 'when' matching on the case/match itself since we know that the incoming bound is already correct (no need to when is_integer(something) when we already know it is that by the constructor

Except you wouldn't! Not unless macro constructors are removed, and I actually think they have the biggest value here - more compile time warnings! Look here, and let's assume that when clauses are supported:

defmodule  Result do # same as above
  ...
end

defmodule X do
  def x do
    val = 1
    Result.c! Ok, val # This is a function call. Once evaluated, returns either
                      # a %Result{case: {Ok, 1}} struct or raises an error if case
                      # tag or param are not valid - param is validated via guards

    Result.c Ok, val # This is equivalent to typing %Result{case: {Ok, val}}
                     # This is a macro, so the struct is put in place of the call
                     # during compilation. So any `when` gaurd clause could not
                     # be ran cause you cant put guards into a struct and the
                     # value of `val` is known only at run-time

    # now let's make a typo!
    Result.c! Eror, :wat # Would rise an error only at run-time
    Result.c Eror, :wat  # Would rise straight away at compile-time.
  end
end

That means we have less 'when' matching on the case/match

You can still achieve that by creating your own constructor functions!*

defmodule Result do
  use DiscUnion, dyn_constructors: false

  defunion Ok in any | Error in any

  def ok(any), :do c Ok, any
  def error(reason) when is_atom(reason) do
    c Error, reason
  end
end

*Once this c constructor is implemented 😄

... plus it is ALWAYS better to die on the constructor instead of the matcher (so we know 'who' is at fault, instead of just that a fault happened)

That's why I think that user-built constructors are the way to go here.

The most frequent use cases I see for discriminated union is creating collections of identifiers. Like in here: x4lldux/ex_json_schema@bff986d/lib/ex_json_schema/validator/error.ex or for creating a collection of CQRS/ES events. With that many variants simple human error will be more frequent - like typos or case omissions - than a wrongly passed case param. At least this is what I believe. For now, I still need to implement the c constructor generate more typespecs.

@OvermindDL1
Copy link

Probably won't happen. Better check out j14159/mlfe, but they will have one big problem: static typing on messages from outside.

That actually seems like a pretty easy issue to resolve, some kind of 'cast'er that returns a Result for Ok theWantedType or Error reason. Will have to design the receive differently, match based on structure then enforce based on type, hence why you always have to handle an Ok/Error result because no putting it back on the mailbox transparently (which is often bad design anyway, so it is good to enforce that out).

@OvermindDL1
Copy link

Sent too soon, continuing...

Except you wouldn't! Not unless macro constructors are removed, and I actually think they have the biggest value here - more compile time warnings! Look here, and let's assume that when clauses are supported:

I would honestly be fine with removing them from match guards. Match guards get integrated into a single function with a head case structure anywhere and I tend to try to follow that pattern already anyway. However, there is a way to work around that by defining a custom def that could integrate such a macro'd when clause into the main clause. Not hard to make but would turn your example into:

# From this:
def func(Result.c(Ok, val)), do: nil
# To this:
defsomething func(Result.c(Ok, val)), do: nil

The most frequent use cases I see for discriminated union is creating collections of identifiers

Yeah that would be the big use-case for me as well, I have a lot of areas where enforced checking like this would save me a lot of hassle.

@x4lldux
Copy link
Owner Author

x4lldux commented Sep 1, 2016

I would honestly be fine with removing them from match guards. Match guards get integrated into a single function with a head case structure anywhere and I tend to try to follow that pattern already anyway. However, there is a way to work around that by defining a custom def that could integrate such a macro'd when clause into the main clause. Not hard to make but would turn your example into:

Don't quite understand your message here. Can you rephrase that? By those "macro constructors" I meant those autogenerated macros for each union, from/1, dynamic constructors and that future c macro.

That actually seems like a pretty easy issue to resolve

I think it's not. Type check they implemented, infers based on what it can find in the code. Now, what would happen to messages sent from outside - say a :DOWN message sent by the BEAM; typechecker has no way of knowing this message can be sent. They even say it them selfs:

If you spawn a function which nowhere in its call graph posesses a receive block, the pid will be typed as undefined, which means all message sends to that process will be a type error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants