After years I understand the idea behind the proposal of PR 24990. It wants to bring partial application (or currying) to Julia which is much overdue.
I thought previously, _
is merely a crutch to be eventually used for iteration over multiple data values or pointwise operation through data with the same function. With the new Forum post by user uniment , I understand that Scala’s idea behind f(_, x, _) actually is currying where _
is a placeholder for a non-applied argument. _
turns the call into a mere assignment of arguments to parameters and suppresses the call.
problems with _
As your discussion on Github pointed out, this idea by itself has several problems:
- for growing number of arguments,
_
makes the syntax “explode” linearly in the number of function arguments - It looks like you need to know the number of arguments for the call which prevents generic programming (usage in generic function libraries)
- the entire problem about the scope of
_
(beyond the first pair of parentheses or not)
the alternative
There is a second (I think better) syntax (with various different ideas) to achieve the same without the requirement of denoting missing argument places:
a specific function “call” operator which assigns arguments but suppresses the call. Unfortunately, Julia missed the opportunity to generalize function call syntax. Arrays, Hashtables, Functions are abstractly the same thing, just different in terms of mutability. In consequencce, the brackets are not available anymore.
Before suggesting a solution below, I’d like to show what could have been the ideal if currying and partial application would have been a thought from the start:
-
If the brackets could be used, one could have written argument assignment as
f[x][param=y]
orf[x, param=y]
which looks surprisingly similar to substitution syntax in mathematics:f[param1\x, param2\y]
. Then you could writef[x][y][z]()
which is literally equivalent tof[x,y,z]()
. -
Shifting positional arguments would have been possible by using parameter indices for unnamed parameters
f[ 3 => x, y ]
orf[ _3 = x, y ]
instead off(_, _, x, y)
.
Next option would have been the member access operator in conjunction with function call syntax f.(x, y)
which is not available either.
concept
But what about
f.[ x ].[ y ]
= f.[ x, y ]
?
It looks like being invalid syntax in the REPL, free for a meaning, right now. There is also an intuitive mnemonic: [ … ]
defines a mapping and accessing it with .
applies this map to the variable in keyspace (parameter or member space, the codomain) – merely replacing the symbol by a map, in order to map new arguments to old arguments. In correspondence to [ … ]
– which modifies the variable in value space (variable content, return value) – .[ … ]
modifies the function or object interface as if it would be a hashtable or an array and may even allow mutation of the return value for specific patterns.
[spoiler]Maybe you would prefer f.{ x }
. If the operator is not supposed to be assignable, .{ … }
is closer to immutability. Whereas f.[ … ]
looks like it could be assigned. It also looks like it allows for arbitrary expressions inside the brackets. There could also be a reason to reserve .[ … ]
for other things: for example expressions evaluated in the context of an object. In this example, the expression may refer to variables in member space and use lenses so that point.[ .x, .y ] = v, w
is equivalent to point.x = v, point.y = w
, point.[ .x + .y ]
being point.x + point.y
, point.[ .x, .y = v, w ]
would be a mathematical substitution like a reference assignment, not a value assignment, or point.[ ::Int, ::Float64 ] = n, x
(assigning n to the first Int member, x to the first Float64 member). End of excourse.
Whatever version people prefer (if any at all), I will just use .[…]
for demonstration.[/spoiler]
underscore revisited
For specific positional currying, it could still have an optional non-applied argument placeholder _
and/or dictionary like keys. In that case, the _
is scoped by the brackets .[ … ]
automatically, problem solved. For everything else, dictionary key expressions could be used (which is preferable in most cases).
Possible notation(s)
-
f.[ _, y, _, x ]
which is_1, _2, params... -> f(_1, y, _2, x, params...)
-
f.[ 4 => x, 2 => y; … ]
#=>
maps an index expression, parameter or member symbol (expression) to a value- instead
->
is known to map an argument pattern to a new value
[spoiler]The relation is, if an input pattern should be prevented, rewriting notation like mathematical substitution) could be allowed, such as
f.[ (7 -> 1 , 3 -> 0) ]
which maps an input to another input, i.e. that a call with argument = 7 –f(7)
– is dynamically deferred tof(1)
,f(3)
would be deferred tof(0)
. This is mainly useful for fallback arguments, for example to handle out of bouns accesses or illegal argument types viaf.[ _ -> 0 ]
where_
is only matched as pattern if the function or object did not find an accessed member or the current parameter did not match the argument. This is perfect for on-the-fly-interpolation views.Instead, the notation
f.[ 7 ] = 1
could remap argument value 7 to return value 1. Supporting this would enable pattern matching definitions as known from functional languages.[/spoiler] - instead
-
f.[ _4 = x, _2 = y ; … ]
- another syntax option using positional parameter names
_index
; these names could also be private and inaccessible from outside - if disambiguation is necessary, people can use
._index
instead which unambiguous - the positional parameter name
_
refers to the next parameter name that hasn’t been accessed yet within the current scope of brackets (automatic index)f.[ _ = x, _ = y ]
is equivalent tof.[x, y]
. Positional parameter names, which are unassigned, will not be applied but still may be used.f.[ _1 , _1 ]
therefore would bex -> f(x, x)
whereasf.[ _ , _1 ]
would rather correspond tof.[ _2 , _1 ]
.
- another syntax option using positional parameter names
semicolon
The last two notations could allow for further arguments after the semicolon
. At first it seems inconsistent with the current convention which puts parameterized names after the semicolon but contrary to that, it is related to the parenthesized expression notation as found in x -> (y = 3; x + y)
.
limits
Anything more complex – which requires internal parameter names or nested anonymous functions – should use the existing arrow syntax which exists exactly for the reason to define paramter names and for more complex scoping.
foo_square_sum = (_foo, _array) -> sum( x -> _foo(x)^2 , _array )
definitions by examples
Without semicolon, the following arguments (without explicit argument position) do automatically increase in position
f.[ 3 => x , w , y, z ]
could mean _1, _2, params... -> f( _1, _2, x,w,y,z, params...)
While by using the semicolon, the following expression is reset to the smallest yet unassigned index.
f.[ 3 => x ; w , y, z ]
would be equivalent to f.[ 3 => x ][w, y, z]
i.e. f(_, _, x, _)( w, y, z, &_ )
in terms of notation mentioned in the initially-linked post.
Multiple expressions could be possible, each value being assigned to parameter(s):
f.[ 3 => x ; 2 => y, z ; w ]
is equivalent to f.[ _, _, x].[ _, y , z ].[ w ]
which would lower to params... -> f(w, y, x, z, params...)
.
The expression before the semicolon mainly is useful to define variables (or members) that can be used after the semicolon. .
prefixed names inside the expression are treated as lenses (i.e. member accesses inside the current context). The dot is used to disambiguate symbols or external variables from member names and also alludes to the member access.
-
f.[ .param = sqrt(x); .param, .param ]
is equivalent toparams... -> (temp = sqrt(x); f( temp, temp, params...; param=temp)
-
f.[ foo = _ + 3; foo, foo ]
could turnfoo
into a scoped local variable:(_1, params...) -> ( foo = _1 + 3; f.[foo, foo, params...] )
- assigned variables are ignored for argument application unless they start with
.
. Reason: flexibility
- assigned variables are ignored for argument application unless they start with
-
point.[ .z = 1 ]
extends a 2D point to homogenous coordinates butpoint.z
might be immutable this way
conclusion
At the end, _
, ._index
, .member
, =>
and ->
is compatible enough to allow for arbitrary combination. I expect skepticism, also because of number of new ideas involved which maybe don’t fit into how seasoned Julian’s do programming. But any subset of these could be chosen in theory in combination with .[ … ]
. It would even be an improvement to have just .[ … ]
with no further feature on top.
Implementing these abstraction would not only itself be quite some work but particularly making these abstractions performant enough for the high demands of special users. In my opinion, usability should still prevail performance by far, so performance should not play too much of a role in the discussion except for when the impact is dramatic. Optimization and readability/maintainability is a trait off.