moonscript icon indicating copy to clipboard operation
moonscript copied to clipboard

Argument checking

Open exists-forall opened this issue 13 years ago • 36 comments

Built in functions produce very comprehensible error messages for bad argument types

math.sin("a string that does not belong") -- Bad argument #1 to function 'sin'.  Number expected, got string.

However, user-defined functions produce cryptic error messages when fed bad arguments or, sometimes, no error message at all

root = (n, exp)-> n^(1 / exp)
root(16)  -- Attempt to perform arithmetic on 'exp' (a nil value).

A moonscript idiom to create well-formatted error messages for bad argument types would be very convenient.

root = (n as number, exp as number)-> n^(1 / exp)
root(16) -- Bad argument #2 to function 'root'.  Number expected, got no value.

Also, for methods (fat arrow syntax), the argument index in the error message should be shifted by one, because

class Foo
    bar: (a as string)=>
        -- method body

Foo!\bar 2

should complain about the first argument to bar, even though, technically, the first argument is 'self'.

exists-forall avatar Dec 31 '12 05:12 exists-forall

It should then support multiple types and have option to turn it off for performance reason (release build e.g.). Kinda foo = (number a, Class1 | Class2 b) ->

fillest avatar Dec 31 '12 11:12 fillest

for methods (fat arrow syntax), the argument index in the error message should be shifted by one

This can lead to even more confusion, because self is quite explicit in Lua. It's better to just use param name instead of index: --Bad argument 'exp' to function 'root'

fillest avatar Dec 31 '12 11:12 fillest

What about having expressions as types? Then the passed it type would be compared with the result of type or ideally moonscript's type.

http://moonscript.org/reference/standard_lib.html#typevalue

x = (arg as "string") -> 

Right now when you ask for the type of a user defined class it returns class object:. So we can reference classes this way:

class Something

y = (a as Something, b as "string") ->

Which would be roughly:

local function Something() end

local function y(a,b)
   local _type = moonscript.type
   if _type(arg) ~= Something then
      error("Bad argument...")
   end
   if _type(arg) ~= "string" then
      error("Bad argument...")
   end
end

leafo avatar Dec 31 '12 19:12 leafo

I don't want to add new keywords unless necessary. Alternative syntax could just be this:

z = ("string" a, Something b) ->

I don't think this syntax collides with anything I want to add in the future.

leafo avatar Dec 31 '12 19:12 leafo

I very much like the idea of arbitrary expressions. To take the abstraction one step further, how about a 'when' clause much like in a list comprehension or switch statement, for checking properties of arguments automagically.

factorial = (n when n >= 0 and n == math.floor(n)) ->

I agree with Fillest that there should be a flag to disable checking for release versions, for performance reasons.

exists-forall avatar Dec 31 '12 20:12 exists-forall

Yes, some sort of pattern matching has been on my mind for a while. So if I went with that approach it would handle this implicitly. I just haven't though about the syntax yet.

leafo avatar Dec 31 '12 20:12 leafo

But if there isn't pattern matching, putting that much logic into the argument list is probably not a good idea.

leafo avatar Dec 31 '12 20:12 leafo

A sort of multimethod system might be good, although the only efficient thing to do would be to dispatch based on type. Something like this maybe?

dot_product = ?> -- ?> being the multimethod symbol (like -> or =>)
    Vec2, "number": (a, b)->
        -- scalar multiplication
    Vec2, Vec2: (a, b)->
        -- ...
    Mat2, Vec2: (a, b)->
        -- ...
    Mat2, Mat2: (a, b)->
        -- ...

becoming

dot_product = setmetatable({}, {__call = function(self, ...)
    types = {}
    for i, arg in ipairs{...} do
        types[#types + 1] = type(arg)
        if types[#types] == "table" and types[#types].__class then
            types[#types] = types[#types].__class
        end
    end
    local implementation = self
    for i, t in ipairs(types) do
        implementation = implementation[t]
        if not implementation then
            error("Bad argument #" .. i .. " to function dot_product.")
        end
    end
    return implementation(...)
end})
dot_product[Vec2] = {
     [Vec2] = function(a, b)
         -- handle Vec2 * Vec2
     end;
     number = function(a, b)
          -- handle Vec2 * number
     end;
}
dot_product[Mat2] = {
    [Mat2] = function(a, b)
        -- handle Mat2 * Mat2
    end;
    [Vec2] = function(a, b)
        -- handle Mat2 * Vec2
    end;
}

exists-forall avatar Dec 31 '12 20:12 exists-forall

This implementation of multimethods is easily modified by other code. In a Mat3by2 class file:

dot_product[Mat3by2] or= {}
dot_product[Mat3by2][Vec2] = (a, b)-> -- ...
dot_product[Mat3by2][Mat2] = (a, b)-> -- ...

exists-forall avatar Dec 31 '12 20:12 exists-forall

What about reusing the syntax for argument type checking from above, so it could look like this:


func = ?>
  (Vec2 a, Vec2 b) ->
    --...
  (Mat2 a, Vec2 b) ->
    --...
  (Mat2 a, Mat2 b) ->
    --...

  (a,b) -> print "default action?"

leafo avatar Dec 31 '12 20:12 leafo

Reads very well, although it's a little bit conceptually at odds with the class syntax of having methods be fields.

I like the default action

exists-forall avatar Dec 31 '12 20:12 exists-forall

It's just like an array table, just delimited by indentation.

leafo avatar Dec 31 '12 20:12 leafo

also, it would be nice to have some symbol or word that represents not nil.

leafo avatar Dec 31 '12 20:12 leafo

"Value"?

exists-forall avatar Dec 31 '12 20:12 exists-forall

No, it conflicts with something that might actually have a type of "Value". And it's too verbose.

leafo avatar Dec 31 '12 20:12 leafo

"$"?

exists-forall avatar Dec 31 '12 20:12 exists-forall

I'm not sure

x = (? hello, ?world) ->
x = (~ hello, ~world) ->
x = (* hello, *world) ->
x = (! hello, !world) ->
x = ($ hello, $world) ->
x = (& hello, &world) ->
x = (+ hello, +world) ->
x = (| hello, |world) ->

leafo avatar Dec 31 '12 20:12 leafo

Also, maybe the semantics should be more like Ruby's class statement: creating a multimethod if it doesn't already exist, and extending it if it does.

multi dot_product
    (Mat2 a, Vec2 b)-> -- ...
    (Vec2 a, Vec2 b)-> -- ...
    -- ...

and in Mat3by2:

multi dot_product
    (Mat3by2 a, Vec2 b)-> -- ...
    (Mat3by2 a, Mat2 b)-> -- ...

exists-forall avatar Dec 31 '12 20:12 exists-forall

Although the ?> syntax is more consistent with the rest of Moonscript.

exists-forall avatar Dec 31 '12 20:12 exists-forall

IMHO a dollar sign is closest to the meaning (something with value, not just nil), but it does make everything look a little Perly.

exists-forall avatar Dec 31 '12 20:12 exists-forall

Underscore for "any type"? It's used for pattern matching in functional languages, and in Lua it means "dummy" or "we don't care what it is, just that it exists," which is somewhat similar.

exists-forall avatar Dec 31 '12 20:12 exists-forall

Ah yeah, underscore is good idea.

more options for not nil:

x = (not nil hello, not nil world) ->
x = (!nil hello, !nil world) ->

leafo avatar Dec 31 '12 20:12 leafo

That's by far the most readable, although a bit tedious to write. Doesn't introduce any new keywords, which is always good.

exists-forall avatar Dec 31 '12 20:12 exists-forall

Also, just so I don't forget. This type checking should probably also work with class inheritance.

leafo avatar Dec 31 '12 20:12 leafo

Yes. I've written multi-dispatch systems in the past, and I've found the best way to do that is to have a dispatch function that queries itself recursively. IE if class B extends A and class D extends C, when dispatch(B, D) doesn't find anything, try dispatch(A, D), which in turn tries dispatch(A, C). If neither of those work, the original call will try dispatch(B, C). Once you've found one (or, if you've found multiple, the one that's the "closest" to the original types), cache it and return it.

exists-forall avatar Dec 31 '12 20:12 exists-forall

Also, it should probably be able to handle extra arguments. If all those other dispatch functions failed, try dispatch(B, "..."), then dispatch(A, "..."), and finally, if all else fails, dispatch("...")

exists-forall avatar Dec 31 '12 20:12 exists-forall

This as an extensible mechanic would be very nice (eg compiler hook that controls its meaning would add quite a bit of flexibility).

ludamad avatar Jun 11 '14 01:06 ludamad

It seems to me, this is all a lot of complex syntactic overhead for a very specific feature. You should reconsider whether this is necessary. And rather, if it's necessary to add in as a special case, instead of providing facilities for the user to craft something themselves.

For instance, I think Moonscript could benefit in general from a preprocessor and some macro syntax. If implemented with enough flexibility, the user could trivially craft their own macro which the compiler expands into proper verbose type-checking if-statements. The benefit here is, you're not pigeon-holing the user into a type checking interface that might not be as flexible as they desire; and moreover you're still adding more syntactic rules to the language, but to serve a greater purpose than just one feature.

I vote on keeping with the Lua philosophy of simplicity.

nefftd avatar Jun 11 '14 01:06 nefftd

I'll have to agree; mainly, I only commented on this because in Lua I'd end up awkwardly marking variables as eg function b(--[[Optional]] a)..., and there's no good way to get enforcement. But yes, I am neutral to how to make this possible.

I'm slightly unsure - though - how a macro facility would make something that looks like type annotations in the function 'signature'.

ludamad avatar Jun 11 '14 02:06 ludamad

Here's an example of code possible with LuaMacro:

require_ "proto"

Function bonzo (a: number, b: string) : string
    return a .. b
end

print (bonzo(10,"hello"))
print (bonzo("hello"))  ---> blows up!
-- test-proto.lua:4 type mismatch argument 1: got string, expecting number

The implementation of Function is here:

https://github.com/stevedonovan/LuaMacro/blob/master/tests/proto.lua

The generated code for that function looks like this:

function  bonzo (a,b)
 _assert_arg (a ,1,"number");_assert_arg (b ,2,"string");return a .. b
end

LuaMacro works only on the lexical level, and since Moonscript is lexically similar to Lua, it should be possible to use it with Moonscript.

stevedonovan avatar Jun 11 '14 11:06 stevedonovan