Closures/first-class functions are a pain in the ass. #439

Open
nddrylliog opened this Issue Jun 17, 2012 · 32 comments

Projects

None yet

4 participants

@nddrylliog
Member

This will be a fun lecture in 'ooc design gone terribly wrong' and why premature optimization is always evil.

Yajit

When we first thought of first-class functions in ooc (that potentially captured context), we went for the most complex solution available: generating machine code at runtime, that would push on the stack the correct arguments for the context, and then the function's argument. yajit was born. (Pronounced 'jah-jeet' by our german friends @fredreichbier and @showstopper, apparently).

We had portability problems with that, it wouldn't work on all architectures, and it was generally a bit wasteful (generating machine code on the run every time you pass a closure is kinda expensive). But it had the nice advantage of giving a simple function pointer that could then be passed to any C function.

@nddrylliog
Member

Closure struct

Then we moved to another system, which is what rock master has (and what rock conspiracy still has for the time being): a simple C struct with two fields: a pointer to the context, and a pointer to the function's entry point.

Here's the struct definition in the lang/core.ooc

Closure: cover {                                                                                                                                                          
    thunk  : Pointer                                                                                                                                                      
    context: Pointer                                                                                                                                                      
}

This has been working out pretty well in practice: it's less expensive than yajit was, and as far as C compatibility goes, most (if not all) APIs have support for a 'user_data' pointer. When registering callbacks with C APIs, you usually just had to split the closure struct in its two fields and pass them in whatever order was required.

For example, in the GLib API, g_idle_add's prototype looks like this:

guint g_idle_add (GSourceFunc function, gpointer data);

Which can be declared in ooc as:

g_idle_add: extern func (thunk, context: Pointer) -> GUInt

Then if you already have a closure struct you can simply do the following

doStuff: func (closure: Closure) {
  g_idle_add(closure thunk, closure context)
}

And it will get called properly.

@nddrylliog
Member

The current situation sucks for interfacing with C libraries

The following code shows why:

// retain references to context struct to avoid them to be collected
gcHack := ArrayList<Pointer> new()

_GObjectStruct: cover from GtkObject
_GObject: cover from _GObjectStruct* {

    connect: func ~nodata (signalName: String, callback: Func) -> GULong {
        c: Closure = callback as Closure
        // FIXME: this will leak, we haven't added a way to remove callbacks
        gcHack add(c context)
        g_signal_connect_swapped(this, signalName, GTK_SIGNAL_FUNC(c thunk), c context)
    }

    connectNaked: func (signalName: String, context, callback: Pointer) -> GULong {
        closure: Closure* = gc_malloc(Closure size)
        closure@ thunk   = callback
        closure@ context = context
        // FIXME: this will leaaaaak
        gcHack add(closure)

        g_signal_connect_swapped(this, signalName,
        GTK_SIGNAL_FUNC(nakedThunk), closure)
    }


    connectKeyEvent: func (signalName: String, callback: Func (Pointer)) -> GULong {
        closure: Closure* = gc_malloc(Closure size)
        // FIXME: this will leaaaaak
        gcHack add(closure)
        memcpy(closure, callback&, Closure size)

        g_signal_connect_swapped(this, signalName,
        GTK_SIGNAL_FUNC(keyEventsThunk), closure)
    }
}

Yeah. You read that right. It's just simply an incredible clusterfuck. (It comes from ooc-gtk, btw)

@nddrylliog
Member

The current solution sucks for pure ooc code too

What if you just want to store a list of callbacks? Or do a proxy between a callback and another? Here's how it's done in ooc-uv:

WrappedFunc: class {                                                                                                                                                      
    closure: Closure*                                                                                                                                                     

    init: func (c: Closure) {                                                                                                                                             
        closure = gc_malloc(Closure size)                                                                                                                                 
        closure@ thunk = c thunk                                                                                                                                          
        closure@ context = c context                                                                                                                                      
    }                                                                                                                                                                     
}                                                                                                                                                                         

wrap: func (f: Func) -> WrappedFunc {                                                                                                                                     
    WrappedFunc new(f as Closure)                                                                                                                                         
}              

Here's how you wrap a function:

    connect: func (sockaddr: SockAddrIn, callback: Func(Int, Stream)) -> Int {                                                                                            
        // TODO: make new request, call uv_tcp_connect                                                                                                                    
        connect := gc_malloc(Connect_s size) as Connect                                                                                                                   
        connect@ data = wrap(callback as Func)                                                                                                                            
        uv_tcp_connect(connect, this, sockaddr@, _connect_cb)                                                                                                             
    }  

And here's how you unwrap and call it:

    _connect_cb: static func (handle: Connect, status: Int) {                                                                                                             
        callback := handle@ data as WrappedFunc                                                                                                                           
        f: Func(Int, Stream) = callback closure@                                                                                                                          
        f(status, handle@ handle)                                                                                                                                         
    }

Every single twist here (the 'as Func' even though the type is already a function, a rhs with a type different from the lhs, allocating a closure struct on the heap to copy (manually) the one allocated on the stack) is necessary to get this to work properly.

Isn't that just plain terrible? It makes you want to avoid first-class functions / ooc altogether.

@nddrylliog
Member

Even the typesystem is fucked up.

Wanna see something fun?

This code:

doStuff: func {}                                                                                                                                                          

main: func {                                                                                                                                                              
    f: Func = doStuff                                                                                                                                                     
}                                                                                                                                                                         

produces this code:

void typesystem__doStuff() {                                                                                                                                              
}                                                                                                                                                                         

lang_Numbers__Int main() {                                                                                                                                                
    GC_INIT();                                                                                                                                                            
    typesystem_load();                                                                                                                                                    
    lang_core__Closure f = (lang_core__Closure) {                                                                                                                         
        typesystem__doStuff,                                                                                                                                              
        NULL                                                                                                                                                              
    };
}

Fun eh? And when you call it, the C code is:

((void (*)(void*)) f.thunk)(f.context);

Even though we reference a simple top-level function with no context at all, we still do the whole wrapping/unwrapping dance.

What if you use Func as a generic type parameter, like this?

doStuff: func {}                                                                                                                                                          

main: func {                                                                                                                                                              
    c := Cell<Func> new(doStuff)                                                                                                                                          
    c val()                                                                                                                                                               
}

No, wait, you can't because rock says: no such function "c val()". So you actually have to cheat:

doStuff: func {}                                                                                                                                                          

main: func {                                                                                                                                                              
    c := Cell<Func> new(doStuff)                                                                                                                                          
    f: Func = c val                                                                                                                                                       
    f()                                                                                                                                                                   
}

Which, again, is pretty terrible. It generates code like this:

lang_Numbers__Int main() {                                                                                                                                                
    GC_INIT();                                                                                                                                                            
    typesystem_load();                                                                                                                                                    
    lang_core__Cell* c = lang_core__Cell_new((lang_core__Class*)lang_core__Closure_class(), (uint8_t*) &(typesystem__doStuff));                                           
    lang_core__Closure f = c->val;                                                                                                                                        
    ((void (*)(void*)) f.thunk)(f.context);                                                                                                                               
    return ((lang_Numbers__Int) (0));                                                                                                                                     
}

Which is not even fucking valid C code! If you do want to generate valid C code you have to do something like that:

main: func {                                                                                                                                                              
    c := Cell<Func> new(doStuff)                                                                                                                                          
    cl: Closure = c val as Closure                                                                                                                                        
    f: Func = cl                                                                                                                                                          
    f()                                                                                                                                                                   
}

Which generates the following C code:

lang_Numbers__Int main() {                                                                                                                                                
    GC_INIT();                                                                                                                                                            
    typesystem_load();                                                                                                                                                    
    lang_core__Cell* c = lang_core__Cell_new((lang_core__Class*)lang_core__Closure_class(), (uint8_t*) &(typesystem__doStuff));                                           
    lang_core__Closure cl = (* (lang_core__Closure*)c->val);                                                                                                              
    lang_core__Closure f = cl;                                                                                                                                            
    ((void (*)(void*)) f.thunk)(f.context);                                                                                                                               
    return ((lang_Numbers__Int) (0));                                                                                                                                     
} 

Which does compile... and segfault, rightfully! You see, the simplest way to make it work correctly is to do this:

main: func {                                                                                                                                                              
    val: Func = doStuff
    c := Cell<Func> new(val)
    cl: Closure = c val as Closure
    f: Func = cl
    f()
}      

Which generates the following C code:

lang_Numbers__Int main() {                                                                                                                                                    GC_INIT();                                                                                                                                                                typesystem_load();                                                                                                                                                    
    lang_core__Closure val = (lang_core__Closure) {                                                                                                                       
        typesystem__doStuff,                                                                                                                                              
        NULL                                                                                                                                                              
    };                                                                                                                                                                    
    lang_core__Cell* c = lang_core__Cell_new((lang_core__Class*)lang_core__Closure_class(), (uint8_t*) &(val));                                                           
    lang_core__Closure cl = (* (lang_core__Closure*)c->val);                                                                                                              
    lang_core__Closure f = cl;                                                                                                                                            
    ((void (*)(void*)) f.thunk)(f.context);                                                                                                                               
    return ((lang_Numbers__Int) (0));                                                                                                                                     
}  

Which, freaking finally, works as intended.

@nddrylliog
Member

What about defaults? Doing this is fine:

onAlways: func (f: Func) {
  f()                                                                                                                                                                   
}

main: func {
  onAlways(|| "All is well" println())                                                                                                                                  
}

But if you don't care about the callback you should be able to write stuff like this:

onAlways()

How do you implement that?

onAlways: func (f: Func = null) {                                                                                                                                         
    if (f) f()                                                                                                                                                            
}

The code above generates invalid C code: since Func is actually Closure in disguise, which is a struct, we can't just go ahead and assign null to it.

We can actually cheat our knowledge of the system (but seriously, I'm the only one fucked up enough to find that kind of workaround) and use a struct initializer with a tuple of nulls. You also need to change the if to check if the thunk itself is null:

onAlways: func (f: Func = (null, null) as Closure) {                                                                         
    if (f as Closure thunk) f()                                                                                              
}                                                                                                                            

main: func {                                                                                                                 
    onAlways(|| "All is well" println())                                                                                     
    onAlways()                                                                                                               
}

Which produces the following, terrifying C code:

void default__onAlways(lang_core__Closure f) {                                                                               
    if (((lang_core__Closure) (f)).thunk) {                                                                                  
        ((void (*)(void*)) f.thunk)(f.context);                                                                              
    }                                                                                                                        
}                                                                                                                            

lang_Numbers__Int main() {                                                                                                   
    GC_INIT();                                                                                                               
    default_load();                                                                                                          
    default__onAlways((lang_core__Closure) {                                                                                 
        default____default_closure219,                                                                                       
        NULL                                                                                                                 
    });                                                                                                                      
    default__onAlways((lang_core__Closure) {                                                                                 
        NULL,                                                                                                                
        NULL                                                                                                                 
    });                                                                                                                      
    return ((lang_Numbers__Int) (0));                                                                                        
}                                                                                                                            

void default____default_closure219() {                                                                                       
    lang_string__String_println(__strLit115);                                                                                
}

But at least it does the right thing.

One would think you could do it in a simpler way, e.g.:

onAlways: func (f: Func = ||) {                                                                                         
    f()                                                                                                                      
}  

But that's actually invalid syntax: || for anonymous function parameter lists is only valid as a function argument - we could make that syntax work, but it's kinda irrelevant.

We can also try this:

onAlways: func (f: Func = func {}) {                                                                                         
    f()                                                                                                                      
}  

Which produces invalid C code...

    default__onAlways(void default____default_closure201() {});

What the fuck rock? Did you seriously just write an inline function declaration in C? We ain't using GNU extension and even if it were we're not in sweet ooc land where everything is possible.

However, you can make it work like this:

noop: Func = func {}                                                                                                         

onAlways: func (f: Func = noop) {                                                                                            
    f()                                                                                                                      
}

or like this:

noop: func {}                                                                                                         

onAlways: func (f: Func = noop) {                                                                                            
    f()                                                                                                                      
}

And if rock was a little less braindead about unwrapping function declarations, you could probably even do that:

onAlways: func (f: Func = (noop := func {})) {                                                                                            
    f()                                                                                                                      
}

But since actually a sane person can't be reasonably expected to come up with any of these, it's really not satisfying.

@nddrylliog
Member

How to kiss it where it's sore

I suggest that we turn Closure from a cover (struct) into a class. And that we also rename it to Func.
And that slim functions should really just be pointers, which we can cover as CFunc for convenience, just like we do for String vs CString.

Which would give us:

CFunc: cover from Pointer

Func: class <Context> {
  thunk: CFunc
  context: Context
  init: func (=thunk, =context)
  call: func (args: ...) {
    thunk(context, args)
  }
}

When an anonymous function is created, we generate a context (probably a struct, but not necessarily), and a thunk, that takes the address of the context along with an instance of ooc Varargs. Since ooc varargs contain types, this allows us to check that we're calling functions with the right types at runtime (ask me more about this later).

@nddrylliog
Member

For ooc code

It's super-duper friendly now. Since Func is an object (and thus a reference), you can use it in Cells, Lists, HashMaps, etc. without problems. You can assign it to null and compare it with null! You can store it anywhere with the certainty that its context won't be collected randomly by the GC.

A simple callback system would look like this:

EventEmitter: class {
  listeners := MultiMap<String, Func> new()
  on := listeners put(_, _) // fictitious syntax but I really wish that would exist
  yield: func (eventName, args: ...) listeners eachFor(eventName, |listener|
    listener(args)
  )
}

Again, it's type-checked so if we registered the wrong kind of listener we'll have an exception at runtime (hint: look at getNextType() from VarArgs, it can be used in the generated thunks to check types at runtime.)

@nddrylliog
Member

Woops, forgot the example of the event system:

Request: class {
  init: func (f: Func(Response)) {
    on("response", f)
  }

  run: func {
    // do stuff ... let's say we have an unauthorized request
    yield("response", Response new(:statusCode => 401))
  }
}

(By that you can deduce that 1) I want symbols, 2) we need a convenience method to turn varargs of Tuples into Hashbags, 3) I'm high as a kite)

@nddrylliog
Member

For C code

It's really really cool too. We still have a thunk and a context, only now the type system is consistent, so going back to our g_idle_add example you can now do:

doStuff: func (f: Func) {
  g_idle_add(f thunk, f context)
}

doStuff(|| "Zzzzz...." println())

and it'll work just like before, without any trouble (and without extraneous casting and stuff).

@nddrylliog
Member

Convenience stuff

We could have implicit conversion between CFunc and Func, and if CFunc is qualified correctly with argument types, the generated thunk could even do the type-checking like for native ooc anonymous functions.

Oh, and CFunc would work like a normal C function pointer, ie:

doStuff: func {}

cf: CFunc = doStuff
cf()

at no point should generate a structure.

Other stuff

Oh, and by the way, in the new implementation, this:

doStuff: func {}                                                                                                                                                          

main: func {                                                                                                                                                              
    Cell<Func> new(doStuff) val()                                                                                                                                 
}

Should work out of the box.

@duckinator
Collaborator

Sounds pretty damn awesome. But two things:

First: Would this break any old code, excluding all code that's a flaming pile of shittastic hacks?

Second: Wrt Cell<Func> new(doStuff) val() -- that seems a bit unclear to me. Even if it works perfectly fine I'd probably personally use [...] val call() just so I know what the hell is going on.

EDIT: Wouldn't that generate just about the same code, either way?

@nddrylliog
Member

First: Would this break any old code, excluding all code that's a flaming pile of shittastic hacks?

Breakage seems unavoidable, but just exactly how much and how painful the migration is going to be stays approximate to this day.

Second: Wrt Cell new(doStuff) val() -- that seems a bit unclear to me. Even if it works perfectly fine I'd probably personally use [...] val call() just so I know what the hell is going on.

Yeah, I wouldn't write code like that either, but in any case, it should work because it's correct.

@duckinator
Collaborator

Well... On a scale of 1 to 10, with 10 being as bad as CString/String, where do you expect it to be? I'm betting probably a 3-4?

Also: even if it breaks a lot of shit, I think that this would likely be worth it. It simplifies things a ton. Perhaps if it's really that bad, we could split off a rock-0.x so people will just grumble instead of cry?

EDIT: Or....well....I guess a rock-0.x release off master right before merging in the conspiracy branch would be ok... ;)

@nddrylliog
Member

Well... On a scale of 1 to 10, with 10 being as bad as CString/String, where do you expect it to be? I'm betting probably a 3-4?

Also: even if it breaks a lot of shit, I think that this would likely be worth it. It simplifies things a ton. Perhaps if it's really that bad, we could split off a rock-1.0 so people will just grumble instead of cry?

EDIT: Or....well....I guess a rock-1.0 release off master right before merging in the conspiracy branch would be ok... ;)

Yeah, we're moving forward even if it pisses off people. I'll be glad to help them with the transition, but what I described above (if we don't find major drawbacks with it) will be the standard for 1.0

@duckinator
Collaborator

Derp, s/rock-1.0/rock-0.x/g above. _fixes_

@shamanas
Collaborator

Sounds good!
I have a couple of questions however.
First of all, you say that the call method will take C varargs and do type-checks at runtime.
So essentially, a function type will also store a number an array of types of its arguments and of its return types, which will still be determined by the function type (eg Func(Int)) right?

I also think this has the potential to be really fun. One could do something like this:

MyFunc: class <Context> extends Func <Context> {
    new: static func(f: Func) -> This { f as This }

    call: func(args...) {
        intercept(args)
        super(args)
    }
}

extend Func {
    _ : MyFunc { get { MyFunc new(this) } }
}

f: func(g: Func(Int)) {
    g(42) // This will call MyFunc call :D
}

f((|x| x toString() println()) _)
@fredreichbier
Member

I got a bit carried away from the ooc land (and I'm not fully awake yet), so please correct me if I'm wrong. This all sounds awesome, but I'm not sure about the C interfacing: In your example, g_idle_add will call thunk with one parameter, context. But if thunk is a function expecting a context and an ooc varargs object, won't that lead to problems since GLib doesn't pass an ooc varargs object? It would be even worse if there were additional arguments passed.

@shamanas
Collaborator

By the way, I have thought of another potential flaw.
In the code above, Func call does not return anything. The only logical thing to make sure it does in native ooc is using an additional generic type, thus making the code look like that:

CFunc: cover from Pointer

Func: class <Context, Return> {
    thunk: CFunc
    context: Context
    init: func (=thunk, =context)
    call: func (args: ...) -> Return {
        thunk(context, args) as Return
    }
}

Now, this looks fine BUT consider tuples. In ooc, tuples are not true types and therefore cannot be passed as generic arguments. As such, it would make first class functions impossible to return tuples.

Thats all for now :P

@nddrylliog
Member

First of all, you say that the call method will take C varargs and do type-checks at runtime.
So essentially, a function type will also store a number an array of types of its arguments and of its return types, which will still be determined by the function type (eg Func(Int)) right?

I talked about ooc varargs, not C varargs :) And yes, that's how ooc varargs work: the layout works like this, let's say you pass an Int, a String, and a Char, the layout would work like this:

[  Int_class()  ][     Int      ][  String_class() ][Pointer to char][  Char_class()   ][char]
     8 bytes          4 bytes          8 bytes           8 bytes           8 bytes      1 byte

(That's on 64-bit with 32-bit ints)

And yes it would be insanely fun :)

(Update: woops, fixed class pointer sizes, thanks @fredreichbier)

@fredreichbier
Member

(side note: If 64-bit with 32-bit ints means that pointers are 8 bytes, wouldn' the _class() values in your example be 8 bytes, too?)

@nddrylliog
Member

I got a bit carried away from the ooc land (and I'm not fully awake yet), so please correct me if I'm wrong. This all sounds awesome, but I'm not sure about the C interfacing: In your example, g_idle_add will call thunk with one parameter, context. But if thunk is a function expecting a context and an ooc varargs object, won't that lead to problems since GLib doesn't pass an ooc varargs object? It would be even worse if there were additional arguments passed.

Ah, you're right. We'll probably need two thunks then, one that takes ooc varargs and one that takes regular C arguments (to be passed to C functions). But honestly maybe it's simpler to do a thunk yourself in that case (that calls the ooc one), like this:

GLib: class {
  idleAdd: func (f: Func) {
    g_idle_add(_idle_cb, f)
  }

  // private stuff

  _idle_cb: static func (arg1: SomeType, arg2: OtherType, userData: Func) {
    userData call(arg1, arg2)
  }

(note that idle callbacks probably don't have arg1 and arg2 but it's just to show you how it would work with arguments).

This pattern is used in ooc-uv, see here: https://github.com/movies-io/ooc-uv/blob/master/source/uv.ooc#L51

(But right now it's a pain in the ass because you can't just pass a Func, you have to wrap it)

@nddrylliog
Member

@shamanas:

Now, this looks fine BUT consider tuples. In ooc, tuples are not true types and therefore cannot be passed as generic arguments. As such, it would make first class functions impossible to return tuples.

Well I guess we have to make tuples real types then? We'll have to think about that. But I'm not really worried about return types (and even less so about tuple return types) for callbacks, as async-style doesn't really care about return values ;) (except maybe an int for status).

@shamanas
Collaborator

@nddrylliog Ah yes, I meant ooc varargs, oops :P
(Ive also worked with them quite a bit, dont worry ;D)

Btw, I think we could implement variadic generics (bring on the pain!) to help us with that and to be an all-around nice feature (take a look at the feature requests issue, I commented there)

@fredreichbier
Member

Concerning the arguments thing: The custom-thunk solution looks a bit boilerplate-codeish, but I guess it's still the better solution compared to the "two thunks" thing. So +1 here!

Still, one disadvantage is that we would have to rely on C libraries having a user data pointer. Or wait, couldn't we still use yajit with the Func-as-class solution?

@shamanas
Collaborator

@nddrylliog The thing is, closures are not only meat to be used for async callbacks... Think how many calls to map, filter or contains the rock codebase has...
I am for making tuples "real" types btw, laid out as simple C structures, it would be quite nice ;)

@nddrylliog
Member

Concerning the arguments thing: The custom-thunk solution looks a bit boilerplate-codeish, but I guess it's still the better solution compared to the "two thunks" thing. So +1 here!

Still, one disadvantage is that we would have to rely on C libraries having a user data pointer. Or wait, couldn't we still use yajit with the Func-as-class solution?

Well, it's very little boilerplate compared to most high-level languages when calling into C functions (and, worse, passing a callback), so I guess we're fine.

We already rely on C functions have a user data pointer btw, and I think it's a reasonable expectation: cite me one serious C library that doesn't have that.

I think yajit was a lot of fun but it should probably be left in peace. It has served enough ;)

@shamanas
Collaborator

@nddrylliog

I am starting to have doubts about this, at least with the approach taken now.
First of all, let's preface this by saying I am (my body is/should be ? w/e) really tired, as I've slept for like 3 hours in the last 32 - 33 hours, so if I am rambling and not making any sense just cut me off and call out my bullshit.

However, I do believe I have a point. Let's take a look at the current implementation of the func2 Func class:

/**
 * Function types
 */
CFunc: cover from Pointer

Func: class <Context, Return> {
    thunk  : CFunc
    context: Context

    init: func (=thunk, =context)
    call: func (args: ...) -> Return {
        thunk(context, args)
    }
}

Here, we can clearly see that the thunk C function expects two parameters:

  • A context, which is an anonymous structure created by rock the same way as it has always been
  • A VarArgs structure, created by rock when calling Func call and then passed to the C thunk

When we call f(args) rock automatically replaces that call with f call(args). If, additionally, generic types must be passed, the call looks like this: f call(K, V, args)
And this is the final (thunk) call: f thunk( AST { contextVars }, VarArgs { K, V, args })

Then, the thunk is supposed to check that the arguments provided are compatible and call the "true" function generated by rock.
Here is an example of a thunk generated right now from my func2 branch, for a function that uses two generic types, K and V and expects an argument of each type:

__structs_HashMap_closure291_thunk(context: __structs_HashMap_closure291_ctx@, arguments: VarArgs) -> Void {
K : Class
V : Class
arg0 : K
arg1 : V
iter : VarArgsIterator = arguments iterator()
if (arguments count != 4 || (!iter getNextType() inheritsFrom__quest(ClassDecl Class) && (K = iter next(Class) || true)) || (!iter getNextType() inheritsFrom__quest(ClassDecl Class) && (V = iter next(Class) || true)) || (!iter getNextType() inheritsFrom__quest(K) && (arg0 = iter next(K) || true)) || (!iter getNextType() inheritsFrom__quest(V) && (arg1 = iter next(V) || true))){raise("Invalid arguments passed to function") }
__structs_HashMap_closure291(context this, arg0, arg1)
}

So, why is this bad (imho)?

Well, thanks for asking!
I think we are doing things too complicated... And by we, I mean I, because I may very well have missed your original design by miles.
Runtime checks should NOT be necessary and do not serve a purpose...
Also, this will make passing ooc Func's to C a real pain in the ass.
The thunk will simply not be compatible, as the C library will try to call thunk(context), bypassing the VarArgs needed to specify arguments.

So, what do I think should be done?
Clearly, the current implementation sucks. What I essentially want is the following:
Func basically being Closure*
By that I mean, the same thunk and context as before are generated, but our first class functions are scalar type, thus enabling us to do things like bind them to null etc.

Essentially, the following things should be done:

  • Instead of having something like (Closure) { thunk, context } generated to qualify a function, we have Func_new(thunk, context)
  • When calling f(args...), the call is essentially f->thunk(f->context, args...)

We keep the C interfacing flexibility of the current system and add to it more ooc capabilities :D
Sounds good, right?
... Right? D:

@nddrylliog
Member

@shamanas I don't know why I haven't replied to that earlier - and I apologize for that. That's indeed an interesting point but my primary objection to doing it was that it introduced one more level of indirection when
calling a closure. Ie. instead of doing f.thunk(f.context), we'd have f->thunk(f->context).

I really can't tell how much of a performance impact it would have though, would anyone be
so kind as to test it? (Micro-benchmarks are evil, yes, but..)

@shamanas
Collaborator

@nddrylliog By the way, I think this is working on my 'func3' branch.
I was also attempting to fix the generic bug (Cell new(function) val() not working) but I failed (it may have something to do with #425.
I think that rock never figures out the true type of a generic access, just its generic type.

@nddrylliog
Member

Yup, historically rock finds out the "True types" fine on function calls, but not so much on variable access.

@shamanas
Collaborator

Sounds like something I could (try) to fix! o/

@nddrylliog
Member

Yup :) You might want to stay up to date with the changes I'm making in branches though - I'll commit a backwards-compatible, yet needs-latest-rock version of os/Terminal soon, so I'll make a new bootstrap for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment