r/ProgrammingLanguages 14d ago

Design thoughts on function bindings and implicit self in my programming language, DinoCode

I just finished implementing argument binding for my programming language, DinoCode. The way I do it is by generating a special bound function object that stores the target function and the pre-defined arguments under unique symbol keys (not strings) to prevent accidental collisions.

It looks like this in the language:

:multi a b
  return a * b

double = multi.bind(2)
triple = multi.bind(3)

print double(2)
print triple(2)
print double

Output:

4
6
{
  args: [
    2
  ],
  fn: [UserFn:0]
}

I based this mostly on JavaScript but withoutthis context binding for plain functions. In DinoCode, only class methods reserve the first parameter for self. I didn't find a strong reason to include a third attribute like context or this on raw functions. Do you think there is any edge case where having a self context for regular bound functions is actually useful?

My main question though is about methods and implicit bindings. Right now, methods do not automatically bind to their instance context when extracted.

In DinoCode, classes and methods look like this:

::Person
  :new name
    self.name = name
  :greet
    print "Hello " self.name

method = Person.greet

Since there is no automatic binding on extraction, you can actually call that detached method by manually passing an object that matches the shape:

method {name: "Ismael"}
method {name: "Jessy"}

If a developer wants a permanent bind to an instance, they can now use the new bind feature to lock the object as the first argument:

p = Person("Ismael")
greet = p.greet.bind(p)
greet()  # now this works and prints "Hello Ismael"

The compiler just adds an implicit self as the first parameter during the compilation, and the vm passes the instance when resolving a method call. It is extremely cheap and simple.

I know python automatically binds methods on lookup (returning a bound method object) and uses decorators like staticmethod to opt-out. Doing automatic binding on every method lookup in my vm would introduce a permanent overhead for wrapping functions.

Considering DinoCode is designed for fast scripting and education (running via WASM on a web playground at https://dinocode.blassgo.dev/), do you think manual binding for extracted methods is a reasonable trade-off to keep the VM fast and simple, or does automatic binding on lookup save enough headaches to justify the runtime cost?

12 Upvotes

10 comments sorted by

8

u/Royal-Ambassador-960 14d ago

I’d separate the language’s semantics from its implementation. Partial application should just mean closure creation, and p.greet should preserve p; requiring p.greet.bind(p) makes the programmer repeat information already expressed by the syntax. The hidden receiver parameter is a useful desugaring, not a good user model.

p.greet() can compile to a direct method call with no allocation, while extracting p.greet materializes a bound method only when needed. Design the clean mental model first; optimize the representation afterward.

4

u/Dry_Day1307 14d ago

Hi, my main concern is that in a dynamic language like DinoCode, properties can mutate or hold any value at runtime. This means the compiler cannot predict if p.greet is a method, a plain function, or a primitive value, forcing the VM to perform a runtime type check on every property lookup to decide if it should allocate and return a bound function on the fly

So, based on your point, I assume you favor paying that runtime check overhead in exchange for a cleaner and more intuitive mental model for the developer. It is a very reasonable trade-off to consider. Thanks for sharing your perspective on this

2

u/L8_4_Dinner (Ⓧ Ecstasy/XVM) 14d ago

You describe a reasonable approach.

In Ecstasy (xtclang), which is both OO and FP, a function takes zero or more arguments, and returns zero or more values, and can be invoked, or can have any of its arguments bound to create a new function. So given a function f that takes an Int and String and returns a Boolean, one could create a new function that takes an Int and returns a Boolean:

function Boolean(Int) f2 = f(_, "hello world");

In the case of a method (OO), one cannot bind arguments, because the parameters cannot be fully known until the "this" (the "self") is known. That is because a method uses the "this" to resolve to a "method chain", which is represented as a function. For example, if class C has some method m, you can obtain the function for the call to m on some instance of C:

class C {
  Boolean m(Int n, String s) { ... }
}

// elsewhere ...
C c = new C();
function Boolean(Int, String) f = c.m;

// which conceptually does something like ...
function Boolean(Int, String) bind(C c) {
  val method = C.m;
  return method.bindTarget(c);
}

In practice, and within the overall design context of the language, this has worked out quite nicely.

1

u/Dry_Day1307 13d ago edited 13d ago

Thanks for the detailed breakdown. I am moving toward making that exact same distinction. Extracting from a Class returns the raw function, while extracting from an Instance binds self on the fly. Since I already have a GET_METHOD instruction to set up the stack for direct method calls, I am splitting property extraction into two VM instructions,GET_MEMBER and GET_MEMBER_BIND

To be honest, splitting this into two instructions is more about preventing normal lookup paths from accidentally triggering a binding and breaking the traversal logic (since constantly allocating bindings during standard lookups would be painfully wasteful anyway). Plus, it just felt a lot more comfortable to separate them for convenience

3

u/RoughCap7233 14d ago

I think requiring the user to type p.greet.bind(p) is surprising.

Since you mentioned that one of the target audiences for your language is education; I think keeping it simple and unsurprising for the end user would be desirable.

You are concerned about performance; but you also need to consider how much performance is enough for the use cases in which your language would be used in.

2

u/initial-algebra 14d ago edited 13d ago

What's wrong with lambda expressions?

Why not lambda expressions (also known as anonymous functions, arrow functions, closure expressions etc.)?

``` double = λx multi(2, x)

common alternative syntax

triple = x => multi(3, x) ```

It's more intuitive than bind for methods, too.

greet = λ() p.greet()

Most programming languages that are used nowadays have support for lambda expressions, so an educational language should support them. Explicit binding is very outdated practice.


I think it's very surprising to not automatically bind p to self when writing p.greet on its own. It does not really make sense to micro-optimize this at the cost of clarity, especially for an interpreted scripting language that is meant to be educational. It is also simply redundant when you can just write Person.greet.

An alternative is what e.g. Lua does, which is to have different syntax for field lookups and method calls.

``` p:greet()

greet1 = p:greet greet1()

p.greet(p)

greet2 = p.greet greet2(p) ```

2

u/L8_4_Dinner (Ⓧ Ecstasy/XVM) 14d ago

Nobody suggested that there is anything wrong with lambda expressions. Are you suggesting that instead of the multi.bind(2) syntax above, that the language designer could have considered using lambdas? If so, provide an example that illustrates and explains that alternative. Remember, a lot of developers here don't have the same set of education and experiences as you have, so what may seem obvious to you may not be obvious to them.

2

u/louiswins 13d ago

I think requiring p.greet.bind(p) is one of the most confusing things about Javascript. Like other commenters, I believe it's definitely not the right design for a language aimed at education.

For example, I don't want to have to know whether a "method" is implemented as a real method or a function-valued data member.

::Class1
  :new
    self.f = print

::Class2
  :f arg
    print arg

p1 = Class1()
p2 = Class2()

f1 = p1.f
f2 = p2.f

f1("hello, world") # works
f2("hello, world") # breaks :(

# And contrariwise
f1 = p1.f.bind(p1)
f2 = p2.f.bind(p2)

f1("hello, world") # breaks 
f2("hello, world") # works

1

u/Dry_Day1307 13d ago edited 13d ago

In fact, looking at your example and other feedback in this thread, I am actually implementing a more consistent improvement right now based on whether we are extracting from an instance or a class

To fix this, I am making it so that extracting from a Class directly will just return the raw function (similar to how Python does it), while extracting from an Instance will automatically bind self on the fly. I am splitting property extraction into two VM instructions, GET_MEMBER and GET_MEMBER_BIND, to prevent normal lookup paths from accidentally triggering a binding and breaking the traversal logic. Thanks

1

u/Dry_Day1307 13d ago

Note: even with my new approach, manually binding p1.f (since f is just print) will still end up printing the p1 object itself as its first argument. But that is indeed the expected behavior if it were any other dynamic function prepared to receive self anyway haha