jsonnet icon indicating copy to clipboard operation
jsonnet copied to clipboard

Add an easier way to check function params

Open sparkprime opened this issue 9 years ago • 4 comments

@huggsboson asked "Any interest in static or dynamically checked type assertions on parameters?"

You're the first to draw attention to this.

We decided pretty early on that dynamic typing would be most approachable. It fits in better with the laziness and dynamic binding, and avoids all the syntactic overhead of type annotations. There was a previous language that used type inference (HM system) but people found the unification errors hard to understand. Adding some form of gradual typing into the language now would probably be a bit incongruous. However some middle ground might be to have a dynamic checking framework, e.g. maybe something that looks like:

local f(x, y) = 
    assert std.check({x: x, y: y}, { ... some spec here ... });
    ...

This is just a library addition That would allow consistent and (relatively) concise checking of function preconditions for libraries. It could also be conservatively interpreted by linting tools. We could add more syntax sugar to make it even more concise:

local f(x, y) =
    checkparams { ... some spec here ... };
    ....

The hardest problem here is defining what the spec should be -- how expressive is it, should it use some existing thing or subset of an existing thing (jsonschema is probably overkill).

sparkprime avatar Apr 01 '16 18:04 sparkprime

I've played around with this a bit and this is what I came up with:

local checkParameters(o) =
  local failures = [
    'Parameter %s is invalid' % n
    for n in std.objectFields(o)
    if !o[n]
  ];
  local tests = std.all([
    o[n]
    for n in std.objectFields(o)
    if !o[n]
  ]);
  if tests
  then true
  else
    std.trace(
      std.join(
        '\n  ',
        ['\nInvalid parameters:']
        + failures
      ),
      false
    );

local customCheck(value) =
  std.member(['a', 'b'], value);

local f(num, str, enum) =
  {
    assert checkParameters({
      num: std.isNumber(num),
      str: std.isString(str),
      enum: customCheck(enum),
    }),
  };

f(1, 1, 'c')

Output looks like this:

TRACE: a.jsonnet:15 
Invalid parameters:
  Parameter enum is invalid
  Parameter str is invalid
RUNTIME ERROR: Object assertion failed.
	a.jsonnet:(29:5)-(33:7)	
	Checking object assertions	
	During manifestation	

More concise it could look like this:

local f(num, str, enum) =
    checkparams {
      num: std.isNumber,
      str: std.isString,
      enum: function(x) std.member(['a', 'b'], x),
    },
   ...

Duologic avatar Jun 13 '23 14:06 Duologic

I went down the rabbit hole and brought back a jsonnet library: https://github.com/Duologic/validate-libsonnet

Duologic avatar Jun 15 '23 17:06 Duologic

    checkparams {
      num: std.isNumber,

This approach doesn't allows to return what exactly is wrong with the parameter. What about adding two builtins, and then trying to make syntax sugar on top of them?

std.context(contextStr, value) - adds an "context" line to the stack trace, same as the standard "During manifestation" std.force(arr) - forces all array/object values, batching errors, i.e. 'std.force([error "1", error "2"])' should print both errors, instead of stopping on first.

This way we can both specify the parameter name on the call site and then throw the correct error message somewhere deeper in the call stack (This allows us to reuse existing asserts in the code).

CertainLach avatar Jun 15 '23 17:06 CertainLach

In jrsonnet, I had an experiment on adding such type-system like thing:

function(a: t.int, b: t.string, c: t.schema({...})) a + b

Being evaluated the same way as

function(a, b, c) local _a = t.int(a), _b = t.string(b), _c = t.schema({...})(c); force _a, _b, _c; _a + _b

But I can't find this experiment right now.

CertainLach avatar Jun 15 '23 17:06 CertainLach