wren icon indicating copy to clipboard operation
wren copied to clipboard

[RFC] Introduce top-level static methods

Open PureFox48 opened this issue 3 years ago • 25 comments

The purpose of this proposal is to make procedural programming in Wren easier and more familiar to users of other languages by introducing top-level static (TLS) methods. This is how they'd look compared to top-level functions:

static method(s) {
  System.print(s)
}

var function = Fn.new { |s|
  System.print(s)
}

method("I'm a static top-level method")
function.call("I'm a top-level function")

Unlike functions, TLS methods would not be objects and so they could not be passed to, or returned from, other methods or functions or assigned to variables. However, this means that they could be defined in the same way as class-based static methods and would not need to be invoked with 'call' as functions do to avoid the ambiguities that would otherwise arise (see #1045).

One could perhaps think of them as if they were members of an anonymous top-level class though it would not be a precise analogy.

Other aspects of TLS methods:

  1. Parentheses would always be needed whether they took parameters or not.

  2. They could only be defined at top-level, never within a block or nested scope.

  3. They could be recursive.

  4. They would see other top-level entities in exactly the same way as a top-level function and their internal implementation could therefore be similar.

  5. Class based methods would see TLS methods in the same way as other top-level entities and you would therefore need to observe the usual capitalization rules if you wanted to invoke them from within a class.

  6. This proposal would be backwards compatible as no new keyword would be needed and, at present, the static keyword cannot be used at top-level. Functions would continue to work exactly the same as they do now.

This would, of course, make Wren a bit more complicated but I think it would be worthwhile as I regard the current situation of always having to use top-level functions (and their associated 'call' method) for procedural code to be not only an irritation for many existing users but an obstacle to the wider adoption of Wren for simple scripts.

I look forward to comments.

PureFox48 avatar Jan 30 '22 19:01 PureFox48

Without a top level operator for name resolution, they are not usable inside class members. So till this is sorted out, this change has low interest.

mhermier avatar Jan 31 '22 08:01 mhermier

Well, FWIW, I'd also like to see some sort of mechanism to distinguish top-level scope . Either an operator: ::method or ^method as you suggested yourself a while back or perhaps, less cryptically, Top.method if we were to introduce a reserved word to denote the top-level namespace.

However, the present proposal does not depend at all on such a device or on changing the name resolution rules which we are probably stuck with unless we switch to a multi-pass compiler.

TLS methods would be callable from within a class member if they began with a capital letter just the same as top-level functions (unless you alias them) and other variables are now.

Moreover, the point of this proposal is not to make functions easier to call from within classes but to make procedural code (which may not require classes at all) easier to write and more in keeping with how it's done in most other comparable languages.

PureFox48 avatar Jan 31 '22 09:01 PureFox48

Other than that defect point due to the language name lookup, foreign method on top level can exist with such proposition and should be handled. While I agree it would be a nice syntax, I think we really need to see it in action. Because I wonder if there is not a pitfall we don't envision. In particular since it is not an object, we can't take the value of it, and it might be less re-usable as current functions.

mhermier avatar Jan 31 '22 09:01 mhermier

Although I always use classes in embedded scripts, I think I'm right in saying that the host can use wrenGetModuleVariable to get a reference to a top-level function and so perhaps we could do the same for TLS methods.

As they are not objects, TLS methods wouldn't be as versatile as top-level functions but, in the kind of scenarios I'm envisaging, you wouldn't need that versatility anyway. If you did, you'd have to use a function instead.

PureFox48 avatar Jan 31 '22 09:01 PureFox48

There is also the point of accessing __ variables.

Some of the points, makes me wonder how costly it will be in terms of complexity in the compiler, so I would like to see it in action.

mhermier avatar Jan 31 '22 10:01 mhermier

You wouldn't be able to access __ variables within a class unless they had a 'getter' method.

TLS methods would rely on top-level variables for storing state between calls just the same as top-level functions do now.

Incidentally, if you were thinking that we should allow TLS methods to be foreign without any implementation on the Wren side, I'm not sure that would be a good idea as there would be no class reference unless we invent one especially for the purpose.

PureFox48 avatar Jan 31 '22 10:01 PureFox48

On that point, I think null should be passed till Module are publicly available, that would make things a little bit more round and sound. That way we could have a method:

static thisModule() {
  return this
}

But this goes far beyond the scope of that pull request.

mhermier avatar Jan 31 '22 10:01 mhermier

I assume that Module is something which is being developed inside a private repository as I'm not familiar with it.

But, if the idea is to represent code modules as objects, then it sounds like TLS methods may be congruent with it.

PureFox48 avatar Jan 31 '22 10:01 PureFox48

ModuleObj exist in code, it has no public API. Module is only the public declaration to access that object if that has to be a thing. Top level objects are specials anyway, it would not change much to the situation, and would allow to access (some/all?) Modules methods as TLS methods.

mhermier avatar Jan 31 '22 11:01 mhermier

Ah, I see. Thanks for the explanation.

Incidentally, on the subject of possible pitfalls, there may be an issue with recursive TLS methods.

At present we have this situation with recursive top-level functions:

var fib  // forward declaration needed
fib = Fn.new { |n|
  if (n < 2) return n
  return fib.call(n-1) + fib.call(n-2)
}

var Fib = Fn.new { |n| fib.call(n) }

System.print(fib.call(10))  // 55
System.print(Fib.call(10))  // ditto

So, if the function name begins with a lower case letter, you need a forward declaration but not if it begins with an upper case letter. This, of course, is a consequence of the (currently undocumented) situation I drew attention to in #1066.

Presumably, we'd have the same situation with TLS methods though, as they're not objects, a forward declaration may not be feasible (static fib on its own would have no value). I think we may just have to specify that they'd have to begin with a capital letter if they were recursive.

PureFox48 avatar Jan 31 '22 11:01 PureFox48

Well this is the reason why I think that rule should be dropped for a top level lookup operator. It has so many consideration, that it make it not practical to use, unless you fully follow the PascalCase/CamelCase convention strictly... which is not ideal in some situations.

mhermier avatar Jan 31 '22 11:01 mhermier

Well, the current proposal is much more of an issue for me than the top-level name resolution rules per se as I tend to stick fairly rigidly to naming conventions. However, I agree with you that the latter rules can be a nuisance at times and I'd therefore be strongly in favor of some top-level name resolution mechanism (operator or reserved word) which should ameliorate the situation considerably. Hopefully, something can be done there in the next version.

PureFox48 avatar Jan 31 '22 12:01 PureFox48

I dont know if this is feasible or a good idea but maybe a variable can hold the methods and implicitly be called if no higher priority options are found. As a fallback mechanism checking for the Module__ variable

// now all the methods inside the object would be available
var Module__ = MyFunctions.new()

// would be the same as Modle__.sum()
// it would be implicitly called if no sum() is found in the scope
sum()

Example

class Hello {
  static world() { 
      return "hello world"
   }
}

// bind the static methods
var Module__ = Hello

// Would be the same as Hello.world() or Module__.world()
System.print(world())

Lets say that this comes as a default binding to all modules

var Module__ = System

// now we can always use print() without System

Since this is a normal variable it would only affect the scope of the file only and not globally. and you can export them as any normal variable.

other naming ideas would be

var Procedures__
var Methods__
var Functions__
var Funcs__
var Default__
var Static__
var Kernel__
var Core__
var Global__
var Shadow__
var Exports__
var Context__
class Maths {
  static sum(a, b) {
    return a + b
  }
}
Module__ = Maths
var sum = Fn.new {|a, b| a + b}

// will call the function
sum.call(a, b)

// sum is a function, but since it will crash
// it would look inside the Module__ variable
// and it will call the Module__.sum() method
sum(a, b)

Ok lets stretch the idea a little. how about having a Kernel class that contains the most used functions as a core utilities from System or another utility classes


// class kernel that is the main holder of methods
class Kernel {
  construct new() {}
  print(message) {
    System.print(message)
  }
}


// now my custom methods

class MyKernel is Kernel {
   construct new() {}
   hello { "world"}
}

// now I have both print() and hello available

Module__ = MyKernel.new()

// same as Module__.print(Module__.hello)
print(hello)


clsource avatar Feb 17 '22 02:02 clsource

I don't see why it would not be possible to do so. But I can't decide, if it wise or not. From a technical point of view, it doesn't necessitate a lot hacking into the compiler... But it also does not bring much value as this compared to declaring a global variable. But it can bring value, if we consider it as the lookup if the lookup in the module fails (some sort of module subclassing)

mhermier avatar Feb 17 '22 08:02 mhermier

So if I'm understanding this discussion correctly, the intent is to get module-scoped named functions [1] that bypass the .call() ritual, and that potentially have some other niceties, like recursive calls without a forward declaration [2].

[1] That's "function" in the general computer-science sense of a callable routine with a return value, not limited to the narrower sense Wren uses, and not excluding methods.

[2] The compiler already handles recursive calls within method bodies, without a forward declaration, without multiple compiler passes. Does it not? Installing functions in a module namespace doesn't have to be that different, mechanistically, from installing methods in an object or class namespace.

(BTW, the Module__ examples are a little inconsistent on whether you would have to call a constructor on the class that provides the static methods, or just pass the class itself in. I don't see a good reason for there to be state that lives with (and potentially interacts with) the method implementations.)

A magic fallback variable, as seen in the Module__ proposal, is interesting because not only could you use it at module/file scope, but potentially at narrower scopes too, by shadowing the module-scoped magic variable with a local one (depending on the implementation). Module__ wouldn't be a suitable name anymore, but maybe Fns__ or something.

That way you could have easy access to a suite of, say, linear algebra functions throughout your module, but replace it with a suite of language processing functions in certain classes where that is more relevant.

(This is a little messy conceptually because shadowing happens at compile time, but the actual installation of functions doesn't happen until run time. But it's the same thing that's happening with var x = Fn.new {...} anyway.)

(BTW, the Module__ examples are a little inconsistent on whether you would have to call a constructor on the class that provides the static methods, or just pass the class itself in. I don't see a good reason for there to be state that lives with (and potentially interacts with) the method implementations.)

As proposed, there is the annoying limitation that all the functions you want to install have to be in a class, and you have to install every function in that class, because the magic variable is a scalar. Especially when there's single inheritance and no kind of roles/traits, that makes kind of an inflexible system. But hang on, if the magic variable is a namespace, namespaces are aggregates, not scalars. They're more or less maps, right?

Fns["foo"] { System.print("bar") } -- using a subscript setter and the block argument syntax. Or if you wanted, you could put a stack behind each name, and use Fns.push("foo") {...} and Fns.pop("foo") to change definitions.

Unfortunately, now that we're back to Wren-functions rather than Wren-methods, we lose the ability to dispatch the same name to different implementations depending on the signature. That's a shame.

Of course this is all moot if .call() isn't as necessary as supposed. I know that's not a new discussion, but -- the canonical example is a very specific scenario. You have to have a getter that returns a function, and an extremely good reason to give some other method the same name as that getter. Has anyone ever encountered a reason for doing that?

And even if so, it may be viable to let some expressions accept postfix () while others still require .call() -- let the former group include variable names, maybe field names too, and you're probably satisfying almost all of the demand. I haven't been through the whole discussion about .() yet but I think there were some possibilities there too.

eritain avatar Apr 29 '22 23:04 eritain

(BTW, the Module__ examples are a little inconsistent on whether you would have to call a constructor on the class that provides the static methods, or just pass the class itself in. I don't see a good reason for there to be state that lives with (and potentially interacts with) the method implementations.)

Yes it is not needed to be consistent. Since it will work as long as you can use the method as if you were calling the object. Module__ would be a normal variable at the end.

Example:

// example1.wren

class MyClass {
  static myfunc {4}
}

// we bind the a class with only static methods
var Module__ = MyClass

// Now we can use
Module__.myfunc

An instance can be binded too

// example2.wren
class OtherClass {
  construct new() {}
  myMethod { 4 }
}

// Now we have to bind the instance
var Module__ = OtherClass.new()

Module__.myMethod

As proposed, there is the annoying limitation that all the functions you want to install have to be in a class, and you have to install every function in that class, because the magic variable is a scalar. Especially when there's single inheritance and no kind of roles/traits, that makes kind of an inflexible system. But hang on, if the magic variable is a namespace, namespaces are aggregates, not scalars. They're more or less maps, right?

Is not needed to all the functions be on a single class. Since you can return a class within a class. Its only needed a class that is like an "index" with accessor methods for the other classes.


class Other {
 static two { 2 }
} 

class MyClass {
  static other { Other }
}

var Module__ = MyClass

Module__.other.two

clsource avatar Apr 30 '22 14:04 clsource

I thought about something a little bit more problematic, such functions does not seems to fit the import mechanism. While the named variable solution gives a possibility, I find the import syntax a little bit hackward...

mhermier avatar May 02 '22 07:05 mhermier

Wren is an object oriented language, what is the point of 'make procedural programming in Wren easier'? If you want to write procedural code, C is there for you.

HallofFamer avatar May 02 '22 21:05 HallofFamer

When I talked about 'procedural' programming in my opening post, I was thinking chiefly about the sort of short scripts one typically writes in Wren-CLI or the REPL which don't really need classes as such.

Sure, you can think up a class name and put a few static methods into it but, to me (and I suspect others), that's just a lot of noise when all you really need are standalone functions. In recognition of that, Wren allows us to write such functions though, as these are themselves objects, you need to create an instance of them and then use the 'call' method to invoke them which again seems a lot of unnecessary ceremony and may be off-putting to new users.

All I'm trying to do with this proposal is to find a way of declaring and invoking functions in a similar way to how you'd do it in a purely procedural language such as C or in a multi-paradigm language such as C++.

This doesn't mean I'm against the 'functions as objects' concept we have at present - I'm not, they're great when you need to pass functions around as 'first class' entities. TLS methods would simply sit beside them.

It's worth noting that, although class-based, Wren isn't a pure OO language in any case as you can still write top-level procedural code. It's not like Java for instance where you need a public 'start-up' class containing a static 'Main' method.

PureFox48 avatar May 03 '22 08:05 PureFox48

One aspect I omitted to cover in my opening post was whether or not TLS methods could be overloaded. In other words would something like this be allowed:

static method(s) {
  System.print(s)
}

static method(s, t) {
  System.print([s, t])
}

method(1)
method(1, 2)

I see no reason why this shouldn't be allowed as overloaded TLS methods could be distinguished by their 'arity' in the same way as class-based static methods.

If so, then this would give them an advantage over top-level functions which cannot be overloaded - you need to use a different name.

PureFox48 avatar May 05 '22 08:05 PureFox48

Having overload is nice indeed.

The main issue is how we resolve the symbols from within the methods/ functions body and for imports (which may be the more difficult one). If a concessus for that is reached, all the goodness will follow.

mhermier avatar May 05 '22 10:05 mhermier

As far as imports of overloaded TLS methods are concerned, since you'd just be importing the method name - not its full signature - I think the only sensible solution would be for it to import all methods of that name. So the following would be fine:

import "./otherModule" for method

method(1)
method(1, 2)

but this would not be producing a "Module variable already defined." error:

import "./otherModule" for method

static method(s, t, u) {
  System.print([s, t, u])
}

method(1)
method(1, 2)
method(1, 2, 3)

However, you get around this by using an 'as' clause:

import "./otherModule" for method as otherMethod

static method(s, t, u) {
  System.print([s, t, u])
}

otherMethod(1)
otherMethod(1, 2)
method(1, 2, 3)

So I don't think this would be an insuperable problem.

PureFox48 avatar May 05 '22 12:05 PureFox48

As this using regular imports introduce a lots of exception in the compiler. While it should be feasible to some extends, I don't think it is a correct solution as this.

Maybe some thing like import "foo" for static bar seems much more simple to handle in the compiler.

mhermier avatar May 05 '22 13:05 mhermier

In recognition of that, Wren allows us to write such functions though, as these are themselves objects, you need to create an instance of them and then use the 'call' method to invoke them which again seems a lot of unnecessary ceremony and may be off-putting to new users.

In this case I believe it is better if Wren just finds a way to add syntactic sugar function(arg) or function.(arg) which degugars to function.call(args), which is a better idea than to implement another language construct that does similar things but differ in a somewhat subtle way. For your static method idea, it is not a first class function, thus users who expect it to behave like a function object will be surprised by why you cannot pass it as a parameter or return from a method. I dont like second class language constructs, they complicate the language and dont fit well with other first class citizens.

It's worth noting that, although class-based, Wren isn't a pure OO language in any case as you can still write top-level procedural code. It's not like Java for instance where you need a public 'start-up' class containing a static 'Main' method.

A lot of people tend to think that 'pure OO' = 'everything is inside class', this is not correct at all. Wren may not be a pure OO language but its not because you can write top level code, Smalltalk allows you to do that and it is a pure OO language. Also Java is not a pure OO language, it has primitive values, also it doesnt have metaclasses so its classes are not objects themselves.

HallofFamer avatar May 05 '22 14:05 HallofFamer

@mhermier

If it would make life easier for the compiler if we distinguish 'static' imports from normal imports then I'd be fine with that and it might even mean that the second example in my previous post would now be valid because static method(_,_,_) would have a different signature from the two imported versions.

@HallofFamer

If there were a satisfactory way to get rid of call rather than introduce a new language construct, then I'd be more than happy but there just doesn't seem to be one. We looked in depth at the possibility of making call optional in #1045 but couldn't really make that work because of the ambiguities it introduced.

Going right back to #158 where the syntax function.(arg) was suggested, that looks very odd to me and I think newcomers to Wren would probably find it odd as well. Frankly, if that were the only alternative, I'd prefer to stick with function.call(arg).

I actually do regard Wren as an OO language (pure or not) as it is very close in concept to Smalltalk. But without wanting to get into a 'more OO than thou' discussion, I don't think that introducing TLS methods would compromise that description very much as one could think of them as methods of an anonymous top-level class. Admittedly they wouldn't be 'first class' but I don't see that as a problem when functions would continue to work as they do now.

PureFox48 avatar May 05 '22 16:05 PureFox48

I've decided to withdraw this proposal which doesn't appear to have much support anyway.

There are some aspects which I think would make it difficult to implement and people may find it confusing to have to choose between functions and TLS methods when a class is not needed.

Thanks for the discussion, everybody.

PureFox48 avatar Oct 29 '22 09:10 PureFox48