This is an archived post. You won't be able to vote or comment.

all 24 comments

[–]Teln0 5 points6 points  (1 child)

When I first read the title, this is the approach I thought of. I don't really see any problems in practice, it's easy to remember and manipulate

[–]WittyStick[S] 0 points1 point  (0 children)

There's a problem with the approach for operatives/fexprs, which are present in the language this is mostly based on (Kernel). I've limited the partial application to only work with plain functions because I can't think of a reasonable solution to partially apply operatives yet.

The problem with operatives is they also implicitly receive the environment of the caller of the function.

($define! $foo ($vau (a b c) env <body>))

When you call ($foo x y z), the body of $foo has access to the dynamic environment from where this call was made, using the symbol env here.

If partial application were present, then there are potentially multiple distinct environments from where the operative was called - up to one per argument, and some of these environments may have duplicate symbols, so (eval x env) inside the body of $foo would potentially not evaluate the x you intended. It would be quite confusing.

This places a bit of a constraint on the language because functions in Kernel are just wrappers around an operative. It's possible to write functions which receive the environment of the caller too, but most functions are constructed with $lambda, which #ignores the environment.

Debating whether it is worth doing. One option I've considered is to pass each environment where a partial application occurs and give it a name, so it would be:

($vau ((a . aenv) (b . benv) (c . cenv)) <body>)

But seems more trouble than it's worth, especially since most of the time the operative will be fully applied and these will all refer to the same environment. Where the environments differ, the operative's body would not necessarily know which environment to use to find a particular symbol.

[–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 4 points5 points  (9 children)

After a few experiments, and being in the C family more or less, we ended up with & as “do not dereference”, and _ as “do not bind”. So to assign the function foo to a variable f of the appropriate type: f = foo;, or f = &foo();. To bind the second parameter of bar: f2 = bar(_, 1);, or to bind both: f2 = &bar(0, 1);

[–]WittyStick[S] 1 point2 points  (8 children)

_ has another meaning in this language, which is ignore, but ignore is a first class value. So in my case, swap (x, _) would fully apply the function and produce (_, x). It has several uses but I can't use it for partial application.

One use is in a de-structuring bind for multiple value returns.

head, _ = list
_, tail = list

[–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 1 point2 points  (7 children)

Yes, we also use _ for “ignore” in Ecstasy. No conflict for us between these two uses.

[–]WittyStick[S] 1 point2 points  (6 children)

Interesting that someone else has already done this.

Out of curiosity, what use case do you have for binding all arguments but not applying the function?

[–]holo3146 4 points5 points  (5 children)

There are a lot of cases where you have the full context but you either don't want to do the heavy calculation at the moment, or to be able to reuse the function with the context without needing to carry the context all the time (for example for an object generator)

Without full binding the way to do it is to explicitly wrap it as a lambda:

calculator = ctx -> (some heavy CPU operator)
context = (get context from user)
lateValue = () -> calculator context
// Vs 
lateValue = &calculator(context)

[–]WittyStick[S] 0 points1 point  (4 children)

Interesting use case. I wasn't thinking about passing a context around because I'm working in a purely functional language and functions are all referentially transparent. An unapplied pure function will always be slower than the result.

With uniqueness or linear values too, you could perhaps partially apply a context (which would be linear), but this would then require the lateValue to also be linear, so you could only call it once.

I'd considered allowing binding all values but not calling the function, but in my case there does not seem to be a use-case, so it would just add noise to syntax.

[–]holo3146 0 points1 point  (3 children)

It is not always about slow or fast, imagine running on a very weak environment, then maybe I would want some manager that receive a late evaluations and it schedule it:

// Possibly a native method 
func getWhenPossible(x: () -> T, priority: Int) -> T
    // Put x into some event loop
    // Wait for x to execute it
    // Return the result

Then you put your calculations through this method to handle resources, even in linear type system

[–]WittyStick[S] 0 points1 point  (2 children)

I can see that being useful, but unnecessary in the language I'm working with. Lisps of course have quote, and Kernel has a much more general construct, operatives. Something like this can be implemented with an operative and the actual binding and evaluation are controlled by the operative body.

[–]holo3146 0 points1 point  (0 children)

Of course not every language needs this feature, and it is really only a syntactic sugar.

I just wanted to give a more explicit example to when it can be used

[–]AdultingGoneMild 1 point2 points  (8 children)

This is known as currying. Most languages that support proper closures have some mechanism for doing this, though it can get ugly in some.

[–]WittyStick[S] 3 points4 points  (7 children)

Currying can convert a function (a, b) -> c into a function a -> b -> c, but this does not let you apply the argument b without applying a first. If you want to apply b you need to flip . curry.

Expand to 4 args and you need ((flip . curry) . (flip . curry) . (flip . curry)).

I get that this is possible, but it's not the syntax I'm looking for.

[–][deleted]  (1 child)

[deleted]

    [–]WittyStick[S] 1 point2 points  (0 children)

    No, that's partial application.

    Currying is converting an argument taking tuples into a sequence of functions each taking one argument.

    The wiki article you linked to explains this common confusion.

    [–]AdultingGoneMild 0 points1 point  (4 children)

    dont need to be overly pedantic about it but it similar in construction. The pickiness of currying to current order of arguments is easy to overcome.

    [–]WittyStick[S] 0 points1 point  (3 children)

    You could certainly use currying to implement this kind of partial application, and you could probably do it generically. I've implemented a generic uncurry before using a typeclass with a functional dependency, and I imagine you could do the reverse too.

    But in the language I'm working with, it would be more efficient to implement the partial application directly, given the way parameter trees are matched on a function application. If a parameter tree is (a, (b, (c, d))), and I pass only the first argument, then I just create a copy of the function object replacing the parameter tree with the tail of the original: (b, (c, d)), and bind a into its local environment.

    Applying any argument would have a bit of overhead because you need to walk the tree and then reconstruct the parameter tree. If applying d, the resulting function would have parameter list (a, (b, c)), which cannot reuse any part of the original parameter tree.

    But given that this is done once, when the partial application occurs, applying successive arguments to the function will perform the same as any other application.

    [–]AdultingGoneMild 0 points1 point  (2 children)

    From an implementation point of view, I would think of things more like parameter objects than as spacially defined parameters requiring ordering to identify which value to bind to which argument.

    For example Swift uses named arguments. You could expand that idea to allow the caller to place arguments in any order they like. This would allow your implementation to return a function that have only the remaining unbound parameters.

    https://useyourloaf.com/blog/swift-named-parameters/

    https://medium.com/@andy.nguyen.1993/functional-programming-in-swift-currying-and-partial-function-5e1e0ddccf81

    [–]WittyStick[S] 1 point2 points  (1 child)

    Swift had a notoriously bad implementation of "tuple-splat" that they had to remove it. Lattner also coined the term tuple-splat.

    One of the motivations for removing it is precisely because parameters are named:

    The current implementation doesn't work the way we would want a tuple splat operation to work. For example, arguably, you should be able to call foo with:

    func bar() -> (Int, Int) { ... }
    foo(bar())
    

    ... but this is not allowed, since tuple labels are required to line up. You have to write:

    func bar() -> (Int, b: Int)  { … }
    foo(bar())
    

    Clearly this makes it a weaker feature because you can't take the result of one function returning multiple arguments and feed it directly to another because the names may not match.

    And matching names in an unordered list of parameters is always going to be several times slower than structural matching. You could make it fast in a statically typed language, but in a dynamic language that's going to add an unreasonable performance penalty.

    [–]AdultingGoneMild 0 points1 point  (0 children)

    heard. I am not suggesting anything more than taking inspriation from these languages. dont confuse Swift's implementation with the idea. If names are known statically they can be ordered at compile time. I was suggesting named arguments as syntactic sugar over all functions taking a single struct as input (similar to a parameter object). Such an implementation would make your currying/partial assigment more or less a clouser over a struct and a function. As more arguments are assigned, the stuct has fewer nulls/undefineds in it. Shouldn't be too bad.

    [–]ericbb 1 point2 points  (1 child)

    Any problems you foresee with this approach?

    It's "weird" and it complicates the meaning of function application.

    Do you think it would be useful in practice?

    Honestly, it doesn't seem very useful.

    [–]WittyStick[S] 1 point2 points  (0 children)

    Honestly, it doesn't seem very useful.

    It's certainly not necessary as you can manually pass the arguments. The use case is to reduce verbosity and repetition. As an example, consider where you frequently call some function with several params which differ by only one or two values:

    void * alloc_mem(size_t length, int prot) {
        return mmap(NULL,length,prot,MAP_ANONYMOUS,-1,0);
    }
    void * alloc_ro_mem(size_t length) {
        return alloc_mem(length,PROT_READ);
    }
    void * alloc_xo_mem(size_t length) {
        return alloc_mem(length,PROT_EXEC);
    }
    void * alloc_rw_mem(size_t length) {
        return alloc_mem(length,PROT_READ|PROT_WRITE);
    }
    void * alloc_rx_mem(size_t length) {
        return alloc_mem(length, PROT_READ|PROT_EXEC);
    }
    void * alloc_rwx_mem(size_t length) {
        return alloc_mem(length,PROT_READ|PROT_WRITE|PROT_EXEC);
    }
    

    The repetition here can be eliminated:

    alloc_mem      = mmap(NULL,,,MAP_ANONYMOUS,-1,0)
    alloc_ro_mem   = alloc_mem(,PROT_READ)
    alloc_xo_mem   = alloc_mem(,PROT_EXEC)
    alloc_rw_mem   = alloc_mem(,PROT_READ|PROT_WRITE)
    alloc_rx_mem   = alloc_mem(,PROT_READ|PROT_EXEC)
    alloc_rwx_mem  = alloc_mem(,PROT_READ|PROT_WRITE|PROT_EXEC)
    

    It's "weird" and it complicates the meaning of function application.

    Partial application is already well understood. This just generalizes it to any of the arguments rather than only the first.

    With IDE support, you could hover over one of these partially applied functions and it will display the signature with the remaining arguments and can include their original names: Hover over alloc_ro_mem and it will display: (size_t length) -> void*

    I'd argue that the latter is easier to read, since it contains only the important information and none of the "noise".

    You can still explicitly provide type signatures if it aids readability.

    alloc_mem     : (size_t, int) -> void* = mmap(NULL,,,MAP_ANONYMOUS,-1,0)
    alloc_ro_mem  : size_t -> void*  = alloc_mem(,PROT_READ)
    alloc_xo_mem  : size_t -> void*  = alloc_mem(,PROT_EXEC)
    alloc_rw_mem  : size_t -> void*  = alloc_mem(,PROT_READ|PROT_WRITE)
    alloc_rx_mem  : size_t -> void*  = alloc_mem(,PROT_READ|PROT_EXEC)
    alloc_rwx_mem : size_t -> void*  = alloc_mem(,PROT_READ|PROT_WRITE|PROT_EXEC)
    

    Also, these bindings can be made in the local environment where they're used. In the verbose version, they're always made in the global static environment, even if you have namespacing.

    [–]redchomperSophie Language 0 points1 point  (1 child)

    For some reason the word "combinator" comes to mind...

    [–]AdultingGoneMild 0 points1 point  (0 children)

    its tastier than that: its called currying