Comments (7)
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.
Wow, thanks for adding this to the library. Looks like a ton of work. Great job!
from elixir-type_check.
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 inspec
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.
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:
- For existing codebases, it wouldn't blow up when changing a
@spec/@type
to@spec!/@type!
- Providing assurance that the input is correct
- Generating regular typespecs
from elixir-type_check.
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:
- 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.
- 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.
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.
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)
- Lazy generates wrong type when used with one of HOT 1
- Strategy for using type-check as an optional dependency HOT 3
- Unexpected behaviour when defining multiple specs for a function HOT 2
- Compilation error when using `conforms?` HOT 3
- Fully qualified function names in `@type!` guards HOT 1
- UndefinedFunctionError with TypeCheck.conforms HOT 3
- `%{String.t() => any}` syntax raises compile error HOT 6
- Qualified type name is not (always) picked up in specs HOT 1
- `String.t()` is not properly picked up as typename inside map syntax HOT 2
- Unresolved aliases
- crash with `protocol Enumerable not implemented` error HOT 1
- Compile-time dependencies HOT 8
- Union as type argument does not work for remote types HOT 1
- Issues with @spec! on auto-generated type like TypedEctoSchema HOT 2
- Can not define type in a non-Elixir module HOT 3
- Error in Elixir 1.15 trying to use the module TypeCheck.Type which is currently being defined HOT 5
- Rethink approach of 'dogfooding' in TypeCheck.Builtin module
- @type! declarations referencing specific objects cause Dialyxir errors
- protocol TypeCheck.Protocols.ToCheck not implemented for [] of type List
- Fresh LV project - compile time error with type_check HOT 6
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from elixir-type_check.