poke-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[RFC] Methods and functions


From: Jose E. Marchesi
Subject: [RFC] Methods and functions
Date: Sat, 02 May 2020 17:37:19 +0200
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/28.0.50 (gnu/linux)

Hi people!
Right now, Poke supports both regular functions and struct methods.

Regular functions are defined like this:

  defvar n = 10;
  defun foo = (int a, int b) int: { return n + a + b; }

Struct methods are defined in a very similar way, inside a struct type
definition:

  defvar n = 10;
  deftype Foo =
   struct
   {
     defvar x = 5;
     int c;
     defun bar = (int a, int b) int: { return n + x + a + b + c; }
   };

Despite being syntactically identical, there are substantial differences
between functions and methods.

Both functions and methods can access their lexical environment (which
gets captured in a closure at function definition time) in their body,
by referring to variables.

In the example above, foo refers to `n', `a' and `b' which are variables
stored in its lexical environment.  The variables are accessed at
run-time using the `pushvar' instruction, that gets a lexical address.
The expression `n + a + b' gets translated to:

        pushvar 0x2, 0x45
        pushvar 0x1, 0x0
        addi    
        nip2    
        pushvar 0x1, 0x1
        addi    

Where 0x2,0x45 refers to `n', 0x1,0x0 refers to `a' and 0x1,0x0 refers
to `b'.

In the example above, bar also refers to the variables `n', `a' and `b'
in the lexical environment.  In addition, it refers to `x', which is
another variable defined inside the struct type definition.  In all
these cases, it uses pushvar instructions exactly like in foo.

However, bar is a method.  This is because it is a function defined in a
struct type definition.  As a method, it can be invoked using
dot-notation once we have a struct value of that type.  For example,
using a struct constructor:

  (poke) Foo {c = 22}
  Foo {
    c=22
  }
  (poke) (Foo {c = 22}).bar (1, 2)
  40

It is obvious that when the compiler finds `c' in the expression `n + x
+ a + b + c' in bar it cannot handle it as a variable in its lexical
environment.  This is is because the lexical environment of the struct
constructor (also struct mappers) is unique per struct type.

Instead, methods get the struct they are called on as an implicit
argument.  Therefore, in  the call:

  (poke) defvar s = Foo { c = 22 }
  (poke) s.bar (1,2)

The method `bar' gets the struct value `s' as an implicit argument.
When a field is referred inside a method (like `c' in bar above) code
like this gets generated:

        pushvar 0x1, 0x2
        push    "c"
        sref    

Where 1,2 is the lexical address of the struct implicitly passed to the
method, and sref accesses the field `c'.

Methods can also be invoked from other methods, like in:

  deftype Foo =
    struct
    {
      byte a;
      byte b;

      defun method1 = void: { ... }
      defun method2 = void: { ... method1; }
    };

The compiler recognizes that the refernce to `method1' is a method of
the same struct, and does the right thing, i.e. to invoke it passing the
same implicit argument that was passed to `method2'.

Advantages of this approach:

- It is very natural to refer to fields from methods, and also to other
  methods, in exactly the same way than if they were variables in the
  lexical environment.  This avoids the need to use notations like
  `self.c' or `self.method1'.

- We don't need to introduce an additional `defmethod' construction.  It
  is very easy to determine if a given defun defines a function or a
  method, just by looking where it appears in the program.

However, there are also some restrictions, which are enforced by the
compiler:

- Methods can't be referenced as variables in field constraints.  For
  example, consider this:

  deftype Foo =
    struct
    {
      defun prime_p = (int n) int: { ... }
      int i : prime_p (i);
    };

  In the above, prime_p is invoked in the constraint expression at
  construction/mapping time, but since it is a method it would require
  to be passed a struct of type Foo, that doesn't exist already!  The
  compiler will error an "invalid reference to method".

- Likewise, methods can't be referenced as variables in optional field
  conditions.  For example:

  deftype Foo =
    struct
    {
      defun prime_p = (int n) int: { ... }
      int i if prime_p (i);
    };

  The compiler will also error an "invalid reference to method" in this
  case.

- Likewise, methods can't be referenced as variables in initialization
  expressions of variables inside the type:

  deftype Foo =
    struct
    {
      defun prime_p = (int n) int: { ... }
      defvar xxx = prime_p (120);
      ...
    };

   The compiler will also error an "invalid reference to method" in this
   case.

- Fields cannot be referenced as variables in non-method functions.
  Methods can of course contain other functions, like in:

  deftype Foo =
    struct
    {
      int f;
      defun foo = int:
        {
          defun bar = int:
          {
            return f * 100;
          }

          return f + bar;
      }
    };

  In this example, foo is a method, but bar is NOT a method.  Therefore,
  the reference to `f' in bar is invalid, and it is recognized as such
  by the compiler, that emits a "references to struct fields are only
  valid in methods" error.

- Likewise, methods cannot be referenced as variables in non-method
  functions.   Therefore:

  deftype Foo =
    struct
    {
      int f;
      defun quux = (int n) int: { }
      defun foo = int:
        {
          defun bar = int:
          {
            return quux (10);
          }

          return bar;
      }
    };
  
   In this example, the compiler also emits the "references to struct
   fields are only valid in methods" error.

IMO these restrictions are quite natural and easy to understand, once
the user knows that defuns immediately defined as "fields" of struct
types are actually methods and not functions.

I'm quite happy with this.

BUT, I am considering the following: what about introducing explicit
methods separated from functions.

In this alternative approach, we would have the ability to define both
regular functions and methods in struct types:

  deftype Foo =
    struct
    {
      defun prime_p = (int n) int: { ... }

      int f1 : prime_p (f1);
      int f2;

      defmethod frob = void: { ... }
    };

The function `prime_p' in the example above would be a regular function,
and therefore it could be invoked in the constraint expression of the
field f1, as shown.  It woudn't be visible from outside, i.e. you
couldnt do:

  (poke) defvar f = Foo {}
  (poke) f.prime_p (10)  /* ERROR */

On the other hand, `frob' would be a method, having all the properties
and restrictions that we already support for methods, described above,
including the ability to refer to fields as normal variables, etc.  It
would be of course visible from "outside":

  (poke) defvar f = Foo {}
  (poke) f.frob()

What makes me hesitate to follow this approach is that we would need to
introduce a whole new additional set of restrictions, this time for
regular functions defined inside struct types:

- Functions cannot refer to methods as variables.
- Functions cannot refer to struct fields as varaibles.
- ... and so on, basically the same restrictions that exist for methods,
  but reversed.

My current opinion is that it would complicate the language, for little
benefit: after all, it costs nothing to have prime_p defined outside of
the struct type:

  defun prime_p = (int n) int: { ... }

  deftype Foo =
    struct
    {
      int f1 : prime_p (f1);
      int f2;

      defun frob = void: { ... }
    };

So I don't plan to implement support for both regular functions and
methods inside struct types.  I think the current approach is simple,
intuitive, and predictable, given the user has access to some decent
documentation.

But I am very interested in feedback.
What do you think? :)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]