typecheck icon indicating copy to clipboard operation
typecheck copied to clipboard

support checking against leafo's tableshape type object predicates

Open jvprat opened this issue 9 years ago • 12 comments

Hi,

I'm on the process of writing a library that needs to do type checking for arbitrary values, other than arguments and return values, and I'd like to reuse the type specification syntax from this library.

Would you mind adding a new public function that factors out this functionality? I could work on the task if you're interested. (Or maybe you know a better library for this? I haven't found any)

By the way, thanks for working on this library on the first place! It's very useful :smiley:

jvprat avatar Mar 22 '16 09:03 jvprat

Hi!

I'm not sure I understand what you're trying to do exactly. Could you show me some code to demonstrate how you would like to be able to call in to this library imagining that you have any APIs you need already available?

Thanks for the props, much appreciated :)

gvvaughan avatar Mar 22 '16 11:03 gvvaughan

In short, I'd like to be able to do assert(type(a) == "string") but with the extended syntax and types offered by the typecheck library. For example, something like this: assert(typecheck.check_type("string|bool", a)), and maybe offering a convenience assert.

jvprat avatar Mar 22 '16 11:03 jvprat

Hmm. I think you're on to something here. Assert would prefer that we return a nil, "oh no" tuple than raise an error, so we might need to re-architect the internals a little to expose the right bits to support that as well as maintain the current APIs. This would also be useful to feed back through to the expectations module of my Specl project which is already somewhat assert-like, but is currently still wrapped around a much older less flexible version of the code in this project.

Have you seen https://github.com/leafo/tableshape ? I'm also wondering how I can support this style, which would mean that instead of simply looking at the contents of a _type field in the metatable of an argument being typechecked, it would be possible to compose a predicate that could potentially validate the presence and types of table values & keys for much richer TypedLua style checking.

gvvaughan avatar Mar 22 '16 21:03 gvvaughan

Something like this:

local expect = require 'specl.expect'
local types = require 'tableshape'.types

local mypredicate = types.shape { _type = "Object", value = types.string } 
expect (thing).
  to_be_type (types.any_of (types.boolean, types.["function"], mypredicate))

gvvaughan avatar Mar 22 '16 21:03 gvvaughan

I didn't know tableshape, it looks interesting. In fact I'm working on a library similar to it, trying to add runtime support for typed tables, and I was planning to rely on typecheck for the field type checking (I like that its syntax is similar to what's proposed for Typed Lua).

From my point of view, the ideal would be to have a common base library for type checking, and then adding extra modules that use this functionality for function checking (what typecheck does right now) and table checking (what I'm trying to do and what tableshape may already be doing).

For the common type checking, it seems tableshape allows for more powerful type descriptions, but the typecheck style is easier to read. Maybe we could make the typecheck syntax to be parsed into tableshape descriptions?

I'm basically testing the waters, I'm relatively new to the Lua ecosystem and I'm not sure how this would be seen by the community. Do you think it's the way to go or I should try a different solution?

Thanks!

jvprat avatar Mar 23 '16 10:03 jvprat

Sorry for the delay in responding... I didn't have much hacking time over the last few days.

But, I've given this some more thought, and I found a nice way to integrate the flexibility of tableshape with the cleaner syntax of this library - I think that adding an API for registering a complex tableshape structure with the typecheck using a name string in advance of first use, and then looking up otherwise unhandled names in the list of registered additional checks to be handed off for matching in the tableshape library. From the outside, something like this:

local typecheck = require 'typecheck'
local types = require 'tableshape'.types

local StringObject = types.shape { _type = "Object", value = types.string }

typecheck.register ("StringObject", StringObject)

return {
  doit = argscheck ("doit (string|StringObject+) -> string", function (...) return stuff(...) end),
}

I wouldn't worry too much about the community... the key thing is to produce a useful library and be open to constructive feedback. If the thing is useful and the developer(s) responsive, people will use it :)

WDYT?

gvvaughan avatar Mar 29 '16 10:03 gvvaughan

I didn't like it at first because having to register the types adds a possible clashing point: there could be more than one user trying to register different types with the same name.

On a second thought, I think it's okay if it's offered as a helper but it would be great if there was also an alternative to use the types directly, without having to do the name parsing & lookup. I'm thinking of something like:

local argscheck = require 'typecheck'.argscheck
local types = require 'tableshape'.types

local StringObject = types.shape { _type = "Object", value = types.string }

return {
  doit1 = argscheck ("doit1 (?number, string|StringObject+) -> string", func1),
  doit2 = argscheck ("doit2", { "?number", types.one_of { types.string, StringObject } }, "string", func2),
}

So, to summarize, what about allowing both options?

  1. argscheck ("full typespec string", function)
  2. argscheck ("function name", { argument types }, { return types }, function)

Where each argument or return type could be either:

  1. a tableshape type (e.g. types.string)
  2. a typespec string that can use the previously registered tableshape types (e.g. "string|number" or "StringObject+")

jvprat avatar Mar 30 '16 08:03 jvprat

Hi!

Good point about the possibility of nameclashes. However, I don't like the idea of having two ways of doing the same thing too much, it makes the API ugly, and means there is more code to maintain to achieve the same thing.

However, instead of keeping a central registry and all the problems that might cause for contention between different clients in the same address space; so the important thing is to provide a way to make a local map from type-name (as used in the typespec string) to type checking predicate, that can be passed around and reused in isolation from another library that might want to add a different set of mappings.

Here's a first draft of an API for that:

local typecheck = require 'typecheck'
local types = require 'tableshape'.types

local typemap = typecheck.Map {
  StringObject = types.shape { _type = "Object", value = types.string }
}

-- I'd probably use std.functional.bind(), but for clarity let's hand roll a closure here:
local check = function (...) return typecheck.argscheck (typemap, ...) end

return {
  doit = check ("doit (?number, string|StringObject+) -> string", func),
}

That is, the typecheck.Map call is a constructor that returns the existing set of mappings between type names and checking predicates, and we can change or add mappings to that instance at any time; but when passed as an additional optional first argument to argscheck, as the typespec string is parsed the type names are all looked up in that typecheck.Map instance. Another module from a different author/library/module will instantiate its own map if the default map is missing some type predicates that would be useful there.

This also has the advantage of being entirely backwards compatible so any code upgrading from the existing release, or the earlier version from inside lua-stdlib won't require any changes to carry on working the same way.

I'd be happy to work on this in the coming weeks. Doubly so if it meets your needs too?

gvvaughan avatar Mar 30 '16 10:03 gvvaughan

It's definitely is a step forward. It just feels to me like the "check" function is something that everyone will be doing, and it feels artificial. Can we make it easier to use?

In addition to that, I think it would be ideal to make these type mappings available for the other methods of the API (argcheck, parsetypes, whatever...) and the individual value checking that was proposed in the first place here :smile:

That leads me to think of an ideal API (I'm just thinking from the user POV now) where the Map function returns a table with the same functions as the original typecheck, but using the mapped types:

local typecheck = require 'typecheck'
local types = require 'tableshape'.types

local custom_typecheck = typecheck.Map {
  StringObject = types.shape { _type = "Object", value = types.string }
}

return {
  doit1 = typecheck.argscheck ("doit (?number, string+) -> string", func),
  doit2 = custom_typecheck.argscheck ("doit (?number, StringObject+) -> string", func),
}

It could easily be used like typecheck = typecheck.Map { ... } and the client code wouldn't even notice the addition of the new types.

jvprat avatar Mar 30 '16 13:03 jvprat

I like it!

Probably no need for a .Map either, just add a __call metamethod to the table returned by require "typecheck" - either use the APIs from the module return table for the existing built in type checks, or pass a table of custom name -> predicate mapping to get a new table with all the same methods, but capable of parsing the custom map elements too.

Added to my TODO list. Thanks for the help shaping this idea into something nice and clean without complicating the API :-)

gvvaughan avatar Mar 30 '16 14:03 gvvaughan

This proposal looks really great. I especially like named table shapes, because in the library I'm working on, I'm also using some form of LuaDoc (provided by LuaLs) so that users have autocompletion. Being able to refer to the same name in a type would be a great way to provide consistency at edit- and run-time.

I see however that this issue is already a little old and has a help-wanted. (I assume it's just for lack of time, which I understand.) Has it just been brainstorming so far or is there a work-in-progress that I can play around with and build upon?

cassava avatar Oct 12 '23 09:10 cassava

I noodled around with an API but didn't keep the code. I think this issue documents a nice interface now, but I haven't made the time to flesh it out, so nothing more to show than what's here already. It's on my (ever-growing) todo list, but if you had time to push an implementation along that would be awesome!

gvvaughan avatar Oct 12 '23 14:10 gvvaughan