GithubHelp home page GithubHelp logo

Comments (7)

baldwindavid avatar baldwindavid commented on August 15, 2024 1

Just noting some real-world usage here related to this issue. I have a commonly used convention referenced here... https://elixirforum.com/t/balancing-elixir-context-design-with-flexible-web-apis/33989/3?u=baldwindavid

This is basically an anonymous function with a single argument. It probably doesn't matter much beyond that, but it is typically passed an ecto query and then a series of pipes.

I am stubbing out that type with the following custom type...

@type! query_pipe :: function

The idea here is that at some point this function might be replaced with something more specific.

To take it a little further, if I am able to specify the type of that argument, I do have a custom type for checking that something is an Ecto queryable...

@type! queryable :: any when is_queryable?(queryable)

def is_queryable?(queryable) do
  !is_nil(Ecto.Queryable.impl_for(queryable))
end

from elixir-type_check.

baldwindavid avatar baldwindavid commented on August 15, 2024 1

Wow, thanks for adding this to the library. Looks like a ton of work. Great job!

from elixir-type_check.

Qqwy avatar Qqwy commented on August 15, 2024

I've read up on contract-systems in other languages. Most notably, Racket contains a contracts module that has a very interesting API and a great guide that we can learn some things from. https://docs.racket-lang.org/reference/contracts.html

For instance, we can test anonymous functions by creating a closure 'wrapper' with the same signature as the original function, which first tests the function parameters, then calls wrapped the anonymous function, and then tests its result and finally returns the result.

This means:

  • that the check is only evaluated when the function ends up actually being called.
  • In other words, since an anonymous function is changed rather than immediately evaluated, using this in combination with conform and friends does not make sense; only using it in spec makes sense.
  • we need to alter the parameters that are passed to a spec. Until now, we've only checked them without ever altering them, so this requires a significant change in all compound checks (lists, maps, tuples, etc) so we're able to handle anonymous functions that are nested inside something else properly as well.

So while this might be useful and very interesting, it also would mean a lot of work and we'd end up with a check that would only work in certain situations, so it's something to postpone until later and think about some more.

Before the stable version, however, we definitely should be able to read the anonymous function syntax and at least create a check that wraps is_function(x, arity) with the arity provided by the anonymous function syntax.

from elixir-type_check.

baldwindavid avatar baldwindavid commented on August 15, 2024

This is simultaneously the syntax I would most like (that is left) AND the most tricky to implement. I do wonder if there is any easier intermediate step. For instance, would there be any efficiencies in supporting (-> type), (type1, type2 -> type), (... -> type), but making this only partial support in that it only checks the input rather than the return value?

This would provide three valuable things:

  1. For existing codebases, it wouldn't blow up when changing a @spec/@type to @spec!/@type!
  2. Providing assurance that the input is correct
  3. Generating regular typespecs

from elixir-type_check.

Qqwy avatar Qqwy commented on August 15, 2024

The easy part is recognizing the syntax ( -> type), (... -> type) and (type, type, type -> type).
We could rather easily make this compile to a check which calls is_function/1 or is_function/2.

The hard parts are:

  1. Actually checking that the function-parameter itself is:
  • Called with the expected parameter types.
  • Returning the expected result types

The problem here lies in the fact that these checks can only run when the function-parameter is actually invoked (rather than right when the function-with-the-spec is called as all other checks are).
Moreover, the only way to add the checks is to wrap the function-parameter in an anonymous function which adds the checks on top. In pseudocode:

# given as example a parameter called `fun` which itself expects three parameters:
fun = fn arg1, arg2, arg3 ->
  TypeCheck.conforms!(arg1, type_of_arg1)
  TypeCheck.conforms!(arg2, type_of_arg2)
  TypeCheck.conforms!(arg3, type_of_arg3)
  result = fun.(arg1, arg2, arg3) # Here the original function is called
  TypeCheck.conforms!(result,  type_of_result)
end

No other check alter the parameters of the function-with-the-spec. To make function-checks work reliably we would now need to support altering the parameters. If a function is passed as a top-level parameter this is doable. If it is however nested inside any other datatype (tuple, map, list, etc) we would need to alter this containing structure.
In other words: this altering-code would need to be supported by all recursive checks as well.

  1. Data generation for property-testing
    There is a simple approach where we just return a newly-generated value whenever the function is called, but this breaks shrinking. The 'proper' approach which is used by e.g. QuickCheck, Hedgehog, Hypothesis etc. is to turn the actual values the function is called with into the seed which is used to generate the output value, making the generated function deterministic/pure.
    This is not that difficult, but it is quite a bit of work to get right. (We would need to introduce 'coarbitrary' instances for all Elixir datatypes.)

Arguably this second point is the less difficult one of the two, and it is possible to wait with introducing the deterministic aspect of function-generation.


What do you think? Is it worthwhile to already add the possibility to recognize -> even though the resulting checks will be imprecise (accept any function of the expected arity) and will not support data generation/spectests?

from elixir-type_check.

baldwindavid avatar baldwindavid commented on August 15, 2024

Thank you for the explanation. Yeah, I was referring to the return value as the entire anonymous function when it is executed. Understood that the easy part ONLY means checking that this is a function at all and that nothing inside the function would be checked.

I think there is value in those 3 points I mentioned above. Any lack of support requires that an existing codebase either doesn't use the syntax or doesn't switch to the @spec!/@type! syntax for an annotation.

I do think it would be problematic if, say, only half of the typespec syntax was supported, because it would be hard to keep track and trust that anything is being checked. However, given most is already supported (and probably more coming), I think it's okay to introduce one bit of partial support.

I suppose you could also make it opt-in if you like with something like...

use TypeCheck, allow_unchecked_anonymous_functions: true # default: false

That requires someone to make that decision to opt-in. Then, if the full support is ever added, a compilation warning or something could alert them that the opt-in option is no longer necessary and that anonymous functions are fully checked.

from elixir-type_check.

Qqwy avatar Qqwy commented on August 15, 2024

Yes, it was quite insane. I had to start over two times because making sure that deeply nested wrapped functions are passed on correctly was quite tricky.

I'm very happy that it was possible to make it work; TypeCheck feels much more mature now 🙂 .

from elixir-type_check.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.