[Feature] Pipe Operator for Function Calls
As seen here https://prog21.dadgum.com/32.html
The suffix "K" to indicate kilobytes in numeric literals. For example, you can say "16K" instead of "16384". How many times have you seen C code like this:
char Buffer[512 * 1024];
The "* 1024" is so common, and so clunky in comparison with:
char Buffer[512K];
I personally think this feature is a small improvement to the numeric literals in Wren :)
While I see great values in having units in a language, I would say no in the proposed syntax as this. The cited example is fine tuned for a very specific and limited usage (assembly does have to deal a lot with allocations sizes).
Maybe can be similar to hex.
0k512
Otherwise if more modular is needed I think adding more methods to the Num class would do for these constants.
I was thinking to something more general like:
import "my_unit" for Ki
512 <operator_here> Ki // Would invoke: Ki.call(512)
Maybe
// A class named constants inside Num.
Num.constants.kilobyte // With static methods with common values such as 1024
Num.constants.kilobyte(512) // A static method with one parameter that will multiply the value by the constant
Your solution is the same as doing:
import "constants" for Constant
Constants.kilobyte // With static methods with common values such as 1024
Constants.kilobyte(512) // A static method with one parameter that will multiply the value by the constant
With the advantage that you are in control.
My solution also put you in control, but with a syntax more near to what you originally planned, and can be reused for other stuff:
import "to_list" for ToList
(0..42) <operator_here> ToList
that reminds me more or less to pipes.
(0..42) |> ToList
In my POC here https://github.com/wren-lang/wren/pull/944 I overloaded the operator in classes.
Maybe here would be an operator only for using call in functions.?
It would make things more functional style, but it is the more extensible sugar syntax I can think of. In extra, if that as importance or extra usage, it would inverse order of evaluation before execution...
If a pipe operator is added |> just as a syntax sugar to function.call() I totally support the idea.
var Kilobyte = Fn.new{|n| n * 1024}
var Print = Fn.new{|s| System.print(s)}
Print.call(Kilobyte.call(512))
Becomes
var Kilobyte = Fn.new{|n| n * 1024}
var Print = Fn.new{|s| System.print(s)}
512
|> Kilobyte
|> Print
At usage, it would definitively be equivalent. The operator has to be accepted or changed, but it gives an extra reason to have that language construction.
I will link to the Pipe proposal for JS https://github.com/tc39/proposal-pipeline-operator
The pipe operator attempts to marry the convenience and ease of method chaining with the wide applicability of expression nesting.
The general structure of all the pipe operators is
value |> e1 |> e2 |> e3, where e1, e2, e3 are all expressions that take consecutive values as their parameters. The|>operator then does some degree of magic to “pipe” value from the lefthand side into the righthand side.
var kilobyte = Fn.new{|n| n * 1024}
var division = Fn.new{|x, y| x / y }
var print = Fn.new{|s| System.print(s)}
# print.call(division.call(kilobyte.call(512), 2))
512
|> kilobyte
|> division(2)
|> print
I don't think it would be a good idea to have something like this built into the language (or even the standard library) partly because of the confusion over whether the prefix kilo should mean 1000 or 1024 (and similarly for mega, giga and friends) - see Wikipedia - but also because what one would probably do now is very simple and flexible:
var K = 1024
System.print(512 * K)
On the general question of working with units, Wren is an OO language and, if - for some particular quantity - there's a significant difference between the units and the actual numbers, we should be thinking in terms of creating a class to represent that quantity.
An example of this is financial applications where, because of difficulties in working with floating point, most folks prefer instead to work in cents (or whatever the sub-unit is called) even though it's tedious and error prone to convert from and to the actual monetary amounts.
Recognizing this, I recently created a Money module in Wren which does all this in the background for me. It was a fair bit of work but I was able to build a lot of flexibility into it such as different thousand separators, decimal points, currency symbols etc.
With regard to whether Wren should have pipes, I think that's a more general question regarding how best to chain function calls. Although I'm not keen on having to use the call method all the time, I'm worried that if we start talking about pipes and other functional stuff such as currying and monads, it will make people feel that Wren is turning into a kind of mini-Haskell and run for the hills!
I don't think it would be a good idea to have something like this built into the language (or even the standard library) partly because of the confusion over whether the prefix kilo should mean 1000 or 1024 (and similarly for mega, giga and friends) - see Wikipedia - but also because what one would probably do now is very simple and flexible:
var K = 1024 System.print(512 * K)On the general question of working with units, Wren is an OO language and, if - for some particular quantity - there's a significant difference between the units and the actual numbers, we should be thinking in terms of creating a class to represent that quantity.
An example of this is financial applications where, because of difficulties in working with floating point, most folks prefer instead to work in cents (or whatever the sub-unit is called) even though it's tedious and error prone to convert from and to the actual monetary amounts.
Recognizing this, I recently created a Money module in Wren which does all this in the background for me. It was a fair bit of work but I was able to build a lot of flexibility into it such as different thousand separators, decimal points, currency symbols etc.
With regard to whether Wren should have pipes, I think that's a more general question regarding how best to chain function calls. Although I'm not keen on having to use the
callmethod all the time, I'm worried that if we start talking about pipes and other functional stuff such as currying and monads, it will make people feel that Wren is turning into a kind of mini-Haskell and run for the hills!
I agree. Having such values inside Wren core may not be a wise idea, if is best to delegate that to an external lib.
For the pipe I think that is just syntax sugar to calling functions. Other functional language constructs would be left to maybe external libs if any, since Wren is OOP after all.
Looking at the JS paper, the placeholder operator seems overkill (introduce too much complexity in the compiler). Instead, I propose that we can guarantee that we can put at minimum a function as second argument:
512 |> Fn.new {|n| n * 1024} |> Fn.new {|s| System.print(s)} // expect: 524288
So the priority level of the operator should be thought with care.
That said, I consider |> to be a placeholder, to ease the discussion. But since the pattern emerged at least twice, it shows there is a real need for such language construction/syntax sugar.
512 |> Fn.new {|n| n * 1024} |> Fn.new {|s| System.print(s)} // expect: 524288
So, vis-à-vis #1128, you can see some value in System.print returning its argument after all :)
Well since there is no chain going further, I didn't thought more about it. For me it does return null as a way to terminate the chain. But the fact, it does return its value is not that important, since we can always chain with something like:
var debug = Fn.new {|value|
System.print(value)
return value
}
512 |> Fn.new {|n| n * 1024} |> debug |> ... // do something else
The fact that it can be compressed to a one-liner is nice indeed, but not that important.
Well, functional or not, I have to admit that I'm quite sold on pipes as an answer to the present mess of nesting function calls.
They seem particularly elegant when the functions are named and being able to dispense with the dreaded .call syntax is, of course, a big bonus :)
I like this way of injecting additional arguments into the pipeline:
512 |> kilobyte |> division(2) |> print
though that only works if the return value of the previous function is the first argument to the next function. Not sure what to suggest if it's the second - perhaps division(2, ).
Another question is how to present multiple arguments to the first function in the chain - perhaps (x, y) would be the most natural and it can't be confused with anything else such as a list or map literal.
I think a pipeline would work best if the accumulator is the first param.
The accumulator is just the return of the previous operation that is passed down the pipeline.
But functions with more params would need to be named since it wont be possible to more than one params without wrapping it in another function.
var division = Fn.new{|acc, div| acc / div}
512
|> Fn.new{|acc| acc * 1024}
|> division(2)
# We need to have a named function for this.
# otherwise if only a single accumulator would be passed around
# we would need a wrapper function
#
# Fn.new{|acc| division.call(acc, 2)}
#
|> Fn.new{|acc| System.print(acc)}
So the idea would be.
- Use the operator
<<value>> |> <<function>>.call()to pass the left side value to a function call in the right side. - To pass more params to the function call will consider the values inside parenthesis
()(similar to methods in classes). This would only be valid if the function is inside a callable variable (named).
Example
100
|> division(2)
Would be translated to
division.call(100, 2)
But functions with more params would need to be named since it wont be possible to more than one params without wrapping it in another function.
Yes, I agree with that. So, if the accumulator was the second or n'th argument to division(or whatever named function was being called), we'd need a wrapper function to express that.
Also thinking a little further, if everything after the first element in the pipeline was guaranteed to be a function, we could perhaps dispense with Fn.new and the example would then become:
512
|> {|acc| acc * 1024}
|> division(2)
|> {|acc| System.print(acc)}
If the first function in the chain took no arguments, that could be expressed as (). in fact, even if it only took a single argument perhaps it would be better to wrap it in parentheses to ease parsing.
Also thinking a little further, if everything after the first element in the pipeline was guaranteed to be a function, we could perhaps dispense with Fn.new and the example would then become.
I agree.
If the first function in the chain took no arguments, that could be expressed as (). in fact, even if it only took a single argument perhaps it would be better to wrap it in parentheses to ease parsing.
I think pipeline functions must be at least have 1 argument that would be the accumulator. The accumulator can be any Wren Valid Value. Like Ints, Strings, Fn. Is the next function in the pipeline to decide what to do with the given acc.
var kb = 512
|> {|acc| acc * 1024}
|> division(2)
System.print(kb) // 262144
I think pipeline functions must be at least have 1 argument that would be the accumulator.
I agree with that for all functions after the first though, if we allowed the first function to take no arguments, then we could (optionally) get rid of call altogether by invoking a function, f, like this:
() |> f
// instead of f.call()
The first function could, of course, still return a value to be passed to the next function (if any) in the pipeline even if it took no arguments itself
() |> f
// instead of f.call()
I think the pipe operator would be best to ease multiple nested function calls.
For a single call I don't know if is better than just using call
Well, it would be a consistent way to make a single call albeit entirely optional.
Remember also that, even in a chain, the first function might not need any arguments as (being a closure) it could produce a return value by manipulating captured variables.
Currently you can pass any arguments to a 0 arity function without Wren complaining.
var print = Fn.new{|s| System.print(s)}
var hello = Fn.new{print.call("Hello Wren")}
hello.call(null)
hello.call(1234)
So if you want you can use the pipe with an empty function
null |> f
Yes, but the reason why that works is because any surplus arguments are ignored - another bone of contention!
null could still be a valid argument to a single parameter function.
If I understand, for the Pipe |> to work both sides must implement the call method (be like functions).
It will first call the left side and use the return value as the first param to the call in the right side.
This would mean that basic types such as Numbers would need to implement the 512.call() that will just return itself.
For
512
|> division(2)
To work.
var result = Fn.new{}
|> piped_fn_1
|> piped_fn_2
If that's the case, then I don't see how we could start with multiple arguments such as (512, 513) because we don't have tuples in the language on which to hang a call() method. We can't use a list [512, 513] because that could be a single argument rather than two.
After chewing this over some more, I think - like it or not - we will have to use a list to pass multiple or no arguments to the first function so we can hang a call() function on it.
If the first function takes 1 argument, then the list will be accepted as that argument.
If the first function takes more than 1 argument, then it will need to be wrappped in another function which takes a single argument (namely the list) and then calls the wrapped function with the appropriate arguments taken from the list.
If the first function takes no arguments, then it will simply discard the list (which could then be the empty list []) as surplus to requirements. This is something which I understand from @mhermier is unlikely to change as it makes the implementation much easier.
I'd be happy with that as I could still do my single parameterless function call with:
[] |> f
As long as we can pass 1 argument we can passe any argument, using the following trick:
class Apply {
construct new(fn) {
_fn = fn
}
call(args) {
var count = args.count
if (count == 0) return _fn.call()
if (count == 1) return _fn.call(args[0])
if (count == 2) return _fn.call(args[0], args[1])
if (count == 3) return _fn.call(args[0], args[1], args[2])
...
}
}
[] |> Apply.new(f)
If list of arguments are accepted as input, we may need to produce list as output to chain. Though, I wonder how it is really usable and if it does not produce a mess...